heimdall-tide
Version:
SDK for communicating with a Tide Enclave
258 lines (216 loc) • 9.74 kB
text/typescript
export const version = "1";
export function wrapper(arr: NestedEntry): TideMemory {
// If array is only Uint8Arrays - create a TideMemory out of it
// If there is any entry in an array that is another array
// -> Go inside that array and repeat the process
if(arr.every(a => a instanceof Uint8Array)) return TideMemory.CreateFromArray(arr);
else {
// Go through each entry
arr.forEach((a) => {
// If the entry is an array, apply the wappa on it
if(Array.isArray(a)){
// Reassign the value of the entry -> to the serialized wrapper
a = wrapper(a);
}else if(a["value"]){
// Let's check if is a number, boolean or Uint8Array. If none of those, it'll be null
const res = encode(a["value"]);
if(res){
// serialized correctly
a = res;
}else{
if(typeof a["value"] == "string"){
// Serialize it into Uint8Array
if(!a["encoding"]){
// No encoding provided
// Let's default to UTF-8
a = encodeStr(a["value"], "UTF-8");
}else{
a = encodeStr(a["value"], a["encoding"]);
}
}
else throw 'Unsupported type';
}
}
else throw 'Unexpected format';
})
if(arr.every(a => a instanceof Uint8Array)) return TideMemory.CreateFromArray(arr); // Check to make sure everything was serialized correctly from the wappa
else throw 'There was an error encoding all your values';
}
}
export function encodeStr(str: string, enc: string): Uint8Array {
switch(enc){
case "UTF-8":
return new TextEncoder().encode(str);
case "HEX":
// 1) Strip 0x prefix
let normalized = str.replace(/^0x/i, "");
// treat empty as invalid
if (normalized.length === 0) {
throw new Error("Empty hex string");
}
// 2) Pad odd length
if (normalized.length % 2 !== 0) {
normalized = "0" + normalized;
}
// 3) Validate
if (!/^[0-9A-Fa-f]+$/.test(normalized)) {
throw new Error("Invalid hex string");
}
// 4) Parse into bytes
const byteCount = normalized.length / 2;
const out = new Uint8Array(byteCount);
for (let i = 0; i < byteCount; i++) {
out[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
}
return out;
case "B64":
const binaryString = atob(str);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
case "B64URL":
// 1) Replace URL-safe chars with standard Base64 chars
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
// 2) Pad with '=' so length is a multiple of 4
const pad = base64.length % 4;
if (pad === 2) {
base64 += '==';
} else if (pad === 3) {
base64 += '=';
} else if (pad === 1) {
// This shouldn’t happen for valid Base64-URL, but just in case…
base64 += '===';
}
// 3) Decode to binary string
const binary = atob(base64);
// 4) Convert to Uint8Array
const ulen = binary.length;
const ubytes = new Uint8Array(ulen);
for (let i = 0; i < ulen; i++) {
ubytes[i] = binary.charCodeAt(i);
}
return ubytes;
default:
// catches anything else (should never happen)
throw new TypeError(`Unsupported encoding: ${enc}`);
}
}
export function encode(data: number | boolean | Uint8Array): Uint8Array | undefined {
switch (typeof data) {
case 'number':
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, data, true);
return new Uint8Array(buffer);
case 'boolean':
return new Uint8Array([data ? 1 : 0]);
case 'object':
// since a Uint8Array is an object at runtime, we need to check it here
if (data instanceof Uint8Array) {
return new Uint8Array(data.slice(0));
}
// if we fall through, it wasn't one of our allowed types
throw new TypeError(`Unsupported object type: ${data}`);
default:
// catches anything else (should never happen)
return undefined;
}
}
interface entry{
value: any;
encoding?: string;
}
type NestedEntry = (entry | Uint8Array | NestedEntry)[]; // added Uint8Array as an optional type so we can serialize it without deep copy
// Tide Memory Object helper functions from tide-js
export class TideMemory extends Uint8Array{
static CreateFromArray(datas: Uint8Array[]): TideMemory {
const length = datas.reduce((sum, next) => sum + next.length, 0);
const mem = this.Create(datas[0], length);
for(let i = 1; i < datas.length; i++){
mem.WriteValue(i, datas[i]);
}
return mem;
}
static Create(initialValue: Uint8Array, totalLength: number, version: number = 1): TideMemory {
if (totalLength < initialValue.length + 4) {
throw new Error("Not enough space to allocate requested data. Make sure to request more space in totalLength than length of InitialValue plus 4 bytes for length.");
}
// Total buffer length is 4 (version) + totalLength
const bufferLength = 4 + totalLength;
const buffer = new TideMemory(bufferLength);
const dataView = new DataView(buffer.buffer);
// Write version at position 0 (4 bytes)
dataView.setInt32(0, version, true); // true for little-endian
let dataLocationIndex = 4;
// Write data length of initialValue at position 4 (4 bytes)
dataView.setInt32(dataLocationIndex, initialValue.length, true);
dataLocationIndex += 4;
// Write initialValue starting from position 8
buffer.set(initialValue, dataLocationIndex);
return buffer;
}
WriteValue(index: number, value: Uint8Array): void {
if (index < 0) throw new Error("Index cannot be less than 0");
if (index === 0) throw new Error("Use CreateTideMemory to set value at index 0");
if (this.length < 4 + value.length) throw new Error("Could not write to memory. Memory too small for this value");
const dataView = new DataView(this.buffer);
let dataLocationIndex = 4; // Start after the version number
// Navigate through existing data segments
for (let i = 0; i < index; i++) {
if (dataLocationIndex + 4 > this.length) {
throw new RangeError("Index out of range.");
}
// Read data length at current position
const nextDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4;
dataLocationIndex += nextDataLength;
}
// Check if there's enough space to write the value
if (dataLocationIndex + 4 + value.length > this.length) {
throw new RangeError("Not enough space to write value");
}
// Check if data has already been written to this index
const existingLength = dataView.getInt32(dataLocationIndex, true);
if (existingLength !== 0) {
throw new Error("Data has already been written to this index");
}
// Write data length of value at current position
dataView.setInt32(dataLocationIndex, value.length, true);
dataLocationIndex += 4;
// Write value starting from current position
this.set(value, dataLocationIndex);
}
GetValue<T extends Uint8Array>(index: number): T{
// 'a' should be an ArrayBuffer or Uint8Array
if (this.length < 4) {
throw new Error("Insufficient data to read.");
}
// Create a DataView for reading integers in little-endian format
const dataView = new DataView(this.buffer, this.byteOffset, this.byteLength);
// Optional: Read the version if needed
// const version = dataView.getInt32(0, true);
let dataLocationIndex = 4;
for (let i = 0; i < index; i++) {
// Check if there's enough data to read the length of the next segment
if (dataLocationIndex + 4 > this.length) {
throw new RangeError("Index out of range.");
}
const nextDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4 + nextDataLength;
}
// Check if there's enough data to read the length of the final segment
if (dataLocationIndex + 4 > this.length) {
throw new RangeError("Index out of range.");
}
const finalDataLength = dataView.getInt32(dataLocationIndex, true);
dataLocationIndex += 4;
// Check if the final data segment is within bounds
if (dataLocationIndex + finalDataLength > this.length) {
throw new RangeError("Index out of range.");
}
return this.subarray(dataLocationIndex, dataLocationIndex + finalDataLength) as T;
}
}