trytes
Version:
Conversion of trytes, bytes and text
410 lines (349 loc) • 13.1 kB
JavaScript
/**
* trytes.js
*
* Deals with "converting" to and from trytes.
*
* A little glossary for _this_ package:
* - trit: equivalent to bit, just trinary instead of binary.
* - tryte: a collective noun for any trinary tryte, independent on number of trits.
* - tryte3: a tryte of 3 trits, (may hold values of 0-26).
* - tryte5: a tryte of 5 trits, (may hold values of 0-242).
* - tryte6: a tryte of 6 trits, of 2 "IOTA trytes" (e.g. 'KB'), may hold values of 0-728.
* - tryte character: in IOTA a string representation of a tryte3, i.e 9 + A-Z.
* - tryte string: a text string containing tryte characters. Equivalent to a hex string "FF" that has 2 letters, and not the value (255 or 0xFF).
* - tryte value: just a number, like the number 42, stored internally in whatever way. Outside the boundaries for a tryte3, may be a legal value for tryte5 and tryte6.
*
* A little note about conversions:
* - convert: a conversion must work equally in both directions, such as a byte to a character, and a character to a byte. On the other hand, a tryte3 may be "converted" to a (tryte) character, but not all characters may be converted to a tryte3.
* Therefore, I try to use a different word. Such as:
* - encode/decode: A tryte3 may be encoded to a (tryte) character, and may be reverted by decoding it back. This does not imply that all characters may be decoded to a tryte3.
* - shifting: in binary you may shift bites left and right. Equivalent, we may shift trits left and right when going from tryte3 to tryte5 and vice versa.
*
* Directions:
* - Order in all array and strings, are with least significant value first, ending with most significant. (Opposite for normal numbers, but following the IOTA standard.)
*/
const TRYTE_CHARS = '9ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // All legal tryte3 characters
const POWEROF3 = [1, 3, 9, 27, 3 * 27, 9 * 27, 27 * 27]; // Pre calculated 3^i
const encodingMap_Buffer = {
'utf8': 'utf8',
'utf16': 'utf16le',
'ansi': 'latin1',
};
const TRYTE_BOM = {
'YZ': 'utf16le',
'ZY': 'utf16be',
'YY': 'utf8',
}
/**
* Encode bytes as tryte6 characters in a string
*
* With some minor adjustments, it is basically a copy of:
* iota.lib.js asciiToTrytes.js toTrytes()
*
* Not to be confused with decodeTryteStringFromBytes().
*
* @param {*} bytes Array of bytes, or a string of one-byte chars (e.g. [250, 16])
*
* @returns a tryte6 string (having 2 tryte chars representing each byte)
*/
function encodeBytesAsTryteString(bytes) {
var trytes = "";
for (var i = 0; i < bytes.length; i++) {
var value = bytes[i];
// If outside bounderies of a byte, return null
if (value > 255) {
return null;
}
var firstValue = value % 27;
var secondValue = (value - firstValue) / 27;
var trytesValue = TRYTE_CHARS[firstValue] + TRYTE_CHARS[secondValue];
trytes += trytesValue;
}
return trytes;
}
/**
* Decode a tryte6 string back to ts original bytes
*
* With some minor adjustments, it is basically a copy of:
* iota.lib.js asciiToTrytes.js fromTrytes()
*
* Not to be confused with encodeTryteStringAsBytes().
*
* @param {*} inputTrytes a tryte6 string (having 2 tryte3 chars representing a byte)
*
* @returns an array of the original bytes
*/
function decodeBytesFromTryteString(inputTrytes) {
// If input is not a string, return null
if (typeof inputTrytes !== 'string') return null
// If input length is odd, return null
if (inputTrytes.length % 2) return null
let bytes = [];
for (var i = 0; i < inputTrytes.length; i += 2) {
// get a trytes pair
var trytes = inputTrytes[i] + inputTrytes[i + 1];
var firstValue = TRYTE_CHARS.indexOf(trytes[0]);
var secondValue = TRYTE_CHARS.indexOf(trytes[1]);
var value = firstValue + secondValue * 27;
bytes.push(value);
}
return bytes;
}
/**
* Encode tryte3 strings (e.g. 'KB9Z') to array of bytes, Uint8Array.
*
* Under the hood, it:
* - converts tryte characters (9 + A-Z) to its corresponding trey3 values (0-26)
* - then, shifts the tryte3 (0-26), to tryte5 (0-242).
* In other words, it not really converted to bytes, but to tryte5. But this may be stored inside bytes.
*
* Last byte may contain a padding code, to indicate if and how many tryte3s should be removed,
* when decoding back to original form.
*
* Not to be confused with decodeBytesFromTryteString().
*
* @param {*} tryte3Str, texts tring with tryte chars (9 + A-Z), such as an IOTA seed, address, etc.
*
* @returns Uint8Array A representation of the trytes, stored within an 8-bit byte.
*/
function encodeTryteStringAsBytes(tryte3Str) {
let tryte3Values = convertTryte3CharsToValues(tryte3Str);
let tryte5Values = _shiftTrytes(tryte3Values, 3, 5);
return new Uint8Array(tryte5Values);
}
/**
* Decode bytes back to the original tryte string (e.g. 'KB9Z').
*
* Under the hood, the "bytes" can only hold tryte5 values (0-242), which are:
* - shifted from tryte5 (0-242) to tryte3 (0-26) values
* - then converted from tryte3 to a tryte character (9 + A-Z)
* The last byte may contain a flag for removing some trailing zero values (or '9' characters).
*
* Not to be confused with encodeBytesAsTryteString().
*
* @param {*} bytes Array of values from 0-242
*
* @returns A tryte3 string, containing only characters (9 + A-Z)
*/
function decodeTryteStringFromBytes(bytes) {
let tryte3Values = _shiftTrytes(bytes, 5, 3);
let tryte3Str = convertTryte3ValuesToChars(tryte3Values);
return tryte3Str;
}
/**
* Encode any (unicode) text string as a tryte string.
*
* The 'encoding' is optional, and is encouraged to be left blank.
* The default encoding will be chosen to create the shortest tryte string.
* - all characters below 128: latin1, without BOM
* - more than half of chars in \u0800-\uFFFF: utf16le, include BOM YZ
* - else: utf8, include BOM YY
*
* Discussion point: BOM
* The BOM could be solved using proper Unicode byte-order-mark FEFF, but that would require 6 tryte characters.
* The BOM has been solved using
* YZ, ZY and YY,
* for UTF-16 LE, UTF-16 BE, UTF-8, respectively.
*
*
* @param {*} text The string to encode
* @param {*} encoding Optional, may contain the Buffer() encodings 'latin1', 'utf8' or 'utf16le'
*
* @returns A tryte3 string, containing only characters (9 + A-Z)
*/
function encodeTextAsTryteString(text, encoding) {
if (typeof encoding === 'undefined') {
encoding = selectBestEncoding(text);
}
let bytes = encodeTextAsBytes(text, encoding);
let tryte3Str = encodeBytesAsTryteString(bytes);
let bom = getKeyByValue(TRYTE_BOM, encoding);
if (bom)
tryte3Str = bom + tryte3Str;
return tryte3Str;
}
/**
* Decodes a tryte3 string into a normal JavaScript Unicode string.
*
* As long as the tryte string constain the BOM, there is no need to use 'encoding'.
*
* @param {*} tryte3Str The tryte3 string
* @param {*} encoding Optional
*/
function decodeTextFromTryteString(tryte3Str, encoding) {
// Check if BOM exists
let bom = tryte3Str.substr(0, 2);
if (bom in TRYTE_BOM) {
// Remove bom from tryte string
tryte3Str = tryte3Str.substr(2);
// FIXME: What should take precedence? Given encoding argument, or given BOM in data?
if (typeof encoding === 'undefined') {
encoding = TRYTE_BOM[bom];
}
}
if (typeof encoding === 'undefined') {
// If no BOM is found, decode as it it was a one byte characterset
encoding = 'latin1';
}
let bytes = decodeBytesFromTryteString(tryte3Str);
let text = decodeTextFromBytes(bytes, encoding);
return text;
}
function encodeTextAsBytes(text, encoding) {
if (typeof encoding === 'undefined') {
encoding = selectBestEncoding(text);
}
// IF NodeJS:
let bytes = Buffer.from(text, encoding);
// IF browser
// let bytes = TextDecoder(....)
return bytes;
}
function decodeTextFromBytes(bytes, encoding) {
// IF NodeJS:
let text = Buffer.from(bytes).toString(encoding);
// IF browser
//let text = TextDecoder(...)
return text;
}
/**
* Shifting trits in arrays of tryte values, e.g. from tryte3 to tryte5, or vice versa
*
* @param {*} fromArray Array of the original tryte values
* @param {*} sizeFrom Numer of trits in the original tryte values
* @param {*} sizeTo Number of trits in the new "converted" tryte
*
* @returns an array of tryte values
*/
function _shiftTrytes(fromArray, sizeFrom, sizeTo) {
let toArray = [];
let trits = 0;
let tmpTryte = 0;
let padding = 0;
for (let i = 0; i < fromArray.length; i++) {
// Verify boundaries (exception for the last element)
if (fromArray[i] > POWEROF3[sizeFrom]) {
padding = fromArray[i];
break;
}
// Add new trits into the tryte
let factor = POWEROF3[trits]; // = 3^trits
trits += sizeFrom;
tmpTryte += fromArray[i] * factor;
// If more
while (trits >= sizeTo) {
let tryte = tmpTryte % POWEROF3[sizeTo];
tmpTryte = (tmpTryte - tryte) / POWEROF3[sizeTo];
trits -= sizeTo;
toArray.push(tryte);
}
}
while (trits > 0) {
let newTryte = tmpTryte % POWEROF3[sizeTo];
tmpTryte = (tmpTryte - newTryte) / POWEROF3[sizeTo];
trits -= sizeTo;
toArray.push(newTryte);
}
if (sizeTo > sizeFrom) {
// If going up in size, a padding code may be added at the end
if (trits < 0) {
toArray.push(POWEROF3[sizeTo] - Math.floor(trits / sizeFrom));
}
} else {
// When going down in size, check if any padding characters should be removed
if (padding != 0) {
let skip = padding - POWEROF3[sizeFrom];
//console.log("skip last ", (skip), "chars");
toArray = toArray.slice(0, -skip);
}
}
//console.log('Converted '+fromArray+' from ' + sizeFrom + '-' + sizeTo + ': Length: ' + fromArray.length + '-' + toArray.length + ': Remaining trits: ' + trits);
return toArray;
}
/**
* Convert a tryte string (9+A-Z), to an array of tryte3 values (0-26)
*
* @param {*} tryte3Str a tryte string, e.g. "KB0Z"
*
* @returns Array of tryte3 valules, e.g. [11, 2, 0, 26]
*
*/
function convertTryte3CharsToValues(tryte3Str) {
let tryte3Values = new Array(tryte3Str.length);
for (let i = 0; i < tryte3Str.length; i++) {
let value = TRYTE_CHARS.indexOf(tryte3Str[i]);
if (value < 0)
return null;
tryte3Values[i] = value;
}
return tryte3Values;
}
/**
* Convert an array of tryte3 values (0-26) to a tryte string (9+A-Z)
*
* @param {*} tryte3Values Array of tryte3 valules, e.g. [11, 2, 0, 26]
*
* @returns a tryte string, e.g. "KB0Z"
*
*/
function convertTryte3ValuesToChars(tryte3Values) {
let tryte3Str = "";
let value = 0;
for (let i = 0; i < tryte3Values.length; i++) {
value = tryte3Values[i];
if (value < 0 || value >= TRYTE_CHARS.length)
return null;
tryte3Str += TRYTE_CHARS[value];
}
return tryte3Str;
}
/**
* Select encoding based on the given text
*
* Ascii characters only, yields 'latin1', otherwise utf8 provides the shortest encoding.
*
* Except, if the unicode string has more than half of the Upper Basic Multilingual Plane
* from \u0800 to \uFFFF, since these use more bytes in UTF-8 than in UTF-16.
*
* @param {*} text
*
* @returns 'latin1', 'utf8' or 'utf16le'
*/
function selectBestEncoding(text) {
if (containsAsciiOnly(text)) {
return 'latin1';
}
else if (countUnicodeUperBMP(text) <= text.length / 2) {
return 'utf8';
}
else {
return 'utf16le';
}
}
function containsAsciiOnly(text) {
for (let i = 0; i < text.length; i++) {
if (text.charCodeAt(i) > 127)
return false;
}
return true;
}
function countUnicodeUperBMP(text) {
let count = 0;
for (let i=0; i<text.length; i++) {
let charCode = text.charCodeAt(i);
if (charCode >= 0x0800 && charCode < 0x10000)
count++;
}
return count;
}
function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
module.exports = {
encodeBytesAsTryteString,
decodeBytesFromTryteString,
encodeTryteStringAsBytes,
decodeTryteStringFromBytes,
encodeTextAsTryteString,
decodeTextFromTryteString,
};