diff --git a/packages/buffer/LICENSE b/packages/buffer/LICENSE new file mode 100644 index 0000000..0f22de8 --- /dev/null +++ b/packages/buffer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2025 lideming + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/buffer/package.json b/packages/buffer/package.json new file mode 100644 index 0000000..be8c182 --- /dev/null +++ b/packages/buffer/package.json @@ -0,0 +1,17 @@ +{ + "name": "@yuuza/buffer", + "version": "1.0.0", + "description": "", + "main": "build/index.umd.js", + "module": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "build": "tsc && rollup build/index.js --format umd --name yuuzaBuffer -o build/index.umd.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "typescript": "^4.6.4" + } +} diff --git a/packages/buffer/src/binval.ts b/packages/buffer/src/binval.ts new file mode 100644 index 0000000..1e4cae6 --- /dev/null +++ b/packages/buffer/src/binval.ts @@ -0,0 +1,340 @@ +// "binval" - Yet another BSON +// See `docs/dev_binval.md` + +import { Buffer, decoder, DynamicBuffer, encoder } from "./buffer"; + +export function encodeValue(val: any) { + const buf = new DynamicBuffer(); + writeValue(val, buf); + return buf.buffer.subarray(0, buf.pos); +} + +export function decodeValue(buf: Uint8Array) { + return readValue(new Buffer(buf, 0)); +} + +export function writeValue(obj: any, buf: Buffer) { + if (obj === null) { + buf.writeU8(Type.Null); + } else if (obj === undefined) { + buf.writeU8(Type.Undefined); + } else if (obj === false) { + buf.writeU8(Type.False); + } else if (obj === true) { + buf.writeU8(Type.True); + } else if (typeof obj === "number") { + if (Number.isInteger(obj)) { + if (obj >= -7 && obj <= 127) { + if (obj > 0) { + buf.writeU8(Type.Number0 + obj); + } else if (obj < 0) { + buf.writeU8(Type.NumberNeg0 + obj); + } else { + if (Object.is(obj, -0)) { + buf.writeU8(Type.NumberNeg0); + } else { // +0 + buf.writeU8(Type.Number0); + } + } + } else { + let negative = 0; + if (obj < 0) { + obj = -obj; + negative = 3; + } + if (obj < 256) { + buf.writeU8(Type.Uint8 + negative); + buf.writeU8(obj); + } else if (obj < 65536) { + buf.writeU8(Type.Uint16 + negative); + buf.writeU16(obj); + } else if (obj < 2 ** 32) { + buf.writeU8(Type.Uint32 + negative); + buf.writeU32(obj); + } else { + // float64 + buf.writeU8(Type.Float64); + buf.writeF64(negative ? -obj : obj); + } + } + } else { + buf.writeU8(Type.Float64); + buf.writeF64(obj); + } + } else if (typeof obj === "string") { + const len = Buffer.calcStringSize(obj); + if (len <= 32) { + buf.writeU8(Type.String0 + len); + } else { + buf.writeU8(Type.String); + buf.writeEncodedUint(len); + } + buf.beforeWriting(len); + const r = encoder.encodeInto(obj, buf.buffer.subarray(buf.pos)); + if (r.written !== len) { + throw new Error("Expect " + r.written + " == " + len); + } + buf.pos += len; + } else if (typeof obj === "object") { + if (obj instanceof Array) { + if (obj.length <= 8) { + buf.writeU8(Type.Array0 + obj.length); + } else { + buf.writeU8(Type.Array); + buf.writeEncodedUint(obj.length); + } + for (const val of obj) { + writeValue(val, buf); + } + } else if (obj instanceof Uint8Array) { + if (obj.length <= 32) { + buf.writeU8(Type.Binary0 + obj.byteLength); + } else { + buf.writeU8(Type.Binary); + buf.writeEncodedUint(obj.byteLength); + } + buf.writeBuffer(obj); + } else { + const keys = Object.keys(obj); + if (keys.length <= 8) { + buf.writeU8(Type.Object0 + keys.length); + } else { + buf.writeU8(Type.Object); + buf.writeEncodedUint(keys.length); + } + for (const key of keys) { + buf.writeString(key); + writeValue(obj[key], buf); + } + } + } else { + throw new Error("Unsupported value " + obj); + } +} + +export function calcEncodedLength(obj: any): number { + if (obj === null) { + return 1; + } else if (obj === undefined) { + return 1; + } else if (obj === false) { + return 1; + } else if (obj === true) { + return 1; + } else if (typeof obj === "number") { + if (Number.isInteger(obj)) { + if (obj >= -7 && obj <= 127) { + return 1; + } else { + if (obj < 0) obj = -obj; + if (obj < 256) { + return 2; + } else if (obj < 65536) { + return 3; + } else if (obj < 2 ** 32) { + return 5; + } else { + // float64 + return 9; + } + } + } else { + return 9; + } + } else if (typeof obj === "string") { + const len = Buffer.calcStringSize(obj); + if (len <= 32) { + return 1 + len; + } else { + return 1 + Buffer.calcEncodedUintSize(len) + len; + } + } else if (typeof obj === "object") { + if (obj instanceof Array) { + let len = 0; + if (obj.length <= 8) { + len += 1; + } else { + len += 1 + Buffer.calcEncodedUintSize(obj.length); + } + for (const val of obj) { + len += calcEncodedLength(val); + } + return len; + } else if (obj instanceof Uint8Array) { + let len = 0; + if (obj.length <= 32) { + len += 1; + } else { + len += 1 + Buffer.calcEncodedUintSize(obj.byteLength); + } + len += obj.byteLength; + return len; + } else { + let len = 0; + const keys = Object.keys(obj); + if (keys.length <= 8) { + len += 1; + } else { + len += 1 + Buffer.calcEncodedUintSize(keys.length); + } + for (const key of keys) { + len += Buffer.calcLenEncodedStringSize(key); + len += calcEncodedLength(obj[key]); + } + return len; + } + } else { + throw new Error("Unsupported value " + obj); + } +} + +export function readValue(buf: Buffer) { + const type = buf.readU8(); + return decodeMap[type](buf, type); +} + +const decodeMap: Array<(buf: Buffer, type: number) => any> = []; + +// 0 ~ 3 +decodeMap.push(() => null); +decodeMap.push(() => undefined); +decodeMap.push(() => false); +decodeMap.push(() => true); + +// big number +// 4 ~ 6 +decodeMap.push((buf) => buf.readU8()); +decodeMap.push((buf) => buf.readU16()); +decodeMap.push((buf) => buf.readU32()); +// 7 ~ 9 +decodeMap.push((buf) => -buf.readU8()); +decodeMap.push((buf) => -buf.readU16()); +decodeMap.push((buf) => -buf.readU32()); +// 10 +decodeMap.push((buf) => buf.readF64()); + +// 11 big string +decodeMap.push((buf) => buf.readString()); + +// 12 big blob +decodeMap.push((buf) => { + const len = buf.readEncodedUint(); + return buf.readBuffer(len); +}); + +// 13 big object +decodeMap.push((buf) => decodeObject(buf, buf.readEncodedUint())); + +// 14 big array +decodeMap.push((buf) => decodeArray(buf, buf.readEncodedUint())); + +// 15 ~ 35 (unused) +for (let i = 15; i <= 35; i++) { + decodeMap.push(decodeUnused); +} + +// 36 ~ 44 small array +for (let i = 0; i <= 8; i++) { + decodeMap.push(decodeSmallArray); +} + +// 45 ~ 53 small object +for (let i = 0; i <= 8; i++) { + decodeMap.push(decodeSmallObject); +} + +// 54 ~ 86 small blob +for (let i = 0; i <= 32; i++) { + decodeMap.push(decodeSmallBlob); +} + +// 87 ~ 119 small string +decodeMap.push(() => ""); +for (let i = 1; i <= 32; i++) { + decodeMap.push(decodeSmallString); +} + +// 120 ~ 255 small number +for (let i = 120; i <= 126; i++) { + decodeMap.push(decodeSmallNegativeNumber); +} +// 127 "-0" +decodeMap.push(() => -0); +for (let i = 128; i <= 255; i++) { + decodeMap.push(decodeSmallPositiveNumber); +} + +function decodeSmallArray(buf: Buffer, type: number) { + return decodeArray(buf, type - Type.Array0); +} + +function decodeSmallObject(buf: Buffer, type: number) { + return decodeObject(buf, type - Type.Object0); +} + +function decodeSmallBlob(buf: Buffer, type: number) { + return buf.readBuffer(type - Type.Binary0); +} + +function decodeSmallString(buf: Buffer, type: number) { + const buffer = buf.readBufferReadonly(type - Type.String0); + return decoder.decode(buffer); +} + +function decodeSmallNegativeNumber(buf: Buffer, type: number) { + return type - Type.NumberNeg0; +} + +function decodeSmallPositiveNumber(buf: Buffer, type: number) { + return type - Type.Number0; +} + +function decodeUnused(buf: Buffer, type: number) { + throw new Error("Unsupported value type " + type); +} + +function BinvalObject() {} +(BinvalObject as any).prototype = Object.create(null); +declare class BinvalObject {} + +function decodeObject(buf: Buffer, propCount: number) { + let obj: any = new BinvalObject(); + for (let i = 0; i < propCount; i++) { + const key = buf.readString(); + obj[key] = readValue(buf); + } + return obj; +} + +function decodeArray(buf: Buffer, itemCount: number) { + let arr: any[] = []; + for (let i = 0; i < itemCount; i++) { + arr.push(readValue(buf)); + } + return arr; +} + +const enum Type { + Null = 0, + Undefined, + False, + True, + Uint8, + Uint16, + Uint32, + NegUint8, + NegUint16, + NegUint32, + Float64, + String, + Binary, + Object, + Array, + // 15 ~ 35 not used + Array0 = 36, + Object0 = 45, + Binary0 = 54, + String0 = 87, + NumberNeg0 = 127, + Number0 = 128, +} diff --git a/packages/buffer/src/buffer.ts b/packages/buffer/src/buffer.ts new file mode 100644 index 0000000..408c277 --- /dev/null +++ b/packages/buffer/src/buffer.ts @@ -0,0 +1,177 @@ +export const encoder = new TextEncoder(); +export const decoder = new TextDecoder(); + +const tmpbuf = new ArrayBuffer(8); +const f64arr = new Float64Array(tmpbuf); +const u8arr = new Uint8Array(tmpbuf); + +export class Buffer { + constructor( + public buffer: Uint8Array, + public pos: number, + ) {} + + writeF64(num: number) { + this.beforeWriting(8); + f64arr[0] = num; + this.writeBuffer(u8arr); + } + readF64() { + for (let i = 0; i < 8; i++) { + u8arr[i] = this.buffer[this.pos + i]; + } + this.pos += 8; + return f64arr[0]; + } + writeU32(num: number) { + this.beforeWriting(4); + this.buffer[this.pos++] = (num >>> 24) & 0xff; + this.buffer[this.pos++] = (num >>> 16) & 0xff; + this.buffer[this.pos++] = (num >>> 8) & 0xff; + this.buffer[this.pos++] = num & 0xff; + } + readU32() { + return ( + this.buffer[this.pos++] << 24 | + this.buffer[this.pos++] << 16 | + this.buffer[this.pos++] << 8 | + this.buffer[this.pos++] + ) >>> 0; + } + writeU16(num: number) { + this.beforeWriting(2); + this.buffer[this.pos++] = (num >> 8) & 0xff; + this.buffer[this.pos++] = num & 0xff; + } + readU16() { + return this.buffer[this.pos++] << 8 | + this.buffer[this.pos++]; + } + writeU8(num: number) { + this.beforeWriting(1); + this.buffer[this.pos++] = num & 0xff; + } + readU8() { + return this.buffer[this.pos++]; + } + writeBuffer(buf: Uint8Array) { + this.beforeWriting(buf.byteLength); + try { + this.buffer.set(buf, this.pos); + } catch { + debugger; + } + this.pos += buf.byteLength; + } + readBuffer(len: number) { + var buf = this.buffer.slice(this.pos, this.pos + len); + this.pos += len; + return buf; + } + readBufferReadonly(len: number) { + var buf = this.buffer.subarray(this.pos, this.pos + len); + this.pos += len; + return buf; + } + writeString(str: string) { + const len = Buffer.calcStringSize(str); + this.writeEncodedUint(len); + this.beforeWriting(len); + const r = encoder.encodeInto(str, this.buffer.subarray(this.pos)); + this.pos += r.written!; + } + writeLenEncodedBuffer(buf: Uint8Array) { + this.writeEncodedUint(buf.length); + this.writeBuffer(buf); + } + writeEncodedUint(val: number) { + if (val < 254) { + this.writeU8(val); + } else if (val < 65536) { + this.writeU8(254); + this.writeU16(val); + } else { + this.writeU8(255); + this.writeU32(val); + } + } + readEncodedUint() { + var val = this.readU8(); + if (val < 254) { + return val; + } else if (val == 254) { + return this.readU16(); + } else { + return this.readU32(); + } + } + readString() { + const len = this.readEncodedUint(); + const str = decoder.decode(this.buffer.subarray(this.pos, this.pos + len)); + this.pos += len; + return str; + } + static calcStringSize(str: string) { + if (nativeStringSize !== undefined) return nativeStringSize(str); + let bytes = 0; + const len = str.length; + for (let i = 0; i < len; i++) { + const codePoint = str.charCodeAt(i); + if (codePoint < 0x80) { + bytes += 1; + } else if (codePoint < 0x800) { + bytes += 2; + } else if (codePoint >= 0xD800 && codePoint < 0xE000) { + if (codePoint < 0xDC00 && i + 1 < len) { + const next = str.charCodeAt(i + 1); + if (next >= 0xDC00 && next < 0xE000) { + bytes += 4; + i++; + } else { + bytes += 3; + } + } else { + bytes += 3; + } + } else { + bytes += 3; + } + } + return bytes; + } + static calcLenEncodedStringSize(str: string) { + const len = Buffer.calcStringSize(str); + return Buffer.calcEncodedUintSize(len) + len; + } + static calcLenEncodedBufferSize(buf: Uint8Array) { + return Buffer.calcEncodedUintSize(buf.length) + buf.length; + } + static calcEncodedUintSize(len: number) { + return (len < 254) ? 1 : (len < 65536) ? 3 : 5; + } + + beforeWriting(size: number) {} +} + +const _globalThis = globalThis as any; + +const nativeStringSize: undefined | ((str: string) => number) = + _globalThis?.Buffer?.byteLength ?? undefined; + +export class DynamicBuffer extends Buffer { + constructor(initSize = 32) { + super(new Uint8Array(initSize), 0); + } + override beforeWriting(size: number) { + const minsize = this.pos + size; + if (minsize > this.buffer.byteLength) { + let newsize = this.buffer.byteLength * 4; + while (minsize > newsize) { + newsize *= 4; + } + const newBuffer = new Uint8Array(newsize); + newBuffer.set(this.buffer); + this.buffer = newBuffer; + } + } +} diff --git a/packages/buffer/src/index.ts b/packages/buffer/src/index.ts new file mode 100644 index 0000000..67cf3ac --- /dev/null +++ b/packages/buffer/src/index.ts @@ -0,0 +1,2 @@ +export * as binval from "./binval"; +export * from "./buffer"; diff --git a/packages/buffer/tsconfig.json b/packages/buffer/tsconfig.json new file mode 100644 index 0000000..c7c841f --- /dev/null +++ b/packages/buffer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "files": ["./src/index.ts"], + "compilerOptions": { + "types": [], + "outDir": "./build" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a81858..e7cd2a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,12 @@ importers: specifier: ^4.9.5 version: 4.9.5 + packages/buffer: + devDependencies: + typescript: + specifier: ^4.6.4 + version: 4.9.5 + packages/i18n: devDependencies: typescript: