Recordais cuando iba a intentar ser consistente a la hora de escribir en el blog? JA.

En fin, hoy os vengo a hablar de un detalle que me rompio la cabeza durante una temporada y he conseguido solucionar de manera relativamente eficaz.

Quien haya trabajo con HBase y su shell, se habra encontrado con los siguientes datos alguna vez:

ROW            COLUMN+CELL
111222333      column=FAM1:ID, timestamp=1611977689866, value=111222333
111222333      column=FAM1:DAT1, timestamp=1611977689866, value=DAT1
111222333      column=FAM1:DAT2, timestamp=1611977689866, value=\x00\x00\x01t\xD1\x93\x1Fp

Y que demonios es lo que pone ahi? Vais a vuestro codigo que se ha encargado de insertar un registro en HBase y veis que lo habeis guardado como un Long (que en Java ocupa 8 bytes)… pero eso no son 8 bytes.

Sin embargo, podeis encontrar los bytes de vuelta si se ven de la siguiente manera:

\x00 \x00 \x01 t \xD1 \x93 \x1F p

Resulta que como muestra la informacion por pantalla HBase es mediante arrays de caracteres que ademas no son “type-aware”, y resulta que escapa en hexa todos los caracteres que son ASCII imprimible, y deja tal cual caracteres que no puede imprimir. Si miramos que rango de caracteres se encuentra dentro de ASCII imprimible, vemos que son 128 caracteres (00 a 7F), pero sospecho que no transforma caracteres de 00 a 1F ya que son simbolos de control, usando realmente 20 a 7F. Basicamente, si puede pasarlo a ASCII imprimible, pasara el byte a ASCII, y si no puede, lo dejara en hexa.

Vamos a hacer el ejercicio de pasar a mano a transformar de vuelta estos bytes al long original, primero pasando a un array de bytes unsigned en decimal:

\x00 --> 00h, 000d
\x00 --> 00h, 000d
\x01 --> 01h, 001d
t    --> 74h, 116d
\xD1 --> D1h, 209d
\x93 --> 93h, 147d
\x1F --> 1Fh, 031d
p    --> 70h, 112d

Quedandonos el array siguiente:

[0,0,1,116,209,147,31,112]

Solo nos quedaria multiplicar los valores por su termino correspondiente (hacer un shift de bytes):

000*2^(7*8) --> 0
000*2^(6*8) --> 0
001*2^(5*8) --> 1099511627776
116*2^(4*8) --> 498216206336
209*2^(3*8) --> 3506438144
147*2^(2*8) --> 9633792
031*2^(1*8) --> 7936
112*2^(0*8) --> 112

SUM = 0 + 0 + 1099511627776 + 498216206336 + 3506438144 + 9633792 + 7936 + 112 = 1601243914096

A los que esten relativamente puestos con tiempo Unix, veran que es un triste timestamp en ms (en concreto del 27 de noviembre de 2020).

Pero hacer esto a mano es un poco infierno. Seguro que hay alguna manera de hacer esto programaticamente, verdad? Pues os traigo 2 soluciones de mi propia cosecha, 1 en JavaScript y otra en Scala, donde introduciendo el string que nos da HBase nos devuelve el querido long. Cada implementacion tiene sus caracteristicas (JavaScript no se lleva excesivamente bien con trabajar a nivel de byte, y hay que tirar de un ArrayBuffer, y hay que tener cuidado con que si es unsigned o signed bytes, ya que para Java sus primitivos son con signo), pero hacen bastante bien su trabajo. Os lo dejo a continuacion.

JavaScript

function toBytes(array){
    var arr = new ArrayBuffer(8);
    var view = new DataView(arr);
    for (i in array) {
        view.setUint8(i, array[i], false);
    }
    return view.getBigInt64();
}
 
function expandHBaseHex(input){
    if (input.length > 2){
        var p1 = input.substr(0,2);
        var p2 = input[2].charCodeAt(0);
        return [parseInt("0x"+p1), p2]
    }
    else return [parseInt("0x"+input)]
}
 
function HBaseHexToString(str){
    var a = str.replace(/\\x/, "").split("\\x");
    console.log(a);
    var b = a.flatMap(expandHBaseHex);
    console.log(b);
    var c = toBytes(b);
    return c;
}
 
HBaseHexToString("\\x00\\x00\\x01t\\xD1\\x93\\x1Fp") // escapar las barras

Scala

import java.nio.ByteBuffer
 
def HBaseHexToBytes (str: String) = {
  val t1 = str
    .replaceFirst("^\\\\x", "")
    .split("\\\\x")
    .flatMap{ x =>
      if (x.length > 2) { // tiene parte de hex + parte char
        var p1 = x.substring(0,2)
        var p2 = x.apply(2).toHexString
        Seq(Integer.parseInt(p1,16),Integer.parseInt(p2,16))
      }
      else Seq(Integer.parseInt(x,16))
    }
  t1.map(_.toByte)
}
 
def transformHBaseHexToLong(str: String) = ByteBuffer.wrap(HBaseHexToBytes(str)).getLong()
 
transformHBaseHexToLong("\\x00\\x00\\x01t\\xD1\\x93\\x1Fp")

Y hasta aqui el articulo de hoy! Espero que os haya sido de utilidad, y si no, pues que al menos os resulte interesante. Hasta la proxima!