urbit-ob

JavaScript utilities for phonemic base wrangling.
git clone git://git.jtobin.io/urbit-ob.git
Log | Files | Refs | README

co.js (10466B)


      1 // ++  co
      2 //
      3 // See arvo/sys/hoon.hoon.
      4 
      5 const BN = require('bn.js')
      6 const chunk = require('lodash.chunk')
      7 const isEqual = require('lodash.isequal')
      8 
      9 const ob = require('./ob')
     10 
     11 const zero = new BN(0)
     12 const one = new BN(1)
     13 const two = new BN(2)
     14 const three = new BN(3)
     15 const four = new BN(4)
     16 const five = new BN(5)
     17 
     18 const pre = `
     19 dozmarbinwansamlitsighidfidlissogdirwacsabwissib\
     20 rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\
     21 holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\
     22 losdilforpilramtirwintadbicdifrocwidbisdasmidlop\
     23 rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\
     24 ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\
     25 ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\
     26 sivtagpadsaldivdactansidfabtarmonranniswolmispal\
     27 lasdismaprabtobrollatlonnodnavfignomnibpagsopral\
     28 bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\
     29 taclabmogsimsonpinlomrictapfirhasbosbatpochactid\
     30 havsaplindibhosdabbitbarracparloddosbortochilmac\
     31 tomdigfilfasmithobharmighinradmashalraglagfadtop\
     32 mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\
     33 nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\
     34 laptalpitnambonrostonfodponsovnocsorlavmatmipfip\
     35 `
     36 
     37 const suf = `
     38 zodnecbudwessevpersutletfulpensytdurwepserwylsun\
     39 rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\
     40 lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\
     41 pyldulhetmevruttylwydtepbesdexsefwycburderneppur\
     42 rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\
     43 secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\
     44 selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\
     45 syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\
     46 lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\
     47 bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\
     48 tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\
     49 bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\
     50 wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\
     51 nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\
     52 remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\
     53 lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
     54 `
     55 
     56 const patp2syls = name =>
     57      name.replace(/[\^~-]/g,'').match(/.{1,3}/g)
     58   || []
     59 
     60 const splitAt = (index, str) => [str.slice(0, index), str.slice(index)]
     61 
     62 const prefixes = pre.match(/.{1,3}/g)
     63 
     64 const suffixes = suf.match(/.{1,3}/g)
     65 
     66 const bex = (n) =>
     67   two.pow(n)
     68 
     69 const rsh = (a, b, c) =>
     70   c.div(bex(bex(a).mul(b)))
     71 
     72 const met = (a, b, c = zero) =>
     73   b.eq(zero)
     74   ? c
     75   : met(a, rsh(a, one, b), c.add(one))
     76 
     77 const end = (a, b, c) =>
     78   c.mod(bex(bex(a).mul(b)))
     79 
     80 /**
     81  * Convert a hex-encoded string to a @p-encoded string.
     82  *
     83  * @param  {String}  hex
     84  * @return  {String}
     85  */
     86 const hex2patp = (hex) => {
     87   if (hex === null) {
     88     throw new Error('hex2patp: null input')
     89   }
     90   return patp(new BN(hex, 'hex'))
     91 }
     92 
     93 /**
     94  * Convert a @p-encoded string to a hex-encoded string.
     95  *
     96  * @param  {String}  name @p
     97  * @return  {String}
     98  */
     99 const patp2hex = (name) => {
    100   if (isValidPat(name) === false) {
    101     throw new Error('patp2hex: not a valid @p')
    102   }
    103   const syls = patp2syls(name)
    104 
    105   const syl2bin = idx =>
    106     idx.toString(2).padStart(8, '0')
    107 
    108   const addr = syls.reduce((acc, syl, idx) =>
    109     idx % 2 !== 0 || syls.length === 1
    110       ? acc + syl2bin(suffixes.indexOf(syl))
    111       : acc + syl2bin(prefixes.indexOf(syl)),
    112   '')
    113 
    114   const bn = new BN(addr, 2)
    115   const hex = ob.fynd(bn).toString('hex')
    116   return hex.length % 2 !== 0
    117     ? hex.padStart(hex.length + 1, '0')
    118     : hex
    119 }
    120 
    121 /**
    122  * Convert a @p-encoded string to a bignum.
    123  *
    124  * @param  {String}  name @p
    125  * @return  {BN}
    126  */
    127 const patp2bn = name =>
    128   new BN(patp2hex(name), 'hex')
    129 
    130 /**
    131  * Convert a @p-encoded string to a decimal-encoded string.
    132  *
    133  * @param  {String}  name @p
    134  * @return  {String}
    135  */
    136 const patp2dec = name => {
    137   let bn
    138   try {
    139     bn = patp2bn(name)
    140   } catch(_) {
    141     throw new Error('patp2dec: not a valid @p')
    142   }
    143   return bn.toString()
    144 }
    145 
    146 /**
    147  * Convert a number to a @q-encoded string.
    148  *
    149  * @param  {String, Number, BN}  arg
    150  * @return  {String}
    151  */
    152 const patq = (arg) => {
    153   const bn = new BN(arg)
    154   const buf = bn.toArrayLike(Buffer)
    155   return buf2patq(buf)
    156 }
    157 
    158 /**
    159  * Convert a Buffer into a @q-encoded string.
    160  *
    161  * @param  {Buffer}  buf
    162  * @return  {String}
    163  */
    164 const buf2patq = buf => {
    165   const chunked =
    166     buf.length % 2 !== 0 && buf.length > 1
    167     ? [[buf[0]]].concat(chunk(buf.slice(1), 2))
    168     : chunk(buf, 2)
    169 
    170   const prefixName = byts =>
    171     byts[1] === undefined
    172     ? prefixes[0] + suffixes[byts[0]]
    173     : prefixes[byts[0]] + suffixes[byts[1]]
    174 
    175   const name = byts =>
    176     byts[1] === undefined
    177     ? suffixes[byts[0]]
    178     : prefixes[byts[0]] + suffixes[byts[1]]
    179 
    180   const alg = pair =>
    181     pair.length % 2 !== 0 && chunked.length > 1
    182     ? prefixName(pair)
    183     : name(pair)
    184 
    185   return chunked.reduce((acc, elem) =>
    186     acc + (acc === '~' ? '' : '-') + alg(elem), '~')
    187 }
    188 
    189 /**
    190  * Convert a hex-encoded string to a @q-encoded string.
    191  *
    192  * Note that this preserves leading zero bytes.
    193  *
    194  * @param  {String}  hex
    195  * @return  {String}
    196  */
    197 const hex2patq = arg => {
    198   const hex =
    199     arg.length % 2 !== 0
    200     ? arg.padStart(arg.length + 1, '0')
    201     : arg
    202 
    203   const buf = Buffer.from(hex, 'hex')
    204   return buf2patq(buf)
    205 }
    206 
    207 /**
    208  * Convert a @q-encoded string to a hex-encoded string.
    209  *
    210  * Note that this preserves leading zero bytes.
    211  *
    212  * @param  {String}  name @q
    213  * @return  {String}
    214  */
    215 const patq2hex = name => {
    216   if (isValidPat(name) === false) {
    217     throw new Error('patq2hex: not a valid @q')
    218   }
    219   const chunks = name.slice(1).split('-')
    220   const dec2hex = dec =>
    221     dec.toString(16).padStart(2, '0')
    222 
    223   const splat = chunks.map(chunk => {
    224     let syls = splitAt(3, chunk)
    225     return syls[1] === ''
    226       ? dec2hex(suffixes.indexOf(syls[0]))
    227       : dec2hex(prefixes.indexOf(syls[0])) +
    228         dec2hex(suffixes.indexOf(syls[1]))
    229   })
    230 
    231   return name.length === 0
    232     ? '00'
    233     : splat.join('')
    234 }
    235 
    236 /**
    237  * Convert a @q-encoded string to a bignum.
    238  *
    239  * @param  {String}  name @q
    240  * @return  {BN}
    241  */
    242 const patq2bn = name =>
    243   new BN(patq2hex(name), 'hex')
    244 
    245 /**
    246  * Convert a @q-encoded string to a decimal-encoded string.
    247  *
    248  * @param  {String}  name @q
    249  * @return  {String}
    250  */
    251 const patq2dec = name => {
    252   let bn
    253   try {
    254     bn = patq2bn(name)
    255   } catch(_) {
    256     throw new Error('patq2dec: not a valid @q')
    257   }
    258   return bn.toString()
    259 }
    260 
    261 /**
    262  * Determine the ship class of a @p value.
    263  *
    264  * @param  {String}  @p
    265  * @return  {String}
    266  */
    267 const clan = who => {
    268   let name
    269   try {
    270     name = patp2bn(who)
    271   } catch(_) {
    272     throw new Error('clan: not a valid @p')
    273   }
    274 
    275   const wid = met(three, name)
    276   return wid.lte(one)
    277     ? 'galaxy'
    278     : wid.eq(two)
    279     ? 'star'
    280     : wid.lte(four)
    281     ? 'planet'
    282     : wid.lte(new BN(8))
    283     ? 'moon'
    284     : 'comet'
    285 }
    286 
    287 /**
    288  * Determine the parent of a @p value.
    289  *
    290  * @param  {String}  @p
    291  * @return  {String}
    292  */
    293 const sein = name => {
    294   let who
    295   try {
    296     who = patp2bn(name)
    297   } catch(_) {
    298     throw new Error('sein: not a valid @p')
    299   }
    300 
    301   let mir
    302   try {
    303     mir = clan(name)
    304   } catch(_) {
    305     throw new Error('sein: not a valid @p')
    306   }
    307 
    308   const res =
    309     mir === 'galaxy'
    310     ? who
    311     : mir === 'star'
    312     ? end(three, one, who)
    313     : mir === 'planet'
    314     ? end(four, one, who)
    315     : mir === 'moon'
    316     ? end(five, one, who)
    317     : zero
    318   return patp(res)
    319 }
    320 
    321 /**
    322  * Weakly check if a string is a valid @p or @q value.
    323  *
    324  * This is, at present, a pretty weak sanity check.  It doesn't confirm the
    325  * structure precisely (e.g. dashes), and for @q, it's required that q values
    326  * of (greater than one) odd bytelength have been zero-padded.  So, for
    327  * example, '~doznec-binwod' will be considered a valid @q, but '~nec-binwod'
    328  * will not.
    329  *
    330  * @param  {String}  name a @p or @q value
    331  * @return  {boolean}
    332  */
    333 const isValidPat = name => {
    334   if (typeof name !== 'string') {
    335     throw new Error('isValidPat: non-string input')
    336   }
    337 
    338   const leadingTilde = name.slice(0, 1) === '~'
    339 
    340   if (leadingTilde === false || name.length < 4) {
    341     return false
    342   } else {
    343     const syls = patp2syls(name)
    344     const wrongLength = syls.length % 2 !== 0 && syls.length !== 1
    345     const sylsExist = syls.reduce((acc, syl, index) =>
    346       acc &&
    347         (index % 2 !== 0 || syls.length === 1
    348           ? suffixes.includes(syl)
    349           : prefixes.includes(syl)),
    350       true)
    351 
    352     return !wrongLength && sylsExist
    353   }
    354 }
    355 
    356 /**
    357  * Validate a @p string.
    358  *
    359  * @param  {String}  str a string
    360  * @return  {boolean}
    361  */
    362 const isValidPatp = str =>
    363   isValidPat(str) && str === patp(patp2dec(str))
    364 
    365 /**
    366  * Validate a @q string.
    367  *
    368  * @param  {String}  str a string
    369  * @return  {boolean}
    370  */
    371 const isValidPatq = str =>
    372   isValidPat(str) && eqPatq(str, patq(patq2dec(str)))
    373 
    374 /**
    375  * Remove all leading zero bytes from a sliceable value.
    376  * @param  {String, Buffer, Array}
    377  * @return  {String}
    378  */
    379 const removeLeadingZeroBytes = str =>
    380   str.slice(0, 2) === '00'
    381   ? removeLeadingZeroBytes(str.slice(2))
    382   : str
    383 
    384 /**
    385  * Equality comparison, modulo leading zero bytes.
    386  * @param  {String, Buffer, Array}
    387  * @param  {String, Buffer, Array}
    388  * @return  {Bool}
    389  */
    390 const eqModLeadingZeroBytes = (s, t) =>
    391   isEqual(removeLeadingZeroBytes(s), removeLeadingZeroBytes(t))
    392 
    393 /**
    394  * Equality comparison on @q values.
    395  * @param  {String}  p a @q-encoded string
    396  * @param  {String}  q a @q-encoded string
    397  * @return  {Bool}
    398  */
    399 const eqPatq = (p, q) => {
    400   let phex
    401   try {
    402     phex = patq2hex(p)
    403   } catch(_) {
    404     throw new Error('eqPatq: not a valid @q')
    405   }
    406 
    407   let qhex
    408   try {
    409     qhex = patq2hex(q)
    410   } catch(_) {
    411     throw new Error('eqPatq: not a valid @q')
    412   }
    413 
    414   return eqModLeadingZeroBytes(phex, qhex)
    415 }
    416 
    417 /**
    418  * Convert a number to a @p-encoded string.
    419  *
    420  * @param  {String, Number, BN}  arg
    421  * @return  {String}
    422  */
    423 const patp = (arg) => {
    424   if (arg === null) {
    425     throw new Error('patp: null input')
    426   }
    427   const n = new BN(arg)
    428 
    429   const sxz = ob.fein(n)
    430   const dyy = met(four, sxz)
    431 
    432   const loop = (tsxz, timp, trep) => {
    433     const log = end(four, one, tsxz)
    434     const pre = prefixes[rsh(three, one, log)]
    435     const suf = suffixes[end(three, one, log)]
    436     const etc =
    437       (timp.mod(four)).eq(zero)
    438         ? timp.eq(zero)
    439           ? ''
    440           : '--'
    441         : '-'
    442 
    443     const res = pre + suf + etc + trep
    444 
    445     return timp.eq(dyy)
    446       ? trep
    447       : loop(rsh(four, one, tsxz), timp.add(one), res)
    448   }
    449 
    450   const dyx = met(three, sxz)
    451 
    452   return '~' +
    453     (dyx.lte(one)
    454     ? suffixes[sxz]
    455     : loop(sxz, zero, ''))
    456 }
    457 
    458 module.exports = {
    459   patp,
    460   patp2hex,
    461   hex2patp,
    462   patp2dec,
    463   sein,
    464   clan,
    465 
    466   patq,
    467   patq2hex,
    468   hex2patq,
    469   patq2dec,
    470 
    471   eqPatq,
    472   isValidPat,
    473   isValidPatp,
    474   isValidPatq
    475 }