*Initial version

This commit is contained in:
Derrick Hammer 2023-01-14 02:21:04 -05:00
parent bc3fa03e2c
commit b1539104b9
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
23 changed files with 2640 additions and 1 deletions

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2023 Hammer Technologies LLC
Original Skynet component code copyright (c) 2022 Skynet Labs (https://github.com/SkynetLabs/skynet-kernel)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "@siaweb/libweb",
"version": "0.1.0",
"author": {
"name": "Derrick Hammer",
"email": "contact@lumeweb.com",
"url": "https://lumeweb.com"
},
"description": "helper library to interact with siaweb's low level primitives",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.js",
"publishConfig": {
"access": "public"
},
"files": [
"/dist"
],
"devDependencies": {
"prettier": "^2.8.3"
}
}

227
src/blake2b.ts Normal file
View File

@ -0,0 +1,227 @@
// Blake2B, adapted from the reference implementation in RFC7693
// Ported to Javascript by DC - https://github.com/dcposch
// Then ported to typescript by https://github.com/DavidVorick
// 64-bit unsigned addition
// Sets v[a,a+1] += v[b,b+1]
// v should be a Uint32Array
function ADD64AA(v: Uint32Array, a: number, b: number) {
const o0 = v[a] + v[b];
let o1 = v[a + 1] + v[b + 1];
if (o0 >= 0x100000000) {
o1++;
}
v[a] = o0;
v[a + 1] = o1;
}
// 64-bit unsigned addition
// Sets v[a,a+1] += b
// b0 is the low 32 bits of b, b1 represents the high 32 bits
function ADD64AC(v: Uint32Array, a: number, b0: number, b1: number) {
let o0 = v[a] + b0;
if (b0 < 0) {
o0 += 0x100000000;
}
let o1 = v[a + 1] + b1;
if (o0 >= 0x100000000) {
o1++;
}
v[a] = o0;
v[a + 1] = o1;
}
// Little-endian byte access
function B2B_GET32(arr: Uint32Array, i: number) {
return arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24);
}
// G Mixing function
// The ROTRs are inlined for speed
function B2B_G(
a: number,
b: number,
c: number,
d: number,
ix: number,
iy: number,
m: Uint32Array,
v: Uint32Array
) {
const x0 = m[ix];
const x1 = m[ix + 1];
const y0 = m[iy];
const y1 = m[iy + 1];
ADD64AA(v, a, b); // v[a,a+1] += v[b,b+1] ... in JS we must store a uint64 as two uint32s
ADD64AC(v, a, x0, x1); // v[a, a+1] += x ... x0 is the low 32 bits of x, x1 is the high 32 bits
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated to the right by 32 bits
let xor0 = v[d] ^ v[a];
let xor1 = v[d + 1] ^ v[a + 1];
v[d] = xor1;
v[d + 1] = xor0;
ADD64AA(v, c, d);
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 24 bits
xor0 = v[b] ^ v[c];
xor1 = v[b + 1] ^ v[c + 1];
v[b] = (xor0 >>> 24) ^ (xor1 << 8);
v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8);
ADD64AA(v, a, b);
ADD64AC(v, a, y0, y1);
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated right by 16 bits
xor0 = v[d] ^ v[a];
xor1 = v[d + 1] ^ v[a + 1];
v[d] = (xor0 >>> 16) ^ (xor1 << 16);
v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16);
ADD64AA(v, c, d);
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 63 bits
xor0 = v[b] ^ v[c];
xor1 = v[b + 1] ^ v[c + 1];
v[b] = (xor1 >>> 31) ^ (xor0 << 1);
v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1);
}
// Initialization Vector
const BLAKE2B_IV32 = new Uint32Array([
0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372,
0x5f1d36f1, 0xa54ff53a, 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c,
0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19,
]);
const SIGMA8 = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13,
6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1,
9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4,
10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5,
15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7,
14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2,
13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6,
1, 12, 0, 2, 11, 7, 5, 3,
];
// These are offsets into a uint64 buffer.
// Multiply them all by 2 to make them offsets into a uint32 buffer,
// because this is Javascript and we don't have uint64s
const SIGMA82 = new Uint8Array(
SIGMA8.map(function (x) {
return x * 2;
})
);
// Compression function. 'last' flag indicates last block.
// Note we're representing 16 uint64s as 32 uint32s
function blake2bCompress(ctx: any, last: boolean) {
const v = new Uint32Array(32);
const m = new Uint32Array(32);
let i = 0;
// init work variables
for (i = 0; i < 16; i++) {
v[i] = ctx.h[i];
v[i + 16] = BLAKE2B_IV32[i];
}
// low 64 bits of offset
v[24] = v[24] ^ ctx.t;
v[25] = v[25] ^ (ctx.t / 0x100000000);
// high 64 bits not supported, offset may not be higher than 2**53-1
// last block flag set ?
if (last) {
v[28] = ~v[28];
v[29] = ~v[29];
}
// get little-endian words
for (i = 0; i < 32; i++) {
m[i] = B2B_GET32(ctx.b, 4 * i);
}
// twelve rounds of mixing
for (i = 0; i < 12; i++) {
B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1], m, v);
B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3], m, v);
B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5], m, v);
B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7], m, v);
B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9], m, v);
B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11], m, v);
B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13], m, v);
B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15], m, v);
}
for (i = 0; i < 16; i++) {
ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16];
}
}
// Creates a BLAKE2b hashing context
// Requires an output length between 1 and 64 bytes
function blake2bInit() {
// state, 'param block'
const ctx = {
b: new Uint8Array(128),
h: new Uint32Array(16),
t: 0, // input count
c: 0, // pointer within buffer
outlen: 32, // output length in bytes
};
// initialize hash state
for (let i = 0; i < 16; i++) {
ctx.h[i] = BLAKE2B_IV32[i];
}
ctx.h[0] ^= 0x01010000 ^ 32;
return ctx;
}
// Updates a BLAKE2b streaming hash
// Requires hash context and Uint8Array (byte array)
function blake2bUpdate(ctx: any, input: Uint8Array) {
for (let i = 0; i < input.length; i++) {
if (ctx.c === 128) {
// buffer full ?
ctx.t += ctx.c; // add counters
blake2bCompress(ctx, false); // compress (not last)
ctx.c = 0; // counter to zero
}
ctx.b[ctx.c++] = input[i];
}
}
// Completes a BLAKE2b streaming hash
// Returns a Uint8Array containing the message digest
function blake2bFinal(ctx: any) {
ctx.t += ctx.c; // mark last block offset
while (ctx.c < 128) {
// fill up with zeros
ctx.b[ctx.c++] = 0;
}
blake2bCompress(ctx, true); // final block flag = 1
// little endian convert and store
const out = new Uint8Array(ctx.outlen);
for (let i = 0; i < ctx.outlen; i++) {
out[i] = ctx.h[i >> 2] >> (8 * (i & 3));
}
return out;
}
const BLAKE2B_HASH_SIZE = 32;
// Computes the blake2b hash of the input. Returns 32 bytes.
function blake2b(input: Uint8Array): Uint8Array {
const ctx = blake2bInit();
blake2bUpdate(ctx, input);
return blake2bFinal(ctx);
}
export { BLAKE2B_HASH_SIZE, blake2b };

39
src/checkObjProps.ts Normal file
View File

@ -0,0 +1,39 @@
import { Err } from "./types.js";
// checkObjProps take an untrusted object and a list of typechecks to perform
// and will check that the object adheres to the typechecks. If a type is
// missing or has the wrong type, an error will be returned. This is intended
// to be used to check untrusted objects after they get decoded from JSON. This
// is particularly useful when receiving objects from untrusted entities over
// the network or over postMessage.
//
// Below is an example object, followed by the call that you would make to
// checkObj to verify the object.
//
// const expectedObj = {
// aNum: 35,
// aStr: "hi",
// aBig: 10n,
// };
//
// const err = checkObjProps(expectedObj, [
// ["aNum", "number"],
// ["aStr", "string"],
// ["aBig", "bigint"],
// ]);
//
// Over time, we intend to extend this function to support more types than just
// the default types supported by javascript. For example, we intend to add
// special cases for arrays, and for cryptographic objects.
function checkObjProps(obj: any, checks: [string, string][]): Err {
for (let i = 0; i < checks.length; i++) {
const check = checks[i];
const type = typeof obj[check[0]];
if (type !== check[1]) {
return "check failed, expecting " + check[1] + " got " + type;
}
}
return null;
}
export { checkObjProps };

96
src/encoding.test.ts Normal file
View File

@ -0,0 +1,96 @@
import {
bufToHex,
bufToB64,
decodeU64,
encodeU64,
hexToBuf,
} from "../src/encoding";
test("encodeAndDecodeU64", () => {
const tests = [0n, 1n, 2n, 35n, 500n, 12345n, 642156n, 9591335n, 64285292n];
for (let i = 0; i < tests.length; i++) {
const [enc, errEU64] = encodeU64(tests[i]);
expect(errEU64).toBe(null);
const [dec, errDU64] = decodeU64(enc);
expect(errDU64).toBe(null);
expect(dec).toBe(tests[i]);
}
});
test("bufToB64", () => {
const tests = [
{ trial: new Uint8Array(0), result: "" },
{ trial: new Uint8Array([1]), result: "AQ" },
{ trial: new Uint8Array([1, 2]), result: "AQI" },
{ trial: new Uint8Array([255]), result: "_w" },
{ trial: new Uint8Array([23, 51, 0]), result: "FzMA" },
{ trial: new Uint8Array([0]), result: "AA" },
{ trial: new Uint8Array([0, 0, 0]), result: "AAAA" },
{ trial: new Uint8Array([30, 1, 3, 45, 129, 127]), result: "HgEDLYF_" },
{
trial: new Uint8Array([155, 196, 150, 83, 71, 54, 205, 231, 249, 34]),
result: "m8SWU0c2zef5Ig",
},
{
trial: new Uint8Array([57, 58, 59, 60, 61, 62, 63, 64]),
result: "OTo7PD0-P0A",
},
];
for (let i = 0; i < tests.length; i++) {
const result = bufToB64(tests[i].trial);
expect(result.length).toBe(tests[i].result.length);
for (let j = 0; j < result.length; j++) {
expect(result[j]).toBe(tests[i].result[j]);
}
}
});
test("bufToHexAndBufToHex", () => {
const tests = [
{ trial: new Uint8Array(0), result: "" },
{ trial: new Uint8Array([1]), result: "01" },
{ trial: new Uint8Array([1, 2]), result: "0102" },
{ trial: new Uint8Array([255]), result: "ff" },
{ trial: new Uint8Array([23, 51, 0]), result: "173300" },
{ trial: new Uint8Array([3, 7, 63, 127, 200, 5]), result: "03073f7fc805" },
{ trial: new Uint8Array([0]), result: "00" },
{ trial: new Uint8Array([0, 0, 0]), result: "000000" },
];
// Test hexToBuf
for (let i = 0; i < tests.length; i++) {
const result = bufToHex(tests[i].trial);
expect(result.length).toBe(tests[i].result.length);
for (let j = 0; j < result.length; j++) {
expect(result[j]).toBe(tests[i].result[j]);
}
}
// Test bufToHex.
for (let i = 0; i < tests.length; i++) {
const [result, err] = hexToBuf(tests[i].result);
expect(err).toBe(null);
expect(result.length).toBe(tests[i].trial.length);
for (let j = 0; j < result.length; j++) {
expect(result[j]).toBe(tests[i].trial[j]);
}
// Check that upper case is also okay.
const [result2, err2] = hexToBuf(tests[i].result.toUpperCase());
expect(err2).toBe(null);
expect(result2.length).toBe(tests[i].trial.length);
for (let j = 0; j < result2.length; j++) {
expect(result2[j]).toBe(tests[i].trial[j]);
}
}
// Create tests to check for invalid inputs.
const invalids = ["0", "000", "aX", "123O", "XX"];
for (let i = 0; i < invalids.length; i++) {
const [, err] = hexToBuf(invalids[i]);
if (err === null) {
console.log(invalids[i]);
}
expect(err).not.toBe(null);
}
});

148
src/encoding.ts Normal file
View File

@ -0,0 +1,148 @@
import { addContextToErr } from "./err.js";
import { Err } from "./types.js";
const MAX_UINT_64 = 18446744073709551615n;
// b64ToBuf will take an untrusted base64 string and convert it into a
// Uin8Array, returning an error if the input is not valid base64.
const b64regex = /^[0-9a-zA-Z-_/+=]*$/;
function b64ToBuf(b64: string): [Uint8Array, Err] {
// Check that the final string is valid base64.
if (!b64regex.test(b64)) {
return [new Uint8Array(0), "provided string is not valid base64"];
}
// Swap any '-' characters for '+', and swap any '_' characters for '/'
// for use in the atob function.
b64 = b64.replaceAll("-", "+").replaceAll("_", "/");
// Perform the conversion.
const binStr = atob(b64);
const len = binStr.length;
const buf = new Uint8Array(len);
for (let i = 0; i < len; i++) {
buf[i] = binStr.charCodeAt(i);
}
return [buf, null];
}
// bufToHex takes a Uint8Array as input and returns the hex encoding of those
// bytes as a string.
function bufToHex(buf: Uint8Array): string {
return [...buf].map((x) => x.toString(16).padStart(2, "0")).join("");
}
// bufToB64 will convert a Uint8Array to a base64 string with URL encoding and
// no padding characters.
function bufToB64(buf: Uint8Array): string {
const b64Str = btoa(String.fromCharCode(...buf));
return b64Str.replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
// bufToStr takes an ArrayBuffer as input and returns a text string. bufToStr
// will check for invalid characters.
function bufToStr(buf: ArrayBuffer): [string, Err] {
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(buf);
return [text, null];
} catch (err: any) {
return [
"",
addContextToErr(err.toString(), "unable to decode ArrayBuffer to string"),
];
}
}
// decodeU64 is the opposite of encodeU64, it takes a uint64 encoded as 8 bytes
// and decodes them into a BigInt.
function decodeU64(u8: Uint8Array): [bigint, Err] {
// Check the input.
if (u8.length !== 8) {
return [0n, "input should be 8 bytes"];
}
// Process the input.
let num = 0n;
for (let i = u8.length - 1; i >= 0; i--) {
num *= 256n;
num += BigInt(u8[i]);
}
return [num, null];
}
// encodePrefixedBytes takes a Uint8Array as input and returns a Uint8Array
// that has the length prefixed as an 8 byte prefix.
function encodePrefixedBytes(bytes: Uint8Array): [Uint8Array, Err] {
const [encodedLen, err] = encodeU64(BigInt(bytes.length));
if (err !== null) {
return [
new Uint8Array(0),
addContextToErr(err, "unable to encode array length"),
];
}
const prefixedArray = new Uint8Array(8 + bytes.length);
prefixedArray.set(encodedLen, 0);
prefixedArray.set(bytes, 8);
return [prefixedArray, null];
}
// encodeU64 will encode a bigint in the range of a uint64 to an 8 byte
// Uint8Array.
function encodeU64(num: bigint): [Uint8Array, Err] {
// Check the bounds on the bigint.
if (num < 0) {
return [new Uint8Array(0), "expected a positive integer"];
}
if (num > MAX_UINT_64) {
return [new Uint8Array(0), "expected a number no larger than a uint64"];
}
// Encode the bigint into a Uint8Array.
const encoded = new Uint8Array(8);
for (let i = 0; i < encoded.length; i++) {
const byte = Number(num & 0xffn);
encoded[i] = byte;
num = num >> 8n;
}
return [encoded, null];
}
// hexToBuf takes an untrusted string as input, verifies that the string is
// valid hex, and then converts the string to a Uint8Array.
const allHex = /^[0-9a-f]+$/i;
function hexToBuf(hex: string): [Uint8Array, Err] {
// The rest of the code doesn't handle zero length input well, so we handle
// that separately. It's not an error, we just return an empty array.
if (hex.length === 0) {
return [new Uint8Array(0), null];
}
// Check that the length makes sense.
if (hex.length % 2 !== 0) {
return [new Uint8Array(0), "input has incorrect length"];
}
// Check that all of the characters are legal.
if (!allHex.test(hex)) {
return [new Uint8Array(0), "input has invalid character"];
}
// Create the buffer and fill it.
const matches = hex.match(/.{2}/g);
if (matches === null) {
return [new Uint8Array(0), "input is incomplete"];
}
const u8 = new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
return [u8, null];
}
export {
b64ToBuf,
bufToHex,
bufToB64,
bufToStr,
decodeU64,
encodePrefixedBytes,
encodeU64,
hexToBuf,
};

16
src/err.ts Normal file
View File

@ -0,0 +1,16 @@
import { objAsString } from "./objAsString.js";
// addContextToErr is a helper function that standardizes the formatting of
// adding context to an error.
//
// NOTE: To protect against accidental situations where an Error type or some
// other type is provided instead of a string, we wrap both of the inputs with
// objAsString before returning them. This prevents runtime failures.
function addContextToErr(err: any, context: string): string {
if (err === null || err === undefined) {
err = "[no error provided]";
}
return objAsString(context) + ": " + objAsString(err);
}
export { addContextToErr };

101
src/errTracker.ts Normal file
View File

@ -0,0 +1,101 @@
// errTracker.ts defines an 'ErrTracker' type which keeps track of historical
// errors. When the number of errors gets too large, it randomly starts pruning
// errors. It always keeps 250 of the most recent errors, and then keeps up to
// 500 historic errors, where the first few errors after runtime are always
// kept, and the ones in the middle are increasingly likely to be omitted from
// the history.
import { Err } from "./types.js";
// MAX_ERRORS defines the maximum number of errors that will be held in the
// HistoricErr object.
const MAX_ERRORS = 1000;
// HistoricErr is a wrapper that adds a date to the Err type.
interface HistoricErr {
err: Err;
date: Date;
}
// ErrTracker keeps track of errors that have happened, randomly dropping
// errors to prevent the tracker from using too much memory if there happen to
// be a large number of errors.
interface ErrTracker {
recentErrs: HistoricErr[];
oldErrs: HistoricErr[];
addErr: (err: Err) => void;
viewErrs: () => HistoricErr[];
}
// newErrTracker returns an ErrTracker object that is ready to have errors
// added to it.
function newErrTracker(): ErrTracker {
const et: ErrTracker = {
recentErrs: [],
oldErrs: [],
addErr: function (err: Err): void {
addHistoricErr(et, err);
},
viewErrs: function (): HistoricErr[] {
return viewErrs(et);
},
};
return et;
}
// addHistoricErr is a function that will add an error to a set of historic
// errors. It uses randomness to prune errors once the error object is too
// large.
function addHistoricErr(et: ErrTracker, err: Err): void {
// Add this error to the set of most recent errors.
et.recentErrs.push({
err,
date: new Date(),
});
// Determine whether some of the most recent errors need to be moved into
// logTermErrs. If the length of the mostRecentErrs is not at least half of
// the MAX_ERRORS, we don't need to do anything.
if (et.recentErrs.length < MAX_ERRORS / 2) {
return;
}
// Iterate through the recentErrs. For the first half of the recentErrs, we
// will use randomness to either toss them or move them to oldErrs. The
// second half of the recentErrs will be kept as the new recentErrs array.
const newRecentErrs = [];
for (let i = 0; i < et.recentErrs.length; i++) {
// If we are in the second half of the array, add the element to
// newRecentErrs.
if (i > et.recentErrs.length / 2) {
newRecentErrs.push(et.recentErrs[i]);
continue;
}
// We are in the first half of the array, use a random number to add the
// error oldErrs probabilistically.
const rand = Math.random();
const target = et.oldErrs.length / (MAX_ERRORS / 2);
if (rand > target || et.oldErrs.length < 25) {
et.oldErrs.push(et.recentErrs[i]);
}
}
et.recentErrs = newRecentErrs;
}
// viewErrs returns the list of errors that have been retained by the
// HistoricErr object.
function viewErrs(et: ErrTracker): HistoricErr[] {
const finalErrs: HistoricErr[] = [];
for (let i = 0; i < et.oldErrs.length; i++) {
finalErrs.push(et.oldErrs[i]);
}
for (let i = 0; i < et.recentErrs.length; i++) {
finalErrs.push(et.recentErrs[i]);
}
return finalErrs;
}
export { ErrTracker, HistoricErr, newErrTracker };

38
src/index.ts Normal file
View File

@ -0,0 +1,38 @@
export { BLAKE2B_HASH_SIZE, blake2b } from "./blake2b.js";
export { checkObjProps } from "./checkObjProps.js";
export {
b64ToBuf,
bufToB64,
bufToHex,
bufToStr,
decodeU64,
encodePrefixedBytes,
encodeU64,
hexToBuf,
} from "./encoding.js";
export { addContextToErr } from "./err.js";
export { ErrTracker, HistoricErr, newErrTracker } from "./errTracker.js";
export { objAsString } from "./objAsString.js";
export { parseJSON } from "./parse.js";
export { SHA512_HASH_SIZE, sha512 } from "./sha512.js";
export {
SKYLINK_U8_V1_V2_LENGTH,
parseSkylinkBitfield,
skylinkV1Bitfield,
} from "./skylinkBitfield.js";
export {
validateSkyfileMetadata,
validateSkyfilePath,
validateSkylink,
} from "./skylinkValidate.js";
export { jsonStringify } from "./stringifyJSON.js";
export {
DataFn,
Err,
ErrFn,
ErrTuple,
KernelAuthStatus,
RequestOverrideResponse,
SkynetPortal,
} from "./types.js";
export { validateObjPropTypes } from "./validateObjPropTypes.js";

44
src/objAsString.test.ts Normal file
View File

@ -0,0 +1,44 @@
import { objAsString } from "../src/objAsString";
test("objAsString", () => {
// Try undefined.
let undefinedVar;
const undefinedResult = objAsString(undefinedVar);
expect(undefinedResult).toBe("[cannot convert undefined to string]");
// Try null.
const nullVar = null;
const nullResult = objAsString(nullVar);
expect(nullResult).toBe("[cannot convert null to string]");
// Try a string.
const strResult = objAsString("asdf");
expect(strResult).toBe("asdf");
const strVar = "asdfasdf";
const strResult2 = objAsString(strVar);
expect(strResult2).toBe("asdfasdf");
// Try an object.
const objVar = { a: "b", b: 7 };
const objResult = objAsString(objVar);
expect(objResult).toBe('{"a":"b","b":7}');
// Try an object with a defined toString that is a function.
objVar.toString = function () {
return "b7";
};
const objResult3 = objAsString(objVar);
expect(objResult3).toBe("b7");
// Try an object with a defined toString that is not a function. We need to
// specifiy 'as any' because we already made 'toString' a string, and now we
// are redefining the field with a new type.
(objVar as any).toString = "b7";
const objResult2 = objAsString(objVar);
expect(objResult2).toBe('{"a":"b","b":7,"toString":"b7"}');
// Try testing an error object.
const err1 = new Error("this is an error");
const err1Result = objAsString(err1);
expect(err1Result).toBe("this is an error");
});

64
src/objAsString.ts Normal file
View File

@ -0,0 +1,64 @@
// objAsString will try to return the provided object as a string. If the
// object is already a string, it will be returned without modification. If the
// object is an 'Error', the message of the error will be returned. If the object
// has a toString method, the toString method will be called and the result
// will be returned. If the object is null or undefined, a special string will
// be returned indicating that the undefined/null object cannot be converted to
// a string. In all other cases, JSON.stringify is used. If JSON.stringify
// throws an exception, a message "[could not provide object as string]" will
// be returned.
//
// NOTE: objAsString is intended to produce human readable output. It is lossy,
// and it is not intended to be used for serialization.
function objAsString(obj: any): string {
// Check for undefined input.
if (obj === undefined) {
return "[cannot convert undefined to string]";
}
if (obj === null) {
return "[cannot convert null to string]";
}
// Parse the error into a string.
if (typeof obj === "string") {
return obj;
}
// Check if the object is an error, and return the message of the error if
// so.
if (obj instanceof Error) {
return obj.message;
}
// Check if the object has a 'toString' method defined on it. To ensure
// that we don't crash or throw, check that the toString is a function, and
// also that the return value of toString is a string.
if (Object.prototype.hasOwnProperty.call(obj, "toString")) {
if (typeof obj.toString === "function") {
const str = obj.toString();
if (typeof str === "string") {
return str;
}
}
}
// If the object does not have a custom toString, attempt to perform a
// JSON.stringify. We use a lot of bigints in libskynet, and calling
// JSON.stringify on an object with a bigint will cause a throw, so we add
// some custom handling to allow bigint objects to still be encoded.
try {
return JSON.stringify(obj, (_, v) => {
if (typeof v === "bigint") {
return v.toString();
}
return v;
});
} catch (err: any) {
if (err !== undefined && typeof err.message === "string") {
return `[stringify failed]: ${err.message}`;
}
return "[stringify failed]";
}
}
export { objAsString };

390
src/parse.ts Normal file
View File

@ -0,0 +1,390 @@
// @ts-nocheck
import { objAsString } from "./objAsString.js";
import { Err } from "./types.js";
// json_parse extracted from the json-bigint npm library
// regexpxs extracted from
// (c) BSD-3-Clause
// https://github.com/fastify/secure-json-parse/graphs/contributors and https://github.com/hapijs/bourne/graphs/contributors
const suspectProtoRx =
/(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])/;
const suspectConstructorRx =
/(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)/;
let json_parse = function (options) {
"use strict";
// This is a function that can parse a JSON text, producing a JavaScript
// data structure. It is a simple, recursive descent parser. It does not use
// eval or regular expressions, so it can be used as a model for implementing
// a JSON parser in other languages.
// We are defining the function inside of another function to avoid creating
// global variables.
// Default options one can override by passing options to the parse()
let _options = {
strict: false, // not being strict means do not generate syntax errors for "duplicate key"
storeAsString: false, // toggles whether the values should be stored as BigNumber (default) or a string
alwaysParseAsBig: false, // toggles whether all numbers should be Big
protoAction: "error",
constructorAction: "error",
};
// If there are options, then use them to override the default _options
if (options !== undefined && options !== null) {
if (options.strict === true) {
_options.strict = true;
}
if (options.storeAsString === true) {
_options.storeAsString = true;
}
_options.alwaysParseAsBig =
options.alwaysParseAsBig === true ? options.alwaysParseAsBig : false;
if (typeof options.constructorAction !== "undefined") {
if (
options.constructorAction === "error" ||
options.constructorAction === "ignore" ||
options.constructorAction === "preserve"
) {
_options.constructorAction = options.constructorAction;
} else {
throw new Error(
`Incorrect value for constructorAction option, must be "error", "ignore" or undefined but passed ${options.constructorAction}`
);
}
}
if (typeof options.protoAction !== "undefined") {
if (
options.protoAction === "error" ||
options.protoAction === "ignore" ||
options.protoAction === "preserve"
) {
_options.protoAction = options.protoAction;
} else {
throw new Error(
`Incorrect value for protoAction option, must be "error", "ignore" or undefined but passed ${options.protoAction}`
);
}
}
}
let at, // The index of the current character
ch, // The current character
escapee = {
'"': '"',
"\\": "\\",
"/": "/",
b: "\b",
f: "\f",
n: "\n",
r: "\r",
t: "\t",
},
text,
error = function (m) {
// Call error when something is wrong.
throw {
name: "SyntaxError",
message: m,
at: at,
text: text,
};
},
next = function (c) {
// If a c parameter is provided, verify that it matches the current character.
if (c && c !== ch) {
error("Expected '" + c + "' instead of '" + ch + "'");
}
// Get the next character. When there are no more characters,
// return the empty string.
ch = text.charAt(at);
at += 1;
return ch;
},
number = function () {
// Parse a number value.
let number,
string = "";
if (ch === "-") {
string = "-";
next("-");
}
while (ch >= "0" && ch <= "9") {
string += ch;
next();
}
if (ch === ".") {
string += ".";
while (next() && ch >= "0" && ch <= "9") {
string += ch;
}
}
if (ch === "e" || ch === "E") {
string += ch;
next();
if (ch === "-" || ch === "+") {
string += ch;
next();
}
while (ch >= "0" && ch <= "9") {
string += ch;
next();
}
}
number = +string;
if (!isFinite(number)) {
error("Bad number");
} else {
if (Number.isSafeInteger(number))
return !_options.alwaysParseAsBig ? number : BigInt(number);
// Number with fractional part should be treated as number(double) including big integers in scientific notation, i.e 1.79e+308
else
return _options.storeAsString
? string
: /[.eE]/.test(string)
? number
: BigInt(string);
}
},
string = function () {
// Parse a string value.
let hex,
i,
string = "",
uffff;
// When parsing for string values, we must look for " and \ characters.
if (ch === '"') {
let startAt = at;
while (next()) {
if (ch === '"') {
if (at - 1 > startAt) string += text.substring(startAt, at - 1);
next();
return string;
}
if (ch === "\\") {
if (at - 1 > startAt) string += text.substring(startAt, at - 1);
next();
if (ch === "u") {
uffff = 0;
for (i = 0; i < 4; i += 1) {
hex = parseInt(next(), 16);
if (!isFinite(hex)) {
break;
}
uffff = uffff * 16 + hex;
}
string += String.fromCharCode(uffff);
} else if (typeof escapee[ch] === "string") {
string += escapee[ch];
} else {
break;
}
startAt = at;
}
}
}
error("Bad string");
},
white = function () {
// Skip whitespace.
while (ch && ch <= " ") {
next();
}
},
word = function () {
// true, false, or null.
switch (ch) {
case "t":
next("t");
next("r");
next("u");
next("e");
return true;
case "f":
next("f");
next("a");
next("l");
next("s");
next("e");
return false;
case "n":
next("n");
next("u");
next("l");
next("l");
return null;
}
error("Unexpected '" + ch + "'");
},
value, // Place holder for the value function.
array = function () {
// Parse an array value.
let array = [];
if (ch === "[") {
next("[");
white();
if (ch === "]") {
next("]");
return array; // empty array
}
while (ch) {
array.push(value());
white();
if (ch === "]") {
next("]");
return array;
}
next(",");
white();
}
}
error("Bad array");
},
object = function () {
// Parse an object value.
let key,
object = Object.create(null);
if (ch === "{") {
next("{");
white();
if (ch === "}") {
next("}");
return object; // empty object
}
while (ch) {
key = string();
white();
next(":");
if (
_options.strict === true &&
Object.hasOwnProperty.call(object, key)
) {
error('Duplicate key "' + key + '"');
}
if (suspectProtoRx.test(key) === true) {
if (_options.protoAction === "error") {
error("Object contains forbidden prototype property");
} else if (_options.protoAction === "ignore") {
value();
} else {
object[key] = value();
}
} else if (suspectConstructorRx.test(key) === true) {
if (_options.constructorAction === "error") {
error("Object contains forbidden constructor property");
} else if (_options.constructorAction === "ignore") {
value();
} else {
object[key] = value();
}
} else {
object[key] = value();
}
white();
if (ch === "}") {
next("}");
return object;
}
next(",");
white();
}
}
error("Bad object");
};
value = function () {
// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.
white();
switch (ch) {
case "{":
return object();
case "[":
return array();
case '"':
return string();
case "-":
return number();
default:
return ch >= "0" && ch <= "9" ? number() : word();
}
};
// Return the json_parse function. It will have access to all of the above
// functions and variables.
return function (source, reviver) {
let result;
text = source + "";
at = 0;
ch = " ";
result = value();
white();
if (ch) {
error("Syntax error");
}
// If there is a reviver function, we recursively walk the new structure,
// passing each name/value pair to the reviver function for possible
// transformation, starting with a temporary root object that holds the result
// in an empty key. If there is not a reviver function, we simply return the
// result.
return typeof reviver === "function"
? (function walk(holder, key) {
let v,
value = holder[key];
if (value && typeof value === "object") {
Object.keys(value).forEach(function (k) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
});
}
return reviver.call(holder, key, value);
})({ "": result }, "")
: result;
};
};
// parseJSON is a wrapper for JSONbig.parse that returns an error rather than
// throwing an error. JSONbig is an alternative to JSON.parse that decodes
// every number as a bigint. This is required when working with the skyd API
// because the skyd API uses 64 bit precision for all of its numbers, and
// therefore cannot be parsed losslessly by javascript. The skyd API is
// cryptographic, therefore full precision is required.
function parseJSON(json: string): [any, Err] {
try {
let obj = json_parse({ alwaysParseAsBig: true })(json);
return [obj, null];
} catch (err: any) {
return [{}, objAsString(err)];
}
}
export { parseJSON };

567
src/sha512.ts Normal file
View File

@ -0,0 +1,567 @@
const SHA512_HASH_SIZE = 64;
const K = [
0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, 0xb5c0fbcf, 0xec4d3b2f,
0xe9b5dba5, 0x8189dbbc, 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,
0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, 0xd807aa98, 0xa3030242,
0x12835b01, 0x45706fbe, 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,
0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, 0x9bdc06a7, 0x25c71235,
0xc19bf174, 0xcf692694, 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,
0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, 0x2de92c6f, 0x592b0275,
0x4a7484aa, 0x6ea6e483, 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,
0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, 0xb00327c8, 0x98fb213f,
0xbf597fc7, 0xbeef0ee4, 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,
0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, 0x27b70a85, 0x46d22ffc,
0x2e1b2138, 0x5c26c926, 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,
0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, 0x81c2c92e, 0x47edaee6,
0x92722c85, 0x1482353b, 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,
0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, 0xd192e819, 0xd6ef5218,
0xd6990624, 0x5565a910, 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,
0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, 0x2748774c, 0xdf8eeb99,
0x34b0bcb5, 0xe19b48a8, 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,
0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, 0x748f82ee, 0x5defb2fc,
0x78a5636f, 0x43172f60, 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,
0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, 0xbef9a3f7, 0xb2c67915,
0xc67178f2, 0xe372532b, 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,
0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, 0x06f067aa, 0x72176fba,
0x0a637dc5, 0xa2c898a6, 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,
0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, 0x3c9ebe0a, 0x15c9bebc,
0x431d67c4, 0x9c100d4c, 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,
0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817,
];
function ts64(x: Uint8Array, i: number, h: number, l: number) {
x[i] = (h >> 24) & 0xff;
x[i + 1] = (h >> 16) & 0xff;
x[i + 2] = (h >> 8) & 0xff;
x[i + 3] = h & 0xff;
x[i + 4] = (l >> 24) & 0xff;
x[i + 5] = (l >> 16) & 0xff;
x[i + 6] = (l >> 8) & 0xff;
x[i + 7] = l & 0xff;
}
function crypto_hashblocks_hl(
hh: Int32Array,
hl: Int32Array,
m: Uint8Array,
n: number
) {
const wh = new Int32Array(16),
wl = new Int32Array(16);
let bh0,
bh1,
bh2,
bh3,
bh4,
bh5,
bh6,
bh7,
bl0,
bl1,
bl2,
bl3,
bl4,
bl5,
bl6,
bl7,
th,
tl,
i,
j,
h,
l,
a,
b,
c,
d;
let ah0 = hh[0],
ah1 = hh[1],
ah2 = hh[2],
ah3 = hh[3],
ah4 = hh[4],
ah5 = hh[5],
ah6 = hh[6],
ah7 = hh[7],
al0 = hl[0],
al1 = hl[1],
al2 = hl[2],
al3 = hl[3],
al4 = hl[4],
al5 = hl[5],
al6 = hl[6],
al7 = hl[7];
let pos = 0;
while (n >= 128) {
for (i = 0; i < 16; i++) {
j = 8 * i + pos;
wh[i] = (m[j + 0] << 24) | (m[j + 1] << 16) | (m[j + 2] << 8) | m[j + 3];
wl[i] = (m[j + 4] << 24) | (m[j + 5] << 16) | (m[j + 6] << 8) | m[j + 7];
}
for (i = 0; i < 80; i++) {
bh0 = ah0;
bh1 = ah1;
bh2 = ah2;
bh3 = ah3;
bh4 = ah4;
bh5 = ah5;
bh6 = ah6;
bh7 = ah7;
bl0 = al0;
bl1 = al1;
bl2 = al2;
bl3 = al3;
bl4 = al4;
bl5 = al5;
bl6 = al6;
bl7 = al7;
// add
h = ah7;
l = al7;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
// Sigma1
h =
((ah4 >>> 14) | (al4 << (32 - 14))) ^
((ah4 >>> 18) | (al4 << (32 - 18))) ^
((al4 >>> (41 - 32)) | (ah4 << (32 - (41 - 32))));
l =
((al4 >>> 14) | (ah4 << (32 - 14))) ^
((al4 >>> 18) | (ah4 << (32 - 18))) ^
((ah4 >>> (41 - 32)) | (al4 << (32 - (41 - 32))));
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// Ch
h = (ah4 & ah5) ^ (~ah4 & ah6);
l = (al4 & al5) ^ (~al4 & al6);
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// K
h = K[i * 2];
l = K[i * 2 + 1];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// w
h = wh[i % 16];
l = wl[i % 16];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
th = (c & 0xffff) | (d << 16);
tl = (a & 0xffff) | (b << 16);
// add
h = th;
l = tl;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
// Sigma0
h =
((ah0 >>> 28) | (al0 << (32 - 28))) ^
((al0 >>> (34 - 32)) | (ah0 << (32 - (34 - 32)))) ^
((al0 >>> (39 - 32)) | (ah0 << (32 - (39 - 32))));
l =
((al0 >>> 28) | (ah0 << (32 - 28))) ^
((ah0 >>> (34 - 32)) | (al0 << (32 - (34 - 32)))) ^
((ah0 >>> (39 - 32)) | (al0 << (32 - (39 - 32))));
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// Maj
h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2);
l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2);
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
bh7 = (c & 0xffff) | (d << 16);
bl7 = (a & 0xffff) | (b << 16);
// add
h = bh3;
l = bl3;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = th;
l = tl;
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
bh3 = (c & 0xffff) | (d << 16);
bl3 = (a & 0xffff) | (b << 16);
ah1 = bh0;
ah2 = bh1;
ah3 = bh2;
ah4 = bh3;
ah5 = bh4;
ah6 = bh5;
ah7 = bh6;
ah0 = bh7;
al1 = bl0;
al2 = bl1;
al3 = bl2;
al4 = bl3;
al5 = bl4;
al6 = bl5;
al7 = bl6;
al0 = bl7;
if (i % 16 === 15) {
for (j = 0; j < 16; j++) {
// add
h = wh[j];
l = wl[j];
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = wh[(j + 9) % 16];
l = wl[(j + 9) % 16];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// sigma0
th = wh[(j + 1) % 16];
tl = wl[(j + 1) % 16];
h =
((th >>> 1) | (tl << (32 - 1))) ^
((th >>> 8) | (tl << (32 - 8))) ^
(th >>> 7);
l =
((tl >>> 1) | (th << (32 - 1))) ^
((tl >>> 8) | (th << (32 - 8))) ^
((tl >>> 7) | (th << (32 - 7)));
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
// sigma1
th = wh[(j + 14) % 16];
tl = wl[(j + 14) % 16];
h =
((th >>> 19) | (tl << (32 - 19))) ^
((tl >>> (61 - 32)) | (th << (32 - (61 - 32)))) ^
(th >>> 6);
l =
((tl >>> 19) | (th << (32 - 19))) ^
((th >>> (61 - 32)) | (tl << (32 - (61 - 32)))) ^
((tl >>> 6) | (th << (32 - 6)));
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
wh[j] = (c & 0xffff) | (d << 16);
wl[j] = (a & 0xffff) | (b << 16);
}
}
}
// add
h = ah0;
l = al0;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[0];
l = hl[0];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[0] = ah0 = (c & 0xffff) | (d << 16);
hl[0] = al0 = (a & 0xffff) | (b << 16);
h = ah1;
l = al1;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[1];
l = hl[1];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[1] = ah1 = (c & 0xffff) | (d << 16);
hl[1] = al1 = (a & 0xffff) | (b << 16);
h = ah2;
l = al2;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[2];
l = hl[2];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[2] = ah2 = (c & 0xffff) | (d << 16);
hl[2] = al2 = (a & 0xffff) | (b << 16);
h = ah3;
l = al3;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[3];
l = hl[3];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[3] = ah3 = (c & 0xffff) | (d << 16);
hl[3] = al3 = (a & 0xffff) | (b << 16);
h = ah4;
l = al4;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[4];
l = hl[4];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[4] = ah4 = (c & 0xffff) | (d << 16);
hl[4] = al4 = (a & 0xffff) | (b << 16);
h = ah5;
l = al5;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[5];
l = hl[5];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[5] = ah5 = (c & 0xffff) | (d << 16);
hl[5] = al5 = (a & 0xffff) | (b << 16);
h = ah6;
l = al6;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[6];
l = hl[6];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[6] = ah6 = (c & 0xffff) | (d << 16);
hl[6] = al6 = (a & 0xffff) | (b << 16);
h = ah7;
l = al7;
a = l & 0xffff;
b = l >>> 16;
c = h & 0xffff;
d = h >>> 16;
h = hh[7];
l = hl[7];
a += l & 0xffff;
b += l >>> 16;
c += h & 0xffff;
d += h >>> 16;
b += a >>> 16;
c += b >>> 16;
d += c >>> 16;
hh[7] = ah7 = (c & 0xffff) | (d << 16);
hl[7] = al7 = (a & 0xffff) | (b << 16);
pos += 128;
n -= 128;
}
return n;
}
const sha512internal = function (out: Uint8Array, m: Uint8Array, n: number) {
const hh = new Int32Array(8),
hl = new Int32Array(8),
x = new Uint8Array(256),
b = n;
let i;
hh[0] = 0x6a09e667;
hh[1] = 0xbb67ae85;
hh[2] = 0x3c6ef372;
hh[3] = 0xa54ff53a;
hh[4] = 0x510e527f;
hh[5] = 0x9b05688c;
hh[6] = 0x1f83d9ab;
hh[7] = 0x5be0cd19;
hl[0] = 0xf3bcc908;
hl[1] = 0x84caa73b;
hl[2] = 0xfe94f82b;
hl[3] = 0x5f1d36f1;
hl[4] = 0xade682d1;
hl[5] = 0x2b3e6c1f;
hl[6] = 0xfb41bd6b;
hl[7] = 0x137e2179;
crypto_hashblocks_hl(hh, hl, m, n);
n %= 128;
for (i = 0; i < n; i++) x[i] = m[b - n + i];
x[n] = 128;
n = 256 - 128 * (n < 112 ? 1 : 0);
x[n - 9] = 0;
ts64(x, n - 8, (b / 0x20000000) | 0, b << 3);
crypto_hashblocks_hl(hh, hl, x, n);
for (i = 0; i < 8; i++) ts64(out, 8 * i, hh[i], hl[i]);
return 0;
};
// sha512 is the standard sha512 cryptographic hash function. This is the
// default choice for Skynet operations, though many of the Sia protocol
// standards use blake2b instead, so you will see both.
function sha512(m: Uint8Array): Uint8Array {
const out = new Uint8Array(SHA512_HASH_SIZE);
sha512internal(out, m, m.length);
return out;
}
export { SHA512_HASH_SIZE, sha512, sha512internal };

View File

@ -0,0 +1,56 @@
import {
skylinkV1Bitfield,
parseSkylinkBitfield,
} from "../src/skylinkBitfield.js";
test.each([
{ dataSize: 0n, expectedFetchSize: 4096n },
{ dataSize: 1n, expectedFetchSize: 4096n },
{ dataSize: 100n, expectedFetchSize: 4096n },
{ dataSize: 200n, expectedFetchSize: 4096n },
{ dataSize: 4095n, expectedFetchSize: 4096n },
{ dataSize: 4096n, expectedFetchSize: 4096n },
{ dataSize: 4097n, expectedFetchSize: 8192n },
{ dataSize: 8191n, expectedFetchSize: 8192n },
{ dataSize: 8192n, expectedFetchSize: 8192n },
{ dataSize: 8193n, expectedFetchSize: 12288n },
{ dataSize: 12287n, expectedFetchSize: 12288n },
{ dataSize: 12288n, expectedFetchSize: 12288n },
{ dataSize: 12289n, expectedFetchSize: 16384n },
{ dataSize: 16384n, expectedFetchSize: 16384n },
{ dataSize: 32767n, expectedFetchSize: 32768n },
{ dataSize: 32768n, expectedFetchSize: 32768n },
{ dataSize: 32769n, expectedFetchSize: 36864n },
{ dataSize: 36863n, expectedFetchSize: 36864n },
{ dataSize: 36864n, expectedFetchSize: 36864n },
{ dataSize: 36865n, expectedFetchSize: 40960n },
{ dataSize: 45056n, expectedFetchSize: 45056n },
{ dataSize: 45057n, expectedFetchSize: 49152n },
{ dataSize: 65536n, expectedFetchSize: 65536n },
{ dataSize: 65537n, expectedFetchSize: 73728n },
{ dataSize: 106496n, expectedFetchSize: 106496n },
{ dataSize: 106497n, expectedFetchSize: 114688n },
{ dataSize: 163840n, expectedFetchSize: 163840n },
{ dataSize: 163841n, expectedFetchSize: 180224n },
{ dataSize: 491520n, expectedFetchSize: 491520n },
{ dataSize: 491521n, expectedFetchSize: 524288n },
{ dataSize: 720896n, expectedFetchSize: 720896n },
{ dataSize: 720897n, expectedFetchSize: 786432n },
{ dataSize: 1572864n, expectedFetchSize: 1572864n },
{ dataSize: 1572865n, expectedFetchSize: 1703936n },
{ dataSize: 3407872n, expectedFetchSize: 3407872n },
{ dataSize: 3407873n, expectedFetchSize: 3670016n },
])(
"skylinkV1Bitfield with data size $dataSize",
({ dataSize, expectedFetchSize }) => {
const skylink = new Uint8Array(34);
const [bitfield, errSVB] = skylinkV1Bitfield(dataSize);
expect(errSVB).toBe(null);
skylink.set(bitfield, 0);
const [version, offset, fetchSize, errPSB] = parseSkylinkBitfield(skylink);
expect(errPSB).toBe(null);
expect(version).toBe(1n);
expect(offset).toBe(0n);
expect(fetchSize).toBe(expectedFetchSize);
}
);

182
src/skylinkBitfield.ts Normal file
View File

@ -0,0 +1,182 @@
// skylinkBitfield.ts defines a bunch of operations for working with skylink
// bitfields. This code is ported from the Sia codebase. You can see that code
// and all of the comments at:
// https://gitlab.com/SkynetLabs/skyd/-/blob/master/skymodules/skylink.go
// SKYLINK_U8_V1_V2_LENGTH defines the length of a skylink that is V1 or V2
// when it is encoded as a Uint8Array.
const SKYLINK_U8_V1_V2_LENGTH = 34;
// SECTOR_SIZE is the size of a sector
const SECTOR_SIZE = 1 << 22;
// parseSkylinkBitfield parses a skylink bitfield and returns the corresponding
// version, offset, and fetchSize.
function parseSkylinkBitfield(
skylinkU8: Uint8Array
): [bigint, bigint, bigint, string | null] {
// Validate the input.
if (skylinkU8.length !== SKYLINK_U8_V1_V2_LENGTH) {
return [0n, 0n, 0n, "provided skylink has incorrect length"];
}
// Extract the bitfield.
let bitfield = new DataView(skylinkU8.buffer).getUint16(0, true);
// Extract the version. We add '1' so that an empty version field maps to
// version 1, and a version field set to '1' maps to version 2.
const version = (bitfield & 3) + 1;
// Only versions 1 and 2 are recognized.
if (version !== 1 && version !== 2) {
return [0n, 0n, 0n, "provided skylink has unrecognized version"];
}
// If the skylink is set to version 2, we only recognize the link if the rest
// of the bits in the bitfield are empty. This is the definition of a v2
// skylink, other skylink versions (such as version 1) use more of the
// bitfield.
if (version === 2) {
if ((bitfield & 3) !== bitfield) {
return [0n, 0n, 0n, "provided skylink has unrecognized version"];
}
return [BigInt(version), 0n, 0n, null];
}
// Verify that the mode is valid, then fetch the mode.
bitfield = bitfield >> 2;
if ((bitfield & 255) === 255) {
return [0n, 0n, 0n, "provided skylink has an unrecognized version"];
}
let mode = 0;
for (let i = 0; i < 8; i++) {
if ((bitfield & 1) === 0) {
bitfield = bitfield >> 1;
break;
}
bitfield = bitfield >> 1;
mode++;
}
// If the mode is greater than 7, this is not a valid v1 skylink.
if (mode > 7) {
return [0n, 0n, 0n, "provided skylink has an invalid v1 bitfield"];
}
// Determine the offset and fetchSize increment.
const offsetIncrement = 4096 << mode;
let fetchSizeIncrement = 4096;
let fetchSizeStart = 0;
if (mode > 0) {
fetchSizeIncrement = fetchSizeIncrement << (mode - 1);
fetchSizeStart = (1 << 15) << (mode - 1);
}
// The next three bits decide the fetchSize.
const fetchSizeBits = (bitfield & 7) + 1; // +1 because semantic range is [1,8].
const fetchSize = fetchSizeBits * fetchSizeIncrement + fetchSizeStart;
bitfield = bitfield >> 3;
// The remaining bits determine the offset.
const offset = bitfield * offsetIncrement;
if (offset + fetchSize > SECTOR_SIZE) {
return [0n, 0n, 0n, "provided skylink has an invalid v1 bitfield"];
}
// Return what we learned.
return [BigInt(version), BigInt(offset), BigInt(fetchSize), null];
}
// skylinkV1Bitfield sets the bitfield of a V1 skylink. It assumes the version
// is 1 and the offset is 0. It will determine the appropriate fetchSize from
// the provided dataSize.
function skylinkV1Bitfield(dataSizeBI: bigint): [Uint8Array, string | null] {
// Check that the dataSize is not too large.
if (dataSizeBI > SECTOR_SIZE) {
return [new Uint8Array(0), "dataSize must be less than the sector size"];
}
const dataSize = Number(dataSizeBI);
// Determine the mode for the file. The mode is determined by the
// dataSize.
let mode = 0;
for (let i = 1 << 15; i < dataSize; i *= 2) {
mode += 1;
}
// Determine the download number.
let downloadNumber = 0;
if (mode === 0) {
if (dataSize !== 0) {
downloadNumber = Math.floor((dataSize - 1) / (1 << 12));
}
} else {
const step = 1 << (11 + mode);
const target = dataSize - (1 << (14 + mode));
if (target !== 0) {
downloadNumber = Math.floor((target - 1) / step);
}
}
// Create the Uint8Array and fill it out. The main reason I switch over
// the 7 modes like this is because I wasn't sure how to make a uint16
// in javascript. If we could treat the uint8array as a uint16 and then
// later convert it over, we could use basic bitshifiting and really
// simplify the code here.
const bitfield = new Uint8Array(2);
if (mode === 7) {
// 0 0 0 X X X 0 1|1 1 1 1 1 1 0 0
bitfield[0] = downloadNumber;
bitfield[0] *= 4;
bitfield[0] += 1;
bitfield[1] = 4 + 8 + 16 + 32 + 64 + 128;
}
if (mode === 6) {
// 0 0 0 0 X X X 0|1 1 1 1 1 1 0 0
bitfield[0] = downloadNumber;
bitfield[0] *= 2;
bitfield[1] = 4 + 8 + 16 + 32 + 64 + 128;
}
if (mode === 5) {
// 0 0 0 0 0 X X X|0 1 1 1 1 1 0 0
bitfield[0] = downloadNumber;
bitfield[1] = 4 + 8 + 16 + 32 + 64;
}
if (mode === 4) {
// 0 0 0 0 0 0 X X|X 0 1 1 1 1 0 0
bitfield[0] = downloadNumber;
bitfield[0] /= 2;
bitfield[1] = (downloadNumber & 1) * 128;
bitfield[1] += 4 + 8 + 16 + 32;
}
if (mode === 3) {
// 0 0 0 0 0 0 0 X|X X 0 1 1 1 0 0
bitfield[0] = downloadNumber;
bitfield[0] /= 4;
bitfield[1] = (downloadNumber & 3) * 64;
bitfield[1] += 4 + 8 + 16;
}
if (mode === 2) {
// 0 0 0 0 0 0 0 0|X X X 0 1 1 0 0
bitfield[0] = 0;
bitfield[1] = downloadNumber * 32;
bitfield[1] += 4 + 8;
}
if (mode === 1) {
// 0 0 0 0 0 0 0 0|0 X X X 0 1 0 0
bitfield[0] = 0;
bitfield[1] = downloadNumber * 16;
bitfield[1] += 4;
}
if (mode === 0) {
// 0 0 0 0 0 0 0 0|0 0 X X X 0 0 0
bitfield[0] = 0;
bitfield[1] = downloadNumber * 8;
}
// Swap the byte order.
const zero = bitfield[0];
bitfield[0] = bitfield[1];
bitfield[1] = zero;
return [bitfield, null];
}
export { SKYLINK_U8_V1_V2_LENGTH, parseSkylinkBitfield, skylinkV1Bitfield };

View File

@ -0,0 +1,29 @@
import { validateSkyfilePath } from "../src/skylinkValidate.js";
test.each([
{ path: "test", result: null },
{ path: "test/subtrial", result: null },
{ path: "test/subtrial.ext", result: null },
{ path: "test/trial.ext/subtrial.ext", result: null },
{ path: ".foo", result: null },
{ path: ".foo/.bar", result: null },
{ path: "foo/.bar", result: null },
{ path: "/", result: "metdata.Filename cannot start with /" },
{ path: "", result: "path cannot be blank" },
{ path: ".", result: "path cannot be ." },
{ path: "./", result: "metdata.Filename cannot start with ./" },
{
path: "a//b",
result: "path cannot have an empty element, cannot contain //",
},
{ path: "a/./b", result: "path cannot have a . element" },
{ path: "a/../b", result: "path cannot have a .. element" },
{ path: "../a/b", result: "metdata.Filename cannot start with ../" },
{ path: "/sometrial", result: "metdata.Filename cannot start with /" },
{
path: "sometrial/",
result: "path cannot have an empty element, cannot contain //",
},
])("testValidateSkyfilePath with path '$path'", ({ path, result }) => {
expect(validateSkyfilePath(path)).toBe(result);
});

130
src/skylinkValidate.ts Normal file
View File

@ -0,0 +1,130 @@
import { addContextToErr } from "./err.js";
import { b64ToBuf } from "./encoding.js";
import {
SKYLINK_U8_V1_V2_LENGTH,
parseSkylinkBitfield,
} from "./skylinkBitfield.js";
import { Err } from "./types.js";
// validateSkyfilePath checks whether the provided path is a valid path for a
// file in a skylink.
function validateSkyfilePath(path: string): string | null {
if (path === "") {
return "path cannot be blank";
}
if (path === "..") {
return "path cannot be ..";
}
if (path === ".") {
return "path cannot be .";
}
if (path.startsWith("/")) {
return "metdata.Filename cannot start with /";
}
if (path.startsWith("../")) {
return "metdata.Filename cannot start with ../";
}
if (path.startsWith("./")) {
return "metdata.Filename cannot start with ./";
}
const pathElems = path.split("/");
for (let i = 0; i < pathElems.length; i++) {
if (pathElems[i] === ".") {
return "path cannot have a . element";
}
if (pathElems[i] === "..") {
return "path cannot have a .. element";
}
if (pathElems[i] === "") {
return "path cannot have an empty element, cannot contain //";
}
}
return null;
}
// validateSkyfileMetadata checks whether the provided metadata is valid
// metadata for a skyfile.
function validateSkyfileMetadata(metadata: any): string | null {
// Check that the filename is valid.
if (!("Filename" in metadata)) {
return "metadata.Filename does not exist";
}
if (typeof metadata.Filename !== "string") {
return "metadata.Filename is not a string";
}
const errVSP = validateSkyfilePath(metadata.Filename);
if (errVSP !== null) {
return addContextToErr(
errVSP,
"metadata.Filename does not have a valid path"
);
}
// Check that there are no subfiles.
if ("Subfiles" in metadata) {
// TODO: Fill this out using code from
// skymodules.ValidateSkyfileMetadata to support subfiles.
return "cannot upload files that have subfiles";
}
// Check that the default path rules are being respected.
if ("DisableDefaultPath" in metadata && "DefaultPath" in metadata) {
return "cannot set both a DefaultPath and also DisableDefaultPath";
}
if ("DefaultPath" in metadata) {
// TODO: Fill this out with code from
// skymodules.validateDefaultPath to support subfiles and
// default paths.
return "cannot set a default path if there are no subfiles";
}
if ("TryFiles" in metadata) {
if (!metadata.TryFiles.IsArray()) {
return "metadata.TryFiles must be an array";
}
if (metadata.TryFiles.length === 0) {
return "metadata.TryFiles should not be empty";
}
if ("DefaultPath" in metadata) {
return "metadata.TryFiles cannot be used alongside DefaultPath";
}
if ("DisableDefaultPath" in metadata) {
return "metadata.TryFiles cannot be used alongside DisableDefaultPath";
}
// TODO: finish the TryFiles checking using skymodules.ValidateTryFiles
return "TryFiles is not supported at this time";
}
if ("ErrorPages" in metadata) {
// TODO: finish using skymodules.ValidateErrorPages
return "ErrorPages is not supported at this time";
}
return null;
}
// validateSkylink returns null if the provided Uint8Array is a valid skylink.
function validateSkylink(skylink: string | Uint8Array): Err {
// If the input is a string, convert it to a Uint8Array.
let skylinkU8: Uint8Array;
if (typeof skylink === "string") {
const [buf, err] = b64ToBuf(skylink);
if (err !== null) {
return addContextToErr(err, "unable to convert skylink from string");
}
skylinkU8 = buf;
} else {
skylinkU8 = skylink;
}
// skylink is now a Uint8
if (skylinkU8.length !== SKYLINK_U8_V1_V2_LENGTH) {
return `skylinkU8 has an invalid length: ${skylinkU8.length}`;
}
const [, , , errPSB] = parseSkylinkBitfield(skylinkU8);
if (errPSB !== null) {
return addContextToErr(errPSB, "skylink did not decode");
}
return null;
}
export { validateSkyfileMetadata, validateSkyfilePath, validateSkylink };

35
src/stringifyJSON.test.ts Normal file
View File

@ -0,0 +1,35 @@
import { jsonStringify } from "./stringifyJSON.js";
test("testJSONStringify", () => {
// Check that the function works as expected with basic input.
const basicObj = {
test: 5,
};
const [str1, err1] = jsonStringify(basicObj);
expect(err1).toBe(null);
// Count the number of quotes in str1, we are expecting 2.
let quotes = 0;
for (let i = 0; i < str1.length; i++) {
if (str1[i] === '"') {
quotes += 1;
}
}
expect(quotes).toBe(2);
// Try encoding a bignum.
const bigNumObj = {
test: 5n,
testBig: 122333444455555666666777777788888888999999999000000000012345n,
};
const [str2, err2] = jsonStringify(bigNumObj);
expect(err2).toBe(null);
// Count the number of quotes in str2, we are expecting 4.
quotes = 0;
for (let i = 0; i < str2.length; i++) {
if (str2[i] === '"') {
quotes += 1;
}
}
expect(quotes).toBe(4);
});

24
src/stringifyJSON.ts Normal file
View File

@ -0,0 +1,24 @@
import { addContextToErr } from "./err.js";
import { objAsString } from "./objAsString.js";
import { Err } from "./types.js";
// jsonStringify is a replacement for JSON.stringify that returns an error
// rather than throwing.
function jsonStringify(obj: any): [string, Err] {
try {
const str = JSON.stringify(obj, (_, v) => {
if (typeof v === "bigint") {
return Number(v);
}
return v;
});
return [str, null];
} catch (err) {
return [
"",
addContextToErr(objAsString(err), "unable to stringify object"),
];
}
}
export { jsonStringify };

88
src/types.ts Normal file
View File

@ -0,0 +1,88 @@
// DataFn can take any object as input and has no return value. The input is
// allowed to be undefined.
type DataFn = (data?: any) => void;
// Err is an error type that is either a string or a null. If the value is
// null, that indicates that there was no error. If the value is a string, it
// indicates that there was an error and the string contains the error message.
//
// The skynet libraries prefer this error type to the standard Error type
// because many times skynet libraries need to pass errors over postMessage,
// and the 'Error' type is not able to be sent over postMessage.
type Err = string | null;
// ErrFn must take an error message as input. The input is not allowed to be
// undefined or null, there must be an error.
type ErrFn = (errMsg: string) => void;
// ErrTuple is a type that pairs a 'data' field with an 'err' field. Skynet
// libraries typically prefer returning ErrTuples to throwing or rejecting,
// because it allows upstream code to avoid the try/catch/throw pattern. Though
// the pattern is much celebrated in javascript, it encourages relaxed error
// handling, and often makes error handling much more difficult because the try
// and the catch are in different scopes.
//
// Most of the Skynet core libraries do not have any `throws` anywhere in their
// API.
//
// Typically, an ErrTuple will have only one field filled out. If data is
// returned, the err should be 'null'. If an error is returned, the data field
// should generally be empty. Callers are expected to check the error before
// they access any part of the data field.
type ErrTuple = [data: any, err: Err];
// KernelAuthStatus is the structure of a message that gets sent by the kernel
// containing its auth status. Auth occurs in 5 stages.
//
// Stage 0; no auth updates
// Stage 1: bootloader is loaded, user is not yet logged in
// Stage 2: bootloader is loaded, user is logged in
// Stage 3: kernel is loaded, user is logged in
// Stage 4: kernel is loaded, user is logging out (refresh iminent)
//
// 'kernelLoaded' is initially set to "not yet" and will be updated when the
// kernel has loaded. If it is set to "success", it means the kernel loaded
// without issues. If it is set to anything else, it means that there was an
// error, and the new value is the error.
//
// 'kernelLoaded' will not be changed until 'loginComplete' has been set to
// true. 'loginComplete' can be set to true immediately if the user is already
// logged in.
//
// 'logoutComplete' can be set to 'true' at any point, which indicates that the
// auth cycle needs to reset.
interface KernelAuthStatus {
loginComplete: boolean;
kernelLoaded: "not yet" | "success" | string;
logoutComplete: boolean;
}
// SkynetPortal defines the type for a skynet portal. In most cases, the url
// and the name are nearly the same (the URL will have the protocol, and the
// name will not). Sometimes however, they will not be the same.
//
// We need the two fields to be separate because the user will derive things
// like their account pubkey based on the name of the portal, and the portal
// may have to change URLs over time for various reasons.
interface SkynetPortal {
url: string;
name: string;
}
// RequestOverrideResponse defines the type that the kernel returns as a
// response to a requestOverride call.
interface RequestOverrideResponse {
override: boolean;
headers?: any; // TODO: I don't know how to do an array of types.
body?: Uint8Array;
}
export {
DataFn,
ErrFn,
Err,
ErrTuple,
KernelAuthStatus,
RequestOverrideResponse,
SkynetPortal,
};

View File

@ -0,0 +1,219 @@
import { validateObjPropTypes } from "./validateObjPropTypes.js";
// unit testing for validateObjPropTypes. Doesn't use test.each because the
// test inputs are complex.
test("validateObjPropTypes", () => {
// Validate a basic object.
const obj1 = {
prop1: "a",
};
const obj1Err = validateObjPropTypes(obj1, [["prop1", "string"]]);
expect(obj1Err).toBe(null);
// Validate a complex object without arrays.
const someVar = 12;
const obj2 = {
prop: "a",
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
toast: false,
pecans: true,
};
const obj2Err = validateObjPropTypes(obj2, [
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["toast", "boolean"],
["pecans", "boolean"],
]);
expect(obj2Err).toBe(null);
// Validate an object that is missing a field.
const obj3 = {
prop: "a",
prop1: `some var: ${someVar}`,
prop2: 5,
toast: false,
pecans: true,
};
const obj3Err = validateObjPropTypes(obj3, [
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["toast", "boolean"],
["pecans", "boolean"],
]);
expect(obj3Err).not.toBe(null);
// Validate an object that is missing the last field.
const obj4 = {
prop: "a",
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
toast: false,
};
const obj4Err = validateObjPropTypes(obj4, [
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["toast", "boolean"],
["pecans", "boolean"],
]);
expect(obj4Err).not.toBe(null);
// Validate an object that is missing the first field.
const obj5 = {
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
toast: false,
pecans: true,
};
const obj5Err = validateObjPropTypes(obj5, [
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["toast", "boolean"],
["pecans", "boolean"],
]);
expect(obj5Err).not.toBe(null);
// Validate an object with an array in it.
const obj6 = {
arr: ["hi", "hello"],
};
const obj6Err = validateObjPropTypes(obj6, [["arr", "stringArray"]]);
expect(obj6Err).toBe(null);
// Validate an object with the wrong array type in it.
const obj7 = {
arr: ["hi", "hello", 5],
};
const obj7Err = validateObjPropTypes(obj7, [["arr", "stringArray"]]);
expect(obj7Err).not.toBe(null);
// Validate an object with every array type, sprinkled in between normal
// types.
const obj8 = {
arrStr: ["hi", "hello"],
prop: "a",
arrNumber: [1, 2, 3],
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
someU8: new Uint8Array([1, 2, 3, 4]),
arrBool: [true, true, false],
toast: false,
pecans: true,
arrBig: [1n, 2n, 3n],
};
// We are now checking the objects out of order as another test.
const obj8Err = validateObjPropTypes(obj8, [
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["someU8", "Uint8Array"],
["toast", "boolean"],
["pecans", "boolean"],
["arrStr", "stringArray"],
["arrNumber", "numberArray"],
["arrBig", "bigintArray"],
["arrBool", "booleanArray"],
]);
expect(obj8Err).toBe(null);
// Validate an object with array types, but some of the types are wrong.
// 'pecans' has the wrong type in this test.
const obj9 = {
someU8: new Uint8Array([1, 2, 3, 4]),
arrStr: ["hi", "hello"],
prop: "a",
arrNumber: [1, 2, 3],
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
arrBool: [true, true, false],
toast: false,
pecans: 1,
arrBig: [1n, 2n, 3n],
};
// We are now checking the objects out of order as another test.
const obj9Err = validateObjPropTypes(obj9, [
["arrStr", "stringArray"],
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["someU8", "Uint8Array"],
["toast", "boolean"],
["pecans", "boolean"],
["arrNumber", "numberArray"],
["arrBig", "bigintArray"],
["arrBool", "booleanArray"],
]);
expect(obj9Err).not.toBe(null);
// Validate an object with array types, but some of the types are wrong.
// 'arrNumber' has the wrong type in this test.
const obj10 = {
arrStr: ["hi", "hello"],
prop: "a",
arrNumber: ["1", "2", "3"],
prop1: `some var: ${someVar}`,
prop2: 5,
butter: 5n,
arrBool: [true, true, false],
toast: false,
pecans: true,
arrBig: [1n, 2n, 3n],
someU8: new Uint8Array([1, 2, 3, 4]),
};
// We are now checking the objects out of order as another test.
const obj10Err = validateObjPropTypes(obj10, [
["arrStr", "stringArray"],
["prop", "string"],
["prop1", "string"],
["prop2", "number"],
["butter", "bigint"],
["toast", "boolean"],
["pecans", "boolean"],
["arrNumber", "numberArray"],
["arrBig", "bigintArray"],
["arrBool", "booleanArray"],
["someU8", "Uint8Array"],
]);
expect(obj10Err).not.toBe(null);
// Validate an object with a Uint8Array.
const u8arr = new Uint8Array([1, 2, 3, 5]);
const obj11 = {
u8: u8arr,
};
const obj11Err = validateObjPropTypes(obj11, [["u8", "Uint8Array"]]);
expect(obj11Err).toBe(null);
// Validate an object with a non Uint8Array.
const uXarr = [257, 1, 2, 3];
const obj12 = {
u8: uXarr,
};
const obj12Err = validateObjPropTypes(obj12, [["u8", "Uint8Array"]]);
expect(obj12Err).not.toBe(null);
// Test that an object works.
const smallObj = {
key: "value",
};
const obj13 = {
innerObj: smallObj,
};
const obj13Err = validateObjPropTypes(obj13, [["innerObj", "object"]]);
expect(obj13Err).toBe(null);
});

109
src/validateObjPropTypes.ts Normal file
View File

@ -0,0 +1,109 @@
import { addContextToErr } from "./err.js";
import { Err } from "./types.js";
// validateObjPropTypes takes an object as input, along with a list of checks
// that should performed on the properties of the object. If all of the
// properties are present in the object and adhere to the suggested types,
// `null` is returned. Otherwise a string is returned indicating the first
// property that failed a check.
//
// This function is intended to be used on objects that were decoded from JSON
// after being received by an untrusted source.
//
// validateObjProperties supports all of the basic types, as well as arrays for
// types boolean, number, bigint, and string. In the future, support for more
// types may be added as well.
//
// Below is an example object, followed by the call that you would make to
// checkObj to verify the object.
//
// const expectedObj = {
// aNum: 35,
// aStr: "hi",
// aBig: 10n,
// aArr: [1, 2, 3],
// };
//
// const err = validateObjPropTypes(expectedObj, [
// ["aNum", "number"],
// ["aStr", "string"],
// ["aBig", "bigint"],
// ["aArr", "numberArray"],
// ["aUint8Array", "Uint8Array"],
// ]);
function validateObjPropTypes(obj: any, checks: [string, string][]): Err {
for (let i = 0; i < checks.length; i++) {
const [property, expectedType] = checks[i];
// Loop through the array cases.
const arrayCases = [
["booleanArray", "boolean"],
["numberArray", "number"],
["bigintArray", "bigint"],
["stringArray", "string"],
];
let checkPassed = false;
for (let j = 0; j < arrayCases.length; j++) {
// If this is not an array case, ignore it.
const [arrCaseType, arrType] = arrayCases[j];
if (expectedType !== arrCaseType) {
continue;
}
// Check every element in the array.
const err = validateArrayTypes(obj[property], arrType);
if (err !== null) {
return addContextToErr(
err,
`check failed for array property '${property}'`
);
}
// We found the expected type for this check, we can stop checking the
// rest.
checkPassed = true;
break;
}
// If the type was an array type, we don't need to perform the next check.
if (checkPassed === true) {
continue;
}
// Uint8Array check.
if (expectedType === "Uint8Array") {
if (obj[property] instanceof Uint8Array) {
continue;
} else {
return `check failed for property '${property};, expecting Uint8Array`;
}
}
// Generic typeof check.
const type = typeof obj[property];
if (type !== expectedType) {
return `check failed for property '${property}', expecting ${expectedType} got ${type}`;
}
}
return null;
}
// validateArrayTypes takes an array as input and validates that every element
// in the array matches the provided type.
//
// This is a helper function for validateObjPropTypes, the property is provided
// as an input to produce a more coherent error message.
function validateArrayTypes(arr: any, expectedType: string): Err {
// Check that the provided input is actually an array.
if (!Array.isArray(arr)) {
return `not an array`;
}
for (let i = 0; i < arr.length; i++) {
const type = typeof arr[i];
if (type !== expectedType) {
return `element ${i} is expected to be ${expectedType}, got ${type}`;
}
}
return null;
}
export { validateObjPropTypes };

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2021",
"declaration": true,
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}