diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index 48fd39c4..c803cd7e 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -191,7 +191,7 @@ "ignoreMissingScript": true, "allowWarningsInSuccessfulBuild": true, "enableParallelism": true, - "incremental": true, + // "incremental": true, "safeForSimultaneousRushProcesses": true }, { diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 59b6ff9b..9dc3fd31 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -62,15 +62,15 @@ importers: '@yume-chan/event': specifier: workspace:^0.0.23 version: link:../event + '@yume-chan/no-data-view': + specifier: workspace:^0.0.23 + version: link:../no-data-view '@yume-chan/stream-extra': specifier: workspace:^0.0.23 version: link:../stream-extra '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -105,9 +105,6 @@ importers: '@yume-chan/adb': specifier: workspace:^0.0.23 version: link:../adb - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@yume-chan/eslint-config': specifier: workspace:^1.0.0 @@ -136,9 +133,6 @@ importers: '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@yume-chan/eslint-config': specifier: workspace:^1.0.0 @@ -173,9 +167,6 @@ importers: '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -213,9 +204,6 @@ importers: '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@types/node': specifier: ^20.12.7 @@ -247,9 +235,6 @@ importers: '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -281,9 +266,6 @@ importers: '@types/w3c-web-usb': specifier: ^1.0.10 version: 1.0.10 - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@yume-chan/eslint-config': specifier: workspace:^1.0.0 @@ -296,10 +278,6 @@ importers: version: 5.4.5 ../../libraries/dataview-bigint-polyfill: - dependencies: - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@yume-chan/eslint-config': specifier: workspace:^1.0.0 @@ -322,9 +300,6 @@ importers: '@yume-chan/async': specifier: ^2.2.0 version: 2.2.0 - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -361,11 +336,40 @@ importers: specifier: ^20.12.7 version: 20.12.7 + ../../libraries/no-data-view: + devDependencies: + '@jest/globals': + specifier: ^30.0.0-alpha.3 + version: 30.0.0-alpha.3 + '@types/node': + specifier: ^20.12.7 + version: 20.12.7 + '@yume-chan/eslint-config': + specifier: workspace:^1.0.0 + version: link:../../toolchain/eslint-config + '@yume-chan/tsconfig': + specifier: workspace:^1.0.0 + version: link:../../toolchain/tsconfig + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + jest: + specifier: ^30.0.0-alpha.3 + version: 30.0.0-alpha.3(@types/node@20.12.7) + prettier: + specifier: ^3.2.5 + version: 3.2.5 + tinybench: + specifier: ^2.7.0 + version: 2.7.0 + ts-jest: + specifier: ^29.1.2 + version: 29.1.2(jest@30.0.0-alpha.3)(typescript@5.4.5) + typescript: + specifier: ^5.4.5 + version: 5.4.5 + ../../libraries/pcm-player: - dependencies: - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -397,6 +401,9 @@ importers: ../../libraries/scrcpy: dependencies: + '@yume-chan/no-data-view': + specifier: workspace:^0.0.23 + version: link:../no-data-view '@yume-chan/stream-extra': specifier: workspace:^0.0.23 version: link:../stream-extra @@ -449,9 +456,6 @@ importers: tinyh264: specifier: ^0.0.7 version: 0.0.7 - tslib: - specifier: ^2.6.2 - version: 2.6.2 yuv-buffer: specifier: ^1.0.0 version: 1.0.0 @@ -489,6 +493,9 @@ importers: '@yume-chan/event': specifier: workspace:^0.0.23 version: link:../event + '@yume-chan/no-data-view': + specifier: workspace:^0.0.23 + version: link:../no-data-view '@yume-chan/scrcpy': specifier: workspace:^0.0.23 version: link:../scrcpy @@ -498,9 +505,6 @@ importers: '@yume-chan/stream-extra': specifier: workspace:^0.0.23 version: link:../stream-extra - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -535,9 +539,6 @@ importers: '@yume-chan/struct': specifier: workspace:^0.0.23 version: link:../struct - tslib: - specifier: ^2.6.2 - version: 2.6.2 devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -566,12 +567,9 @@ importers: ../../libraries/struct: dependencies: - '@yume-chan/dataview-bigint-polyfill': + '@yume-chan/no-data-view': specifier: workspace:^0.0.23 - version: link:../dataview-bigint-polyfill - tslib: - specifier: ^2.6.2 - version: 2.6.2 + version: link:../no-data-view devDependencies: '@jest/globals': specifier: ^30.0.0-alpha.3 @@ -4571,6 +4569,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false + /tinybench@2.7.0: + resolution: {integrity: sha512-Qgayeb106x2o4hNzNjsZEfFziw8IbKqtbXBjVh7VIZfBxfD5M4gWtpyx5+YTae2gJ6Y6Dz/KLepiv16RFeQWNA==} + dev: true + /tinyh264@0.0.7: resolution: {integrity: sha512-etkBRgYkSFBdAi2Cqk4sZgi+xWs/vhzNgvjO3z2i4WILeEmORiNqxuQ4URJatrWQ9LPNV3WPWAtzsh/LA/XL/g==} dev: false diff --git a/common/config/rush/repo-state.json b/common/config/rush/repo-state.json index 8020e38d..2c629671 100644 --- a/common/config/rush/repo-state.json +++ b/common/config/rush/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "5dfb0a8a0ad6b0505870eeb38140a9ba82571aee", + "pnpmShrinkwrapHash": "0fb46a0dd9d3d20531a5eb59ce8ba9cacfe15a30", "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" } diff --git a/libraries/adb-credential-web/package.json b/libraries/adb-credential-web/package.json index 6d13ed0f..afd20e78 100644 --- a/libraries/adb-credential-web/package.json +++ b/libraries/adb-credential-web/package.json @@ -30,8 +30,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@yume-chan/adb": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/adb": "workspace:^0.0.23" }, "devDependencies": { "@yume-chan/eslint-config": "workspace:^1.0.0", diff --git a/libraries/adb-daemon-webusb/package.json b/libraries/adb-daemon-webusb/package.json index 6b0bed00..5052b6a2 100644 --- a/libraries/adb-daemon-webusb/package.json +++ b/libraries/adb-daemon-webusb/package.json @@ -34,8 +34,7 @@ "@types/w3c-web-usb": "^1.0.10", "@yume-chan/adb": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@yume-chan/eslint-config": "workspace:^1.0.0", diff --git a/libraries/adb-scrcpy/package.json b/libraries/adb-scrcpy/package.json index dece14bd..fb8ee4dc 100644 --- a/libraries/adb-scrcpy/package.json +++ b/libraries/adb-scrcpy/package.json @@ -37,8 +37,7 @@ "@yume-chan/event": "workspace:^0.0.23", "@yume-chan/scrcpy": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/adb-server-node-tcp/package.json b/libraries/adb-server-node-tcp/package.json index 60fb19f7..eb337210 100644 --- a/libraries/adb-server-node-tcp/package.json +++ b/libraries/adb-server-node-tcp/package.json @@ -34,8 +34,7 @@ "dependencies": { "@yume-chan/adb": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@types/node": "^20.12.7", diff --git a/libraries/adb/package.json b/libraries/adb/package.json index 1429faa9..97914140 100644 --- a/libraries/adb/package.json +++ b/libraries/adb/package.json @@ -35,9 +35,9 @@ "@yume-chan/async": "^2.2.0", "@yume-chan/dataview-bigint-polyfill": "workspace:^0.0.23", "@yume-chan/event": "workspace:^0.0.23", + "@yume-chan/no-data-view": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/adb/src/commands/sync/push.ts b/libraries/adb/src/commands/sync/push.ts index ff0e0a99..2c5b437c 100644 --- a/libraries/adb/src/commands/sync/push.ts +++ b/libraries/adb/src/commands/sync/push.ts @@ -97,10 +97,7 @@ export enum AdbSyncSendV2Flags { * 4 */ Zstd = 1 << 2, - /** - * 0x80000000 - */ - DryRun = (1 << 31) >>> 0, + DryRun = 0x80000000, } export interface AdbSyncPushV2Options extends AdbSyncPushV1Options { diff --git a/libraries/adb/src/daemon/crypto.ts b/libraries/adb/src/daemon/crypto.ts index b6148b1d..4655892b 100644 --- a/libraries/adb/src/daemon/crypto.ts +++ b/libraries/adb/src/daemon/crypto.ts @@ -1,7 +1,8 @@ import { - getBigUint64, - setBigUint64, -} from "@yume-chan/dataview-bigint-polyfill/esm/fallback.js"; + getUint64BigEndian, + setInt64BigEndian, + setInt64LittleEndian, +} from "@yume-chan/no-data-view"; /** * Gets the `BigInt` value at the specified byte offset and length from the start of the view. There is @@ -11,7 +12,7 @@ import { * @param byteOffset The place in the buffer at which the value should be retrieved. */ export function getBigUint( - dataView: DataView, + array: Uint8Array, byteOffset: number, length: number, ): bigint { @@ -22,8 +23,8 @@ export function getBigUint( for (let i = byteOffset; i < byteOffset + length; i += 8) { result <<= 64n; - const value = getBigUint64(dataView, i, false); - result += value; + const value = getUint64BigEndian(array, i); + result |= value; } return result; @@ -37,7 +38,7 @@ export function getBigUint( * otherwise a little-endian value should be written. */ export function setBigUint( - dataView: DataView, + array: Uint8Array, byteOffset: number, value: bigint, littleEndian?: boolean, @@ -46,7 +47,7 @@ export function setBigUint( if (littleEndian) { while (value > 0n) { - setBigUint64(dataView, byteOffset, value, true); + setInt64LittleEndian(array, byteOffset, value); byteOffset += 8; value >>= 64n; } @@ -60,7 +61,7 @@ export function setBigUint( } for (let i = uint64Array.length - 1; i >= 0; i -= 1) { - setBigUint64(dataView, byteOffset, uint64Array[i]!, false); + setInt64BigEndian(array, byteOffset, uint64Array[i]!); byteOffset += 8; } } @@ -95,9 +96,8 @@ const RsaPrivateKeyDOffset = 303; const RsaPrivateKeyDLength = 2048 / 8; export function rsaParsePrivateKey(key: Uint8Array): [n: bigint, d: bigint] { - const view = new DataView(key.buffer, key.byteOffset, key.byteLength); - const n = getBigUint(view, RsaPrivateKeyNOffset, RsaPrivateKeyNLength); - const d = getBigUint(view, RsaPrivateKeyDOffset, RsaPrivateKeyDLength); + const n = getBigUint(key, RsaPrivateKeyNOffset, RsaPrivateKeyNLength); + const d = getBigUint(key, RsaPrivateKeyDOffset, RsaPrivateKeyDLength); return [n, d]; } @@ -193,12 +193,12 @@ export function adbGeneratePublicKey( outputOffset += 4; // Write n - setBigUint(outputView, outputOffset, n, true); + setBigUint(output, outputOffset, n, true); outputOffset += 256; // Calculate rr = (2^(rsa_size)) ^ 2 mod n const rr = 2n ** 4096n % n; - outputOffset += setBigUint(outputView, outputOffset, rr, true); + outputOffset += setBigUint(output, outputOffset, rr, true); // exponent outputView.setUint32(outputOffset, 65537, true); @@ -305,12 +305,11 @@ export function rsaSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array { // Encryption // signature = padded ** d % n - const view = new DataView(padded.buffer); - const signature = powMod(getBigUint(view, 0, view.byteLength), d, n); + const signature = powMod(getBigUint(padded, 0, padded.length), d, n); // `padded` is not used anymore, // re-use the buffer to store the result - setBigUint(view, 0, signature, false); + setBigUint(padded, 0, signature, false); return padded; } diff --git a/libraries/adb/src/daemon/dispatcher.ts b/libraries/adb/src/daemon/dispatcher.ts index dd0fb308..9b60be3d 100644 --- a/libraries/adb/src/daemon/dispatcher.ts +++ b/libraries/adb/src/daemon/dispatcher.ts @@ -3,6 +3,10 @@ import { PromiseResolver, delay, } from "@yume-chan/async"; +import { + getUint32BigEndian, + setUint32LittleEndian, +} from "@yume-chan/no-data-view"; import { AbortController, Consumable, @@ -10,7 +14,7 @@ import { type ReadableWritablePair, type WritableStreamDefaultWriter, } from "@yume-chan/stream-extra"; -import { EMPTY_UINT8_ARRAY, NumberFieldType } from "@yume-chan/struct"; +import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct"; import type { AdbIncomingSocketHandler, AdbSocket, Closeable } from "../adb.js"; import { decodeUtf8, encodeUtf8 } from "../utils/index.js"; @@ -189,7 +193,7 @@ export class AdbPacketDispatcher implements Closeable { "Invalid OKAY packet. Payload size should be 4", ); } - ackBytes = NumberFieldType.Uint32.deserialize(packet.payload, true); + ackBytes = getUint32BigEndian(packet.payload, 0); } else { if (packet.payload.byteLength !== 0) { throw new Error( @@ -228,10 +232,7 @@ export class AdbPacketDispatcher implements Closeable { let payload: Uint8Array; if (this.options.initialDelayedAckBytes !== 0) { payload = new Uint8Array(4); - payload[0] = ackBytes & 0xff; - payload[1] = (ackBytes >> 8) & 0xff; - payload[2] = (ackBytes >> 16) & 0xff; - payload[3] = (ackBytes >> 24) & 0xff; + setUint32LittleEndian(payload, 0, ackBytes); } else { payload = EMPTY_UINT8_ARRAY; } diff --git a/libraries/adb/src/server/client.ts b/libraries/adb/src/server/client.ts index 68ea3c99..5aad69f4 100644 --- a/libraries/adb/src/server/client.ts +++ b/libraries/adb/src/server/client.ts @@ -17,7 +17,6 @@ import type { ValueOrPromise, } from "@yume-chan/struct"; import { - BigIntFieldType, EMPTY_UINT8_ARRAY, SyncPromise, decodeUtf8, @@ -29,6 +28,7 @@ import { AdbBanner } from "../banner.js"; import type { AdbFeature } from "../features.js"; import { NOOP, hexToNumber, numberToHex, unreachable } from "../utils/index.js"; +import { getUint64LittleEndian } from "@yume-chan/no-data-view"; import { AdbServerTransport } from "./transport.js"; export interface AdbServerConnectionOptions { @@ -391,13 +391,7 @@ export class AdbServerClient { try { if (transportId === undefined) { const array = await readable.readExactly(8); - // TODO: switch to a more performant algorithm. - const dataView = new DataView( - array.buffer, - array.byteOffset, - array.byteLength, - ); - transportId = BigIntFieldType.Uint64.getter(dataView, 0, true); + transportId = getUint64LittleEndian(array, 0); } await AdbServerClient.readOkay(readable); diff --git a/libraries/android-bin/package.json b/libraries/android-bin/package.json index e5a765f5..c43b87a9 100644 --- a/libraries/android-bin/package.json +++ b/libraries/android-bin/package.json @@ -34,8 +34,7 @@ "dependencies": { "@yume-chan/adb": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/aoa/package.json b/libraries/aoa/package.json index 8e1b3dd6..bac3815a 100644 --- a/libraries/aoa/package.json +++ b/libraries/aoa/package.json @@ -31,8 +31,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@types/w3c-web-usb": "^1.0.10", - "tslib": "^2.6.2" + "@types/w3c-web-usb": "^1.0.10" }, "devDependencies": { "@yume-chan/eslint-config": "workspace:^1.0.0", diff --git a/libraries/dataview-bigint-polyfill/package.json b/libraries/dataview-bigint-polyfill/package.json index 4b6fc0ce..7ecc2465 100644 --- a/libraries/dataview-bigint-polyfill/package.json +++ b/libraries/dataview-bigint-polyfill/package.json @@ -35,9 +35,7 @@ "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4", "prepublishOnly": "npm run build" }, - "dependencies": { - "tslib": "^2.6.2" - }, + "dependencies": {}, "devDependencies": { "@yume-chan/eslint-config": "workspace:^1.0.0", "@yume-chan/tsconfig": "workspace:^1.0.0", diff --git a/libraries/event/package.json b/libraries/event/package.json index 45f4231e..511ef49d 100644 --- a/libraries/event/package.json +++ b/libraries/event/package.json @@ -33,8 +33,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@yume-chan/async": "^2.2.0", - "tslib": "^2.6.2" + "@yume-chan/async": "^2.2.0" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/no-data-view/.npmignore b/libraries/no-data-view/.npmignore new file mode 100644 index 00000000..96b1a6a3 --- /dev/null +++ b/libraries/no-data-view/.npmignore @@ -0,0 +1,19 @@ +.rush + +# Test +coverage +**/*.spec.ts +**/*.spec.js +**/*.spec.js.map +**/__helpers__ +jest.config.js + +.eslintrc.cjs +tsconfig.json +tsconfig.test.json + +# Logs +*.log + +benchmark.js +benchmark.md diff --git a/libraries/no-data-view/LICENSE b/libraries/no-data-view/LICENSE new file mode 100644 index 00000000..9bd36e79 --- /dev/null +++ b/libraries/no-data-view/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 Simon Chan + +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/libraries/no-data-view/README.md b/libraries/no-data-view/README.md new file mode 100644 index 00000000..d8b58ef2 --- /dev/null +++ b/libraries/no-data-view/README.md @@ -0,0 +1,21 @@ +# @yume-chan/no-data-view + +Plain methods to avoid creating `DataView`s. + +## Why? + +If you have many short `Uint8Array`s and you want to read numbers from them, creating `DataView`s for each of them is not a good idea: + +Each `DataView` needs to allocate some memory, it impacts the performance, increases the memory usage, and adds GC pressure. + +(If you are using `DataView`s for large `ArrayBuffer`s, it's fine) + +## How does it work? + +This package provides a set of methods to read/write numbers from `Uint8Array`s without creating `DataView`s. + +Because they are very basic number operations, the performance between a JavaScript implementation and the native `DataView` is nearly identical. + +(Except for `getBigUint64` and `setBigUint64`, Chrome uses an inefficient implementation, so this JavaScript implementation is even faster than the native one). + +Check the [benchmark](./benchmark.md) for more details. diff --git a/libraries/no-data-view/benchmark.js b/libraries/no-data-view/benchmark.js new file mode 100644 index 00000000..241e1b25 --- /dev/null +++ b/libraries/no-data-view/benchmark.js @@ -0,0 +1,243 @@ +/// + +import { once } from "events"; +import { createWriteStream } from "fs"; +import { Bench } from "tinybench"; + +import { + getInt16LittleEndian, + getInt32LittleEndian, + getInt64LittleEndian, + getUint16, + getUint16BigEndian, + getUint16LittleEndian, + getUint32LittleEndian, + getUint64BigEndian, + getUint64LittleEndian, +} from "./esm/index.js"; + +console.log( + "Adjust priority for process", + process.pid, + ", then press Enter to start...", +); +await once(process.stdin, "data"); +console.log("Starting benchmark"); + +const data = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); +const buffer = data.buffer; +const dataView = new DataView(data.buffer); + +const output = createWriteStream("benchmark.md"); + +/** + * + * @param {string} name + * @param {Bench} bench + */ +function print(name, bench) { + console.log(); + console.log(name); + console.table(bench.table()); + + output.write("## " + name + "\n\n"); + output.write( + "| " + + ["Name", "Ops/s", "Avg Time", "Compare"].join(" | ") + + " |\n" + + "|---|---|---|---|\n", + ); + let firstResultHz; + for (const task of bench.tasks) { + const result = task.result; + if (!result) { + return; + } + let compare = 1; + if (firstResultHz) { + compare = result.hz / firstResultHz; + } else { + firstResultHz = result.hz; + } + output.write( + "| " + + [ + task.name, + (result.hz | 0).toLocaleString("en-US"), + (result.mean * 1_000_000).toLocaleString("en-US") + " ns", + (compare * 100).toFixed(2) + "%", + ].join(" | ") + + " |\n", + ); + } + output.write("\n"); +} + +const only = process.argv[2]; + +/** + * + * @param {string} name + * @param {(bench:Bench)=>void} callback + * @returns + */ +async function runBenchmark(name, callback) { + if (only && only !== name) { + return; + } + + const bench = new Bench(); + callback(bench); + + await bench.warmup(); + await bench.run(); + + print(name, bench); +} + +await runBenchmark("getUint16LittleEndian", (bench) => { + bench + .add("getUint16LittleEndian", () => { + getUint16LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getUint16(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getUint16(0, true); + }); +}); + +await runBenchmark("getUint16BigEndian", (bench) => { + bench + .add("getUint16BigEndian", () => { + getUint16BigEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getUint16(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getUint16(0, true); + }); +}); + +await runBenchmark("getUint16", (bench) => { + let littleEndian = true; + + bench + .add( + "getUint16", + () => { + getUint16(data, 0, littleEndian); + }, + { + beforeEach: () => { + littleEndian = Math.random() > 0.5; + }, + }, + ) + .add( + "cached DataView", + () => { + dataView.getUint16(0, true); + }, + { + beforeEach: () => { + littleEndian = Math.random() > 0.5; + }, + }, + ) + .add( + "new DataView", + () => { + new DataView(buffer).getUint16(0, true); + }, + { + beforeEach: () => { + littleEndian = Math.random() > 0.5; + }, + }, + ); +}); + +await runBenchmark("getInt16LittleEndian", (bench) => { + bench + .add("getInt16LittleEndian", () => { + getInt16LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getInt16(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getInt16(0, true); + }); +}); + +await runBenchmark("getUint32LittleEndian", (bench) => { + bench + .add("getUint32LittleEndian", () => { + getUint32LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getUint32(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getUint32(0, true); + }); +}); + +await runBenchmark("getInt32LittleEndian", (bench) => { + bench + .add("getInt32LittleEndian", () => { + getInt32LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getInt32(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getInt32(0, true); + }); +}); + +await runBenchmark("getUint64LittleEndian", (bench) => { + bench + .add("getUint64LittleEndian", () => { + getUint64LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getBigUint64(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getBigUint64(0, true); + }); +}); + +await runBenchmark("getUint64BigEndian", (bench) => { + bench + .add("getUint64BigEndian", () => { + getUint64BigEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getBigUint64(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getBigUint64(0, true); + }); +}); + +await runBenchmark("getInt64LittleEndian", (bench) => { + bench + .add("getInt64LittleEndian", () => { + getInt64LittleEndian(data, 0); + }) + .add("cached DataView", () => { + dataView.getBigInt64(0, true); + }) + .add("new DataView", () => { + new DataView(buffer).getBigInt64(0, true); + }); +}); + +output.close(() => { + process.exit(0); +}); diff --git a/libraries/no-data-view/benchmark.md b/libraries/no-data-view/benchmark.md new file mode 100644 index 00000000..1bf0df4a --- /dev/null +++ b/libraries/no-data-view/benchmark.md @@ -0,0 +1,71 @@ +## getUint16LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| --------------------- | ---------- | ---------- | ------- | +| getUint16LittleEndian | 19,892,072 | 50.271 ns | 100.00% | +| cached DataView | 18,810,767 | 53.161 ns | 94.56% | +| new DataView | 7,585,290 | 131.834 ns | 38.13% | + +## getUint16BigEndian + +| Name | Ops/s | Avg Time | Compare | +| ------------------ | ---------- | ---------- | ------- | +| getUint16BigEndian | 19,553,782 | 51.141 ns | 100.00% | +| cached DataView | 19,919,910 | 50.201 ns | 101.87% | +| new DataView | 7,843,575 | 127.493 ns | 40.11% | + +## getUint16 + +| Name | Ops/s | Avg Time | Compare | +| --------------- | ---------- | ---------- | ------- | +| getUint16 | 18,571,186 | 53.847 ns | 100.00% | +| cached DataView | 19,434,597 | 51.455 ns | 104.65% | +| new DataView | 7,480,502 | 133.681 ns | 40.28% | + +## getInt16LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| -------------------- | ---------- | --------- | ------- | +| getInt16LittleEndian | 19,719,781 | 50.71 ns | 100.00% | +| cached DataView | 20,285,727 | 49.296 ns | 102.87% | +| new DataView | 7,963,668 | 125.57 ns | 40.38% | + +## getUint32LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| --------------------- | ---------- | ---------- | ------- | +| getUint32LittleEndian | 20,037,121 | 49.907 ns | 100.00% | +| cached DataView | 19,750,168 | 50.632 ns | 98.57% | +| new DataView | 8,127,370 | 123.041 ns | 40.56% | + +## getInt32LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| -------------------- | ---------- | ---------- | ------- | +| getInt32LittleEndian | 19,955,247 | 50.112 ns | 100.00% | +| cached DataView | 19,721,023 | 50.707 ns | 98.83% | +| new DataView | 7,888,401 | 126.768 ns | 39.53% | + +## getUint64LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| --------------------- | ---------- | ---------- | ------- | +| getUint64LittleEndian | 19,438,620 | 51.444 ns | 100.00% | +| cached DataView | 16,597,547 | 60.25 ns | 85.38% | +| new DataView | 6,957,942 | 143.721 ns | 35.79% | + +## getUint64BigEndian + +| Name | Ops/s | Avg Time | Compare | +| ------------------ | ---------- | ---------- | ------- | +| getUint64BigEndian | 20,075,843 | 49.811 ns | 100.00% | +| cached DataView | 16,787,123 | 59.569 ns | 83.62% | +| new DataView | 7,469,821 | 133.872 ns | 37.21% | + +## getInt64LittleEndian + +| Name | Ops/s | Avg Time | Compare | +| -------------------- | ---------- | --------- | ------- | +| getInt64LittleEndian | 20,167,285 | 49.585 ns | 100.00% | +| cached DataView | 17,129,268 | 58.38 ns | 84.94% | +| new DataView | 7,605,130 | 131.49 ns | 37.71% | diff --git a/libraries/no-data-view/jest.config.js b/libraries/no-data-view/jest.config.js new file mode 100644 index 00000000..ff68d1cb --- /dev/null +++ b/libraries/no-data-view/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + preset: "ts-jest/presets/default-esm", + extensionsToTreatAsEsm: [".ts"], + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { tsconfig: "tsconfig.test.json", useESM: true }, + ], + }, + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, +}; diff --git a/libraries/no-data-view/package.json b/libraries/no-data-view/package.json new file mode 100644 index 00000000..3eb2afeb --- /dev/null +++ b/libraries/no-data-view/package.json @@ -0,0 +1,45 @@ +{ + "name": "@yume-chan/no-data-view", + "version": "0.0.23", + "description": "Plain methods to avoid creating `DataView`s", + "keywords": [], + "license": "MIT", + "author": { + "name": "Simon Chan", + "email": "cnsimonchan@live.com", + "url": "https://chensi.moe/blog" + }, + "homepage": "https://github.com/yume-chan/ya-webadb/tree/main/packages/no-data-view#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/yume-chan/ya-webadb.git", + "directory": "packages/no-data-view" + }, + "bugs": { + "url": "https://github.com/yume-chan/ya-webadb/issues" + }, + "type": "module", + "main": "esm/index.js", + "types": "esm/index.d.ts", + "scripts": { + "benchmark": "node benchmark.js", + "build": "tsc -b tsconfig.build.json", + "build:watch": "tsc -b tsconfig.build.json", + "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" TS_JEST_DISABLE_VER_CHECKER=true jest --coverage", + "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4", + "prepublishOnly": "npm run build" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^20.12.7", + "@jest/globals": "^30.0.0-alpha.3", + "@yume-chan/eslint-config": "workspace:^1.0.0", + "@yume-chan/tsconfig": "workspace:^1.0.0", + "cross-env": "^7.0.3", + "jest": "^30.0.0-alpha.3", + "prettier": "^3.2.5", + "ts-jest": "^29.1.2", + "tinybench": "^2.7.0", + "typescript": "^5.4.5" + } +} diff --git a/libraries/no-data-view/src/index.ts b/libraries/no-data-view/src/index.ts new file mode 100644 index 00000000..3378e919 --- /dev/null +++ b/libraries/no-data-view/src/index.ts @@ -0,0 +1,7 @@ +export * from "./int16.js"; +export * from "./int32.js"; +export * from "./int64.js"; +export * from "./int8.js"; +export * from "./uint16.js"; +export * from "./uint32.js"; +export * from "./uint64.js"; diff --git a/libraries/no-data-view/src/int16.spec.ts b/libraries/no-data-view/src/int16.spec.ts new file mode 100644 index 00000000..d67e1343 --- /dev/null +++ b/libraries/no-data-view/src/int16.spec.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + getInt16, + getInt16BigEndian, + getInt16LittleEndian, + setInt16, + setInt16BigEndian, + setInt16LittleEndian, +} from "./int16.js"; + +describe("getInt16", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 128]); + expect(getInt16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 127]); + expect(getInt16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0]); + expect(getInt16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2]); + expect(getInt16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([128, 0]); + expect(getInt16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([127, 255]); + expect(getInt16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0]); + expect(getInt16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt16(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2]); + expect(getInt16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2]); + expect(getInt16(array, 0, false)).toBe( + new DataView(array.buffer).getInt16(0, false), + ); + expect(getInt16(array, 0, true)).toBe( + new DataView(array.buffer).getInt16(0, true), + ); + }); +}); + +describe("setInt16", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, -0x8000, true); + const actual = new Uint8Array(2); + setInt16LittleEndian(actual, 0, -0x8000); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, 0x7fff, true); + const actual = new Uint8Array(2); + setInt16LittleEndian(actual, 0, 0x7fff); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, 0, true); + const actual = new Uint8Array(2); + setInt16LittleEndian(actual, 0, 0); + expect(actual).toEqual(expected); + }); + }); + + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, -0x8000, false); + const actual = new Uint8Array(2); + setInt16BigEndian(actual, 0, -0x8000); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, 0x7fff, false); + const actual = new Uint8Array(2); + setInt16BigEndian(actual, 0, 0x7fff); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(2); + new DataView(expected.buffer).setInt16(0, 0, false); + const actual = new Uint8Array(2); + setInt16BigEndian(actual, 0, 0); + expect(actual).toEqual(expected); + }); + }); + + it("should work for selected endianness", () => { + const expected = new Uint8Array(2); + const actual = new Uint8Array(2); + + new DataView(expected.buffer).setInt16(0, 0x7fff, false); + setInt16(actual, 0, 0x7fff, false); + expect(actual).toEqual(expected); + + new DataView(expected.buffer).setInt16(0, 0x7fff, true); + setInt16(actual, 0, 0x7fff, true); + expect(actual).toEqual(expected); + }); +}); diff --git a/libraries/no-data-view/src/int16.ts b/libraries/no-data-view/src/int16.ts new file mode 100644 index 00000000..905e79cc --- /dev/null +++ b/libraries/no-data-view/src/int16.ts @@ -0,0 +1,53 @@ +export function getInt16LittleEndian( + buffer: Uint8Array, + offset: number, +): number { + return ((buffer[offset]! | (buffer[offset + 1]! << 8)) << 16) >> 16; +} + +export function getInt16BigEndian(buffer: Uint8Array, offset: number): number { + return (((buffer[offset]! << 8) | buffer[offset + 1]!) << 16) >> 16; +} + +export function getInt16( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +) { + return littleEndian + ? ((buffer[offset]! | (buffer[offset + 1]! << 8)) << 16) >> 16 + : (((buffer[offset]! << 8) | buffer[offset + 1]!) << 16) >> 16; +} + +export function setInt16LittleEndian( + buffer: Uint8Array, + offset: number, + value: number, +): void { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; +} + +export function setInt16BigEndian( + buffer: Uint8Array, + offset: number, + value: number, +): void { + buffer[offset] = (value >> 8) & 0xff; + buffer[offset + 1] = value & 0xff; +} + +export function setInt16( + buffer: Uint8Array, + offset: number, + value: number, + littleEndian: boolean, +): void { + if (littleEndian) { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; + } else { + buffer[offset] = (value >> 8) & 0xff; + buffer[offset + 1] = value & 0xff; + } +} diff --git a/libraries/no-data-view/src/int32.spec.ts b/libraries/no-data-view/src/int32.spec.ts new file mode 100644 index 00000000..a5889974 --- /dev/null +++ b/libraries/no-data-view/src/int32.spec.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "@jest/globals"; + +import { getInt32, getInt32BigEndian, getInt32LittleEndian } from "./int32.js"; + +describe("getInt32", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 128]); + expect(getInt32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 255, 255, 127]); + expect(getInt32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 0]); + expect(getInt32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getInt32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([128, 0, 0, 0]); + expect(getInt32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([127, 255, 255, 255]); + expect(getInt32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 0]); + expect(getInt32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getInt32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getInt32(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getInt32(array, 0, false)).toBe( + new DataView(array.buffer).getInt32(0, false), + ); + expect(getInt32(array, 0, true)).toBe( + new DataView(array.buffer).getInt32(0, true), + ); + }); +}); diff --git a/libraries/no-data-view/src/int32.ts b/libraries/no-data-view/src/int32.ts new file mode 100644 index 00000000..89e6e867 --- /dev/null +++ b/libraries/no-data-view/src/int32.ts @@ -0,0 +1,36 @@ +export function getInt32LittleEndian( + buffer: Uint8Array, + offset: number, +): number { + return ( + buffer[offset]! | + (buffer[offset + 1]! << 8) | + (buffer[offset + 2]! << 16) | + (buffer[offset + 3]! << 24) + ); +} + +export function getInt32BigEndian(buffer: Uint8Array, offset: number): number { + return ( + (buffer[offset]! << 24) | + (buffer[offset + 1]! << 16) | + (buffer[offset + 2]! << 8) | + buffer[offset + 3]! + ); +} + +export function getInt32( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +) { + return littleEndian + ? buffer[offset]! | + (buffer[offset + 1]! << 8) | + (buffer[offset + 2]! << 16) | + (buffer[offset + 3]! << 24) + : (buffer[offset]! << 24) | + (buffer[offset + 1]! << 16) | + (buffer[offset + 2]! << 8) | + buffer[offset + 3]!; +} diff --git a/libraries/no-data-view/src/int64.spec.ts b/libraries/no-data-view/src/int64.spec.ts new file mode 100644 index 00000000..c06e997a --- /dev/null +++ b/libraries/no-data-view/src/int64.spec.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + getInt64, + getInt64BigEndian, + getInt64LittleEndian, + setInt64, + setInt64BigEndian, + setInt64LittleEndian, +} from "./int64.js"; + +describe("getInt64", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0x80]); + expect(getInt64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, + ]); + expect(getInt64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + expect(getInt64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ]); + expect(getInt64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0x80, 0, 0, 0, 0, 0, 0, 0]); + expect(getInt64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]); + expect(getInt64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + expect(getInt64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ]); + expect(getInt64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigInt64(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + expect(getInt64(array, 0, false)).toBe( + new DataView(array.buffer).getBigInt64(0, false), + ); + expect(getInt64(array, 0, true)).toBe( + new DataView(array.buffer).getBigInt64(0, true), + ); + }); +}); + +describe("setInt64", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64( + 0, + -0x8000_0000_0000_0000n, + true, + ); + const actual = new Uint8Array(8); + setInt64LittleEndian(actual, 0, -0x8000_0000_0000_0000n); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64( + 0, + 0x7fff_ffff_ffff_ffffn, + true, + ); + const actual = new Uint8Array(8); + setInt64LittleEndian(actual, 0, 0x7fff_ffff_ffff_ffffn); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64(0, 0n, true); + const actual = new Uint8Array(8); + setInt64LittleEndian(actual, 0, 0n); + expect(actual).toEqual(expected); + }); + }); + + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64( + 0, + -0x8000_0000_0000_0000n, + false, + ); + const actual = new Uint8Array(8); + setInt64BigEndian(actual, 0, -0x8000_0000_0000_0000n); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64( + 0, + 0x7fff_ffff_ffff_ffffn, + false, + ); + const actual = new Uint8Array(8); + setInt64BigEndian(actual, 0, 0x7fff_ffff_ffff_ffffn); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigInt64(0, 0n, false); + const actual = new Uint8Array(8); + setInt64BigEndian(actual, 0, 0n); + expect(actual).toEqual(expected); + }); + }); + + it("should work for selected endianness", () => { + const expected = new Uint8Array(8); + const actual = new Uint8Array(8); + + new DataView(expected.buffer).setBigInt64( + 0, + 0x7fff_ffff_ffff_ffffn, + false, + ); + setInt64(actual, 0, 0x7fff_ffff_ffff_ffffn, false); + expect(actual).toEqual(expected); + + new DataView(expected.buffer).setBigInt64( + 0, + 0x7fff_ffff_ffff_ffffn, + true, + ); + setInt64(actual, 0, 0x7fff_ffff_ffff_ffffn, true); + expect(actual).toEqual(expected); + }); +}); diff --git a/libraries/no-data-view/src/int64.ts b/libraries/no-data-view/src/int64.ts new file mode 100644 index 00000000..209f8843 --- /dev/null +++ b/libraries/no-data-view/src/int64.ts @@ -0,0 +1,109 @@ +export function getInt64LittleEndian( + buffer: Uint8Array, + offset: number, +): bigint { + return ( + BigInt(buffer[offset]!) | + (BigInt(buffer[offset + 1]!) << 8n) | + (BigInt(buffer[offset + 2]!) << 16n) | + (BigInt(buffer[offset + 3]!) << 24n) | + (BigInt(buffer[offset + 4]!) << 32n) | + (BigInt(buffer[offset + 5]!) << 40n) | + (BigInt(buffer[offset + 6]!) << 48n) | + (BigInt(buffer[offset + 7]! << 24) << 32n) + ); +} + +export function getInt64BigEndian(buffer: Uint8Array, offset: number): bigint { + return ( + (BigInt(buffer[offset]! << 24) << 32n) | + (BigInt(buffer[offset + 1]!) << 48n) | + (BigInt(buffer[offset + 2]!) << 40n) | + (BigInt(buffer[offset + 3]!) << 32n) | + (BigInt(buffer[offset + 4]!) << 24n) | + (BigInt(buffer[offset + 5]!) << 16n) | + (BigInt(buffer[offset + 6]!) << 8n) | + BigInt(buffer[offset + 7]!) + ); +} + +export function getInt64( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +): bigint { + return littleEndian + ? BigInt(buffer[offset]!) | + (BigInt(buffer[offset + 1]!) << 8n) | + (BigInt(buffer[offset + 2]!) << 16n) | + (BigInt(buffer[offset + 3]!) << 24n) | + (BigInt(buffer[offset + 4]!) << 32n) | + (BigInt(buffer[offset + 5]!) << 40n) | + (BigInt(buffer[offset + 6]!) << 48n) | + (BigInt(buffer[offset + 7]! << 24) << 32n) + : (BigInt(buffer[offset]! << 24) << 32n) | + (BigInt(buffer[offset + 1]!) << 48n) | + (BigInt(buffer[offset + 2]!) << 40n) | + (BigInt(buffer[offset + 3]!) << 32n) | + (BigInt(buffer[offset + 4]!) << 24n) | + (BigInt(buffer[offset + 5]!) << 16n) | + (BigInt(buffer[offset + 6]!) << 8n) | + BigInt(buffer[offset + 7]!); +} + +export function setInt64LittleEndian( + buffer: Uint8Array, + offset: number, + value: bigint, +): void { + buffer[offset] = Number(value & 0xffn); + buffer[offset + 1] = Number((value >> 8n) & 0xffn); + buffer[offset + 2] = Number((value >> 16n) & 0xffn); + buffer[offset + 3] = Number((value >> 24n) & 0xffn); + buffer[offset + 4] = Number((value >> 32n) & 0xffn); + buffer[offset + 5] = Number((value >> 40n) & 0xffn); + buffer[offset + 6] = Number((value >> 48n) & 0xffn); + buffer[offset + 7] = Number((value >> 56n) & 0xffn); +} + +export function setInt64BigEndian( + buffer: Uint8Array, + offset: number, + value: bigint, +): void { + buffer[offset] = Number((value >> 56n) & 0xffn); + buffer[offset + 1] = Number((value >> 48n) & 0xffn); + buffer[offset + 2] = Number((value >> 40n) & 0xffn); + buffer[offset + 3] = Number((value >> 32n) & 0xffn); + buffer[offset + 4] = Number((value >> 24n) & 0xffn); + buffer[offset + 5] = Number((value >> 16n) & 0xffn); + buffer[offset + 6] = Number((value >> 8n) & 0xffn); + buffer[offset + 7] = Number(value & 0xffn); +} + +export function setInt64( + buffer: Uint8Array, + offset: number, + value: bigint, + littleEndian: boolean, +): void { + if (littleEndian) { + buffer[offset] = Number(value & 0xffn); + buffer[offset + 1] = Number((value >> 8n) & 0xffn); + buffer[offset + 2] = Number((value >> 16n) & 0xffn); + buffer[offset + 3] = Number((value >> 24n) & 0xffn); + buffer[offset + 4] = Number((value >> 32n) & 0xffn); + buffer[offset + 5] = Number((value >> 40n) & 0xffn); + buffer[offset + 6] = Number((value >> 48n) & 0xffn); + buffer[offset + 7] = Number((value >> 56n) & 0xffn); + } else { + buffer[offset] = Number((value >> 56n) & 0xffn); + buffer[offset + 1] = Number((value >> 48n) & 0xffn); + buffer[offset + 2] = Number((value >> 40n) & 0xffn); + buffer[offset + 3] = Number((value >> 32n) & 0xffn); + buffer[offset + 4] = Number((value >> 24n) & 0xffn); + buffer[offset + 5] = Number((value >> 16n) & 0xffn); + buffer[offset + 6] = Number((value >> 8n) & 0xffn); + buffer[offset + 7] = Number(value & 0xffn); + } +} diff --git a/libraries/no-data-view/src/int8.spec.ts b/libraries/no-data-view/src/int8.spec.ts new file mode 100644 index 00000000..1e60ca00 --- /dev/null +++ b/libraries/no-data-view/src/int8.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "@jest/globals"; +import { getInt8, setInt8 } from "./int8.js"; + +describe("getInt8", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0x80]); + expect(getInt8(array, 0)).toBe(new DataView(array.buffer).getInt8(0)); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([0x7f]); + expect(getInt8(array, 0)).toBe(new DataView(array.buffer).getInt8(0)); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0]); + expect(getInt8(array, 0)).toBe(new DataView(array.buffer).getInt8(0)); + }); +}); + +describe("setInt8", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(1); + new DataView(expected.buffer).setInt8(0, -0x80); + const actual = new Uint8Array(1); + setInt8(actual, 0, -0x80); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(1); + new DataView(expected.buffer).setInt8(0, 0x7f); + const actual = new Uint8Array(1); + setInt8(actual, 0, 0x7f); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(1); + new DataView(expected.buffer).setInt8(0, 0); + const actual = new Uint8Array(1); + setInt8(actual, 0, 0); + expect(actual).toEqual(expected); + }); +}); diff --git a/libraries/no-data-view/src/int8.ts b/libraries/no-data-view/src/int8.ts new file mode 100644 index 00000000..191d6b6c --- /dev/null +++ b/libraries/no-data-view/src/int8.ts @@ -0,0 +1,11 @@ +export function getInt8(buffer: Uint8Array, offset: number): number { + return (buffer[offset]! << 24) >> 24; +} + +export function setInt8( + buffer: Uint8Array, + offset: number, + value: number, +): void { + buffer[offset] = value >>> 0; +} diff --git a/libraries/no-data-view/src/uint16.spec.ts b/libraries/no-data-view/src/uint16.spec.ts new file mode 100644 index 00000000..c86b33bc --- /dev/null +++ b/libraries/no-data-view/src/uint16.spec.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + getUint16, + getUint16BigEndian, + getUint16LittleEndian, +} from "./uint16.js"; + +describe("getUint16", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0]); + expect(getUint16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 255]); + expect(getUint16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 128]); + expect(getUint16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2]); + expect(getUint16LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0]); + expect(getUint16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 255]); + expect(getUint16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([128, 255]); + expect(getUint16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2]); + expect(getUint16BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2]); + expect(getUint16(array, 0, false)).toBe( + new DataView(array.buffer).getUint16(0, false), + ); + expect(getUint16(array, 0, true)).toBe( + new DataView(array.buffer).getUint16(0, true), + ); + }); +}); diff --git a/libraries/no-data-view/src/uint16.ts b/libraries/no-data-view/src/uint16.ts new file mode 100644 index 00000000..7422a109 --- /dev/null +++ b/libraries/no-data-view/src/uint16.ts @@ -0,0 +1,20 @@ +export function getUint16LittleEndian( + buffer: Uint8Array, + offset: number, +): number { + return buffer[offset]! | (buffer[offset + 1]! << 8); +} + +export function getUint16BigEndian(buffer: Uint8Array, offset: number): number { + return (buffer[offset]! << 8) | buffer[offset + 1]!; +} + +export function getUint16( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +) { + return littleEndian + ? buffer[offset]! | (buffer[offset + 1]! << 8) + : buffer[offset + 1]! | (buffer[offset]! << 8); +} diff --git a/libraries/no-data-view/src/uint32.spec.ts b/libraries/no-data-view/src/uint32.spec.ts new file mode 100644 index 00000000..b4afdb5c --- /dev/null +++ b/libraries/no-data-view/src/uint32.spec.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + getUint32, + getUint32BigEndian, + getUint32LittleEndian, + setUint32, + setUint32BigEndian, + setUint32LittleEndian, +} from "./uint32.js"; + +describe("getUint32", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 0]); + expect(getUint32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 255, 255, 255]); + expect(getUint32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 128]); + expect(getUint32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getUint32LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 0]); + expect(getUint32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([255, 255, 255, 255]); + expect(getUint32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([128, 0, 0, 0]); + expect(getUint32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getUint32BigEndian(array, 0)).toBe( + new DataView(array.buffer).getUint32(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2, 3, 4]); + expect(getUint32(array, 0, false)).toBe( + new DataView(array.buffer).getUint32(0, false), + ); + expect(getUint32(array, 0, true)).toBe( + new DataView(array.buffer).getUint32(0, true), + ); + }); +}); + +describe("setUint32", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0, true); + const actual = new Uint8Array(4); + setUint32LittleEndian(actual, 0, 0); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0xffff_ffff, true); + const actual = new Uint8Array(4); + setUint32LittleEndian(actual, 0, 0xffff_ffff); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0x8000_0000, true); + const actual = new Uint8Array(4); + setUint32LittleEndian(actual, 0, 0x8000_0000); + expect(actual).toEqual(expected); + }); + }); + + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0, false); + const actual = new Uint8Array(4); + setUint32BigEndian(actual, 0, 0); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0xffff_ffff, false); + const actual = new Uint8Array(4); + setUint32BigEndian(actual, 0, 0xffff_ffff); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(4); + new DataView(expected.buffer).setUint32(0, 0x8000_0000, false); + const actual = new Uint8Array(4); + setUint32BigEndian(actual, 0, 0x8000_0000); + expect(actual).toEqual(expected); + }); + }); + + it("should work for selected endianness", () => { + const expected = new Uint8Array(4); + const actual = new Uint8Array(4); + + new DataView(expected.buffer).setUint32(0, 0xffff_ffff, false); + setUint32(actual, 0, 0xffff_ffff, false); + expect(actual).toEqual(expected); + + new DataView(expected.buffer).setUint32(0, 0xffff_ffff, true); + setUint32(actual, 0, 0xffff_ffff, true); + expect(actual).toEqual(expected); + }); +}); diff --git a/libraries/no-data-view/src/uint32.ts b/libraries/no-data-view/src/uint32.ts new file mode 100644 index 00000000..287aaef2 --- /dev/null +++ b/libraries/no-data-view/src/uint32.ts @@ -0,0 +1,81 @@ +export function getUint32LittleEndian( + buffer: Uint8Array, + offset: number, +): number { + return ( + (buffer[offset]! | + (buffer[offset + 1]! << 8) | + (buffer[offset + 2]! << 16) | + (buffer[offset + 3]! << 24)) >>> + 0 + ); +} + +export function getUint32BigEndian(buffer: Uint8Array, offset: number): number { + return ( + ((buffer[offset]! << 24) | + (buffer[offset + 1]! << 16) | + (buffer[offset + 2]! << 8) | + buffer[offset + 3]!) >>> + 0 + ); +} + +export function getUint32( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +) { + return littleEndian + ? (buffer[offset]! | + (buffer[offset + 1]! << 8) | + (buffer[offset + 2]! << 16) | + (buffer[offset + 3]! << 24)) >>> + 0 + : ((buffer[offset]! << 24) | + (buffer[offset + 1]! << 16) | + (buffer[offset + 2]! << 8) | + buffer[offset + 3]!) >>> + 0; +} + +export function setUint32LittleEndian( + buffer: Uint8Array, + offset: number, + value: number, +): void { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; + buffer[offset + 2] = (value >> 16) & 0xff; + buffer[offset + 3] = (value >> 24) & 0xff; +} + +export function setUint32BigEndian( + buffer: Uint8Array, + offset: number, + value: number, +): void { + buffer[offset] = (value >> 24) & 0xff; + buffer[offset + 1] = (value >> 16) & 0xff; + buffer[offset + 2] = (value >> 8) & 0xff; + buffer[offset + 3] = value & 0xff; +} + +export function setUint32( + buffer: Uint8Array, + offset: number, + value: number, + littleEndian: boolean, +): void { + if (littleEndian) { + buffer[offset] = value & 0xff; + buffer[offset + 1] = (value >> 8) & 0xff; + buffer[offset + 2] = (value >> 16) & 0xff; + buffer[offset + 3] = (value >> 24) & 0xff; + } else { + buffer[offset] = (value >> 24) & 0xff; + buffer[offset + 1] = (value >> 16) & 0xff; + buffer[offset + 2] = (value >> 8) & 0xff; + buffer[offset + 3] = value & 0xff; + } +} diff --git a/libraries/no-data-view/src/uint64.spec.ts b/libraries/no-data-view/src/uint64.spec.ts new file mode 100644 index 00000000..89e2ed95 --- /dev/null +++ b/libraries/no-data-view/src/uint64.spec.ts @@ -0,0 +1,181 @@ +import { describe, expect, it } from "@jest/globals"; + +import { + getUint64, + getUint64BigEndian, + getUint64LittleEndian, + setUint64, + setUint64BigEndian, + setUint64LittleEndian, +} from "./uint64.js"; + +describe("getUint64", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + expect(getUint64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, true), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]); + expect(getUint64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, true), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0x80]); + expect(getUint64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, true), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ]); + expect(getUint64LittleEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, true), + ); + }); + }); + + describe("big endian", () => { + it("should work for minimal value", () => { + const array = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + expect(getUint64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, false), + ); + }); + + it("should work for maximal value", () => { + const array = new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]); + expect(getUint64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, false), + ); + }); + + it("should work for middle value", () => { + const array = new Uint8Array([0x80, 0, 0, 0, 0, 0, 0, 0]); + expect(getUint64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, false), + ); + }); + + it("should work for random value", () => { + const array = new Uint8Array([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + ]); + expect(getUint64BigEndian(array, 0)).toBe( + new DataView(array.buffer).getBigUint64(0, false), + ); + }); + }); + + it("should work for selected endianness", () => { + const array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + expect(getUint64(array, 0, false)).toBe( + new DataView(array.buffer).getBigUint64(0, false), + ); + expect(getUint64(array, 0, true)).toBe( + new DataView(array.buffer).getBigUint64(0, true), + ); + }); +}); + +describe("setUint64", () => { + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64(0, 0n, true); + const actual = new Uint8Array(8); + setUint64LittleEndian(actual, 0, 0n); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64( + 0, + 0xffff_ffff_ffff_ffffn, + true, + ); + const actual = new Uint8Array(8); + setUint64LittleEndian(actual, 0, 0xffff_ffff_ffff_ffffn); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64( + 0, + 0x8000_0000_0000_0000n, + true, + ); + const actual = new Uint8Array(8); + setUint64LittleEndian(actual, 0, 0x8000_0000_0000_0000n); + expect(actual).toEqual(expected); + }); + }); + + describe("little endian", () => { + it("should work for minimal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64(0, 0n, false); + const actual = new Uint8Array(8); + setUint64BigEndian(actual, 0, 0n); + expect(actual).toEqual(expected); + }); + + it("should work for maximal value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64( + 0, + 0xffff_ffff_ffff_ffffn, + false, + ); + const actual = new Uint8Array(8); + setUint64BigEndian(actual, 0, 0xffff_ffff_ffff_ffffn); + expect(actual).toEqual(expected); + }); + + it("should work for middle value", () => { + const expected = new Uint8Array(8); + new DataView(expected.buffer).setBigUint64( + 0, + 0x8000_0000_0000_0000n, + false, + ); + const actual = new Uint8Array(8); + setUint64BigEndian(actual, 0, 0x8000_0000_0000_0000n); + expect(actual).toEqual(expected); + }); + }); + + it("should work for selected endianness", () => { + const expected = new Uint8Array(8); + const actual = new Uint8Array(8); + + new DataView(expected.buffer).setBigUint64( + 0, + 0xffff_ffff_ffff_ffffn, + false, + ); + setUint64(actual, 0, 0xffff_ffff_ffff_ffffn, false); + expect(actual).toEqual(expected); + + new DataView(expected.buffer).setBigUint64( + 0, + 0xffff_ffff_ffff_ffffn, + true, + ); + setUint64(actual, 0, 0xffff_ffff_ffff_ffffn, true); + expect(actual).toEqual(expected); + }); +}); diff --git a/libraries/no-data-view/src/uint64.ts b/libraries/no-data-view/src/uint64.ts new file mode 100644 index 00000000..e5a33916 --- /dev/null +++ b/libraries/no-data-view/src/uint64.ts @@ -0,0 +1,109 @@ +export function getUint64LittleEndian( + buffer: Uint8Array, + offset: number, +): bigint { + return ( + BigInt(buffer[offset]!) | + (BigInt(buffer[offset + 1]!) << 8n) | + (BigInt(buffer[offset + 2]!) << 16n) | + (BigInt(buffer[offset + 3]!) << 24n) | + (BigInt(buffer[offset + 4]!) << 32n) | + (BigInt(buffer[offset + 5]!) << 40n) | + (BigInt(buffer[offset + 6]!) << 48n) | + (BigInt(buffer[offset + 7]!) << 56n) + ); +} + +export function getUint64BigEndian(buffer: Uint8Array, offset: number): bigint { + return ( + (BigInt(buffer[offset]!) << 56n) | + (BigInt(buffer[offset + 1]!) << 48n) | + (BigInt(buffer[offset + 2]!) << 40n) | + (BigInt(buffer[offset + 3]!) << 32n) | + (BigInt(buffer[offset + 4]!) << 24n) | + (BigInt(buffer[offset + 5]!) << 16n) | + (BigInt(buffer[offset + 6]!) << 8n) | + BigInt(buffer[offset + 7]!) + ); +} + +export function getUint64( + buffer: Uint8Array, + offset: number, + littleEndian: boolean, +): bigint { + return littleEndian + ? BigInt(buffer[offset]!) | + (BigInt(buffer[offset + 1]!) << 8n) | + (BigInt(buffer[offset + 2]!) << 16n) | + (BigInt(buffer[offset + 3]!) << 24n) | + (BigInt(buffer[offset + 4]!) << 32n) | + (BigInt(buffer[offset + 5]!) << 40n) | + (BigInt(buffer[offset + 6]!) << 48n) | + (BigInt(buffer[offset + 7]!) << 56n) + : (BigInt(buffer[offset]!) << 56n) | + (BigInt(buffer[offset + 1]!) << 48n) | + (BigInt(buffer[offset + 2]!) << 40n) | + (BigInt(buffer[offset + 3]!) << 32n) | + (BigInt(buffer[offset + 4]!) << 24n) | + (BigInt(buffer[offset + 5]!) << 16n) | + (BigInt(buffer[offset + 6]!) << 8n) | + BigInt(buffer[offset + 7]!); +} + +export function setUint64LittleEndian( + buffer: Uint8Array, + offset: number, + value: bigint, +): void { + buffer[offset] = Number(value & 0xffn); + buffer[offset + 1] = Number((value >> 8n) & 0xffn); + buffer[offset + 2] = Number((value >> 16n) & 0xffn); + buffer[offset + 3] = Number((value >> 24n) & 0xffn); + buffer[offset + 4] = Number((value >> 32n) & 0xffn); + buffer[offset + 5] = Number((value >> 40n) & 0xffn); + buffer[offset + 6] = Number((value >> 48n) & 0xffn); + buffer[offset + 7] = Number((value >> 56n) & 0xffn); +} + +export function setUint64BigEndian( + buffer: Uint8Array, + offset: number, + value: bigint, +): void { + buffer[offset] = Number((value >> 56n) & 0xffn); + buffer[offset + 1] = Number((value >> 48n) & 0xffn); + buffer[offset + 2] = Number((value >> 40n) & 0xffn); + buffer[offset + 3] = Number((value >> 32n) & 0xffn); + buffer[offset + 4] = Number((value >> 24n) & 0xffn); + buffer[offset + 5] = Number((value >> 16n) & 0xffn); + buffer[offset + 6] = Number((value >> 8n) & 0xffn); + buffer[offset + 7] = Number(value & 0xffn); +} + +export function setUint64( + buffer: Uint8Array, + offset: number, + value: bigint, + littleEndian: boolean, +): void { + if (littleEndian) { + buffer[offset] = Number(value & 0xffn); + buffer[offset + 1] = Number((value >> 8n) & 0xffn); + buffer[offset + 2] = Number((value >> 16n) & 0xffn); + buffer[offset + 3] = Number((value >> 24n) & 0xffn); + buffer[offset + 4] = Number((value >> 32n) & 0xffn); + buffer[offset + 5] = Number((value >> 40n) & 0xffn); + buffer[offset + 6] = Number((value >> 48n) & 0xffn); + buffer[offset + 7] = Number((value >> 56n) & 0xffn); + } else { + buffer[offset] = Number((value >> 56n) & 0xffn); + buffer[offset + 1] = Number((value >> 48n) & 0xffn); + buffer[offset + 2] = Number((value >> 40n) & 0xffn); + buffer[offset + 3] = Number((value >> 32n) & 0xffn); + buffer[offset + 4] = Number((value >> 24n) & 0xffn); + buffer[offset + 5] = Number((value >> 16n) & 0xffn); + buffer[offset + 6] = Number((value >> 8n) & 0xffn); + buffer[offset + 7] = Number(value & 0xffn); + } +} diff --git a/libraries/no-data-view/tsconfig.build.json b/libraries/no-data-view/tsconfig.build.json new file mode 100644 index 00000000..2cb23249 --- /dev/null +++ b/libraries/no-data-view/tsconfig.build.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json" +} diff --git a/libraries/no-data-view/tsconfig.json b/libraries/no-data-view/tsconfig.json new file mode 100644 index 00000000..85fc5a7a --- /dev/null +++ b/libraries/no-data-view/tsconfig.json @@ -0,0 +1,10 @@ +{ + "references": [ + { + "path": "./tsconfig.test.json" + }, + { + "path": "./tsconfig.build.json" + }, + ] +} diff --git a/libraries/no-data-view/tsconfig.test.json b/libraries/no-data-view/tsconfig.test.json new file mode 100644 index 00000000..e987f757 --- /dev/null +++ b/libraries/no-data-view/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "types": [ + ], + }, + "exclude": [] +} diff --git a/libraries/pcm-player/package.json b/libraries/pcm-player/package.json index 8477fbd4..86af9d0e 100644 --- a/libraries/pcm-player/package.json +++ b/libraries/pcm-player/package.json @@ -29,9 +29,7 @@ "lint": "run-eslint && prettier src/**/*.ts --write --tab-width 4", "prepublishOnly": "npm run build" }, - "dependencies": { - "tslib": "^2.6.2" - }, + "dependencies": {}, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", "@types/audioworklet": "^0.0.54", diff --git a/libraries/scrcpy-decoder-tinyh264/package.json b/libraries/scrcpy-decoder-tinyh264/package.json index 0e432d23..8abe7ebf 100644 --- a/libraries/scrcpy-decoder-tinyh264/package.json +++ b/libraries/scrcpy-decoder-tinyh264/package.json @@ -39,7 +39,6 @@ "@yume-chan/scrcpy": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", "tinyh264": "^0.0.7", - "tslib": "^2.6.2", "yuv-buffer": "^1.0.0", "yuv-canvas": "^1.2.11" }, diff --git a/libraries/scrcpy-decoder-webcodecs/package.json b/libraries/scrcpy-decoder-webcodecs/package.json index 5f7fe85b..f1967be2 100644 --- a/libraries/scrcpy-decoder-webcodecs/package.json +++ b/libraries/scrcpy-decoder-webcodecs/package.json @@ -34,10 +34,10 @@ }, "dependencies": { "@yume-chan/event": "workspace:^0.0.23", + "@yume-chan/no-data-view": "workspace:^0.0.23", "@yume-chan/scrcpy": "workspace:^0.0.23", "@yume-chan/scrcpy-decoder-tinyh264": "workspace:^0.0.23", - "@yume-chan/stream-extra": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/stream-extra": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/scrcpy-decoder-webcodecs/src/index.ts b/libraries/scrcpy-decoder-webcodecs/src/index.ts index cb320d40..3732585e 100644 --- a/libraries/scrcpy-decoder-webcodecs/src/index.ts +++ b/libraries/scrcpy-decoder-webcodecs/src/index.ts @@ -8,6 +8,7 @@ import { h264ParseConfiguration, h265ParseConfiguration, } from "@yume-chan/scrcpy"; +import { getUint32LittleEndian } from "@yume-chan/no-data-view"; import type { ScrcpyVideoDecoder, ScrcpyVideoDecoderCapability, @@ -22,15 +23,6 @@ function toHex(value: number) { return value.toString(16).padStart(2, "0").toUpperCase(); } -function toUint32Le(data: Uint8Array, offset: number) { - return ( - data[offset]! | - (data[offset + 1]! << 8) | - (data[offset + 2]! << 16) | - (data[offset + 3]! << 24) - ); -} - export class WebCodecsDecoder implements ScrcpyVideoDecoder { static isSupported() { return typeof globalThis.VideoDecoder !== "undefined"; @@ -137,74 +129,83 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder { this.#animationFrameId = requestAnimationFrame(this.#onFramePresented); }; + #configureH264(data: Uint8Array) { + const { + profileIndex, + constraintSet, + levelIndex, + croppedWidth, + croppedHeight, + } = h264ParseConfiguration(data); + + this.#canvas.width = croppedWidth; + this.#canvas.height = croppedHeight; + this.#sizeChanged.fire({ + width: croppedWidth, + height: croppedHeight, + }); + + // https://www.rfc-editor.org/rfc/rfc6381#section-3.3 + // ISO Base Media File Format Name Space + const codec = + "avc1." + + toHex(profileIndex) + + toHex(constraintSet) + + toHex(levelIndex); + this.#decoder.configure({ + codec: codec, + optimizeForLatency: true, + }); + } + + #configureH265(data: Uint8Array) { + const { + generalProfileSpace, + generalProfileIndex, + generalProfileCompatibilitySet, + generalTierFlag, + generalLevelIndex, + generalConstraintSet, + croppedWidth, + croppedHeight, + } = h265ParseConfiguration(data); + + this.#canvas.width = croppedWidth; + this.#canvas.height = croppedHeight; + this.#sizeChanged.fire({ + width: croppedWidth, + height: croppedHeight, + }); + + const codec = [ + "hev1", + ["", "A", "B", "C"][generalProfileSpace]! + + generalProfileIndex.toString(), + getUint32LittleEndian(generalProfileCompatibilitySet, 0).toString( + 16, + ), + (generalTierFlag ? "H" : "L") + generalLevelIndex.toString(), + getUint32LittleEndian(generalConstraintSet, 0) + .toString(16) + .toUpperCase(), + getUint32LittleEndian(generalConstraintSet, 4) + .toString(16) + .toUpperCase(), + ].join("."); + this.#decoder.configure({ + codec, + optimizeForLatency: true, + }); + } + #configure(data: Uint8Array) { switch (this.#codec) { - case ScrcpyVideoCodecId.H264: { - const { - profileIndex, - constraintSet, - levelIndex, - croppedWidth, - croppedHeight, - } = h264ParseConfiguration(data); - - this.#canvas.width = croppedWidth; - this.#canvas.height = croppedHeight; - this.#sizeChanged.fire({ - width: croppedWidth, - height: croppedHeight, - }); - - // https://www.rfc-editor.org/rfc/rfc6381#section-3.3 - // ISO Base Media File Format Name Space - const codec = `avc1.${[profileIndex, constraintSet, levelIndex] - .map(toHex) - .join("")}`; - this.#decoder.configure({ - codec: codec, - optimizeForLatency: true, - }); + case ScrcpyVideoCodecId.H264: + this.#configureH264(data); break; - } - case ScrcpyVideoCodecId.H265: { - const { - generalProfileSpace, - generalProfileIndex, - generalProfileCompatibilitySet, - generalTierFlag, - generalLevelIndex, - generalConstraintSet, - croppedWidth, - croppedHeight, - } = h265ParseConfiguration(data); - - this.#canvas.width = croppedWidth; - this.#canvas.height = croppedHeight; - this.#sizeChanged.fire({ - width: croppedWidth, - height: croppedHeight, - }); - - const codec = [ - "hev1", - ["", "A", "B", "C"][generalProfileSpace]! + - generalProfileIndex.toString(), - toUint32Le(generalProfileCompatibilitySet, 0).toString(16), - (generalTierFlag ? "H" : "L") + - generalLevelIndex.toString(), - toUint32Le(generalConstraintSet, 0) - .toString(16) - .toUpperCase(), - toUint32Le(generalConstraintSet, 4) - .toString(16) - .toUpperCase(), - ].join("."); - this.#decoder.configure({ - codec, - optimizeForLatency: true, - }); + case ScrcpyVideoCodecId.H265: + this.#configureH265(data); break; - } } this.#config = data; } diff --git a/libraries/scrcpy-decoder-webcodecs/tsconfig.json b/libraries/scrcpy-decoder-webcodecs/tsconfig.json index 85fc5a7a..04f9aba7 100644 --- a/libraries/scrcpy-decoder-webcodecs/tsconfig.json +++ b/libraries/scrcpy-decoder-webcodecs/tsconfig.json @@ -1,5 +1,8 @@ { "references": [ + { + "path": "../no-data-view/tsconfig.build.json" + }, { "path": "./tsconfig.test.json" }, diff --git a/libraries/scrcpy/package.json b/libraries/scrcpy/package.json index b9fac034..7361f389 100644 --- a/libraries/scrcpy/package.json +++ b/libraries/scrcpy/package.json @@ -33,6 +33,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@yume-chan/no-data-view": "workspace:^0.0.23", "@yume-chan/stream-extra": "workspace:^0.0.23", "@yume-chan/struct": "workspace:^0.0.23", "tslib": "^2.6.2" diff --git a/libraries/scrcpy/src/options/1_16/float-to-uint16.ts b/libraries/scrcpy/src/options/1_16/float-to-uint16.ts index 3f21625a..7986efbe 100644 --- a/libraries/scrcpy/src/options/1_16/float-to-uint16.ts +++ b/libraries/scrcpy/src/options/1_16/float-to-uint16.ts @@ -1,4 +1,6 @@ -import { NumberFieldDefinition, NumberFieldType } from "@yume-chan/struct"; +import { getUint16 } from "@yume-chan/no-data-view"; +import type { NumberFieldType } from "@yume-chan/struct"; +import { NumberFieldDefinition } from "@yume-chan/struct"; export function clamp(value: number, min: number, max: number): number { if (value < min) { @@ -16,7 +18,7 @@ export const ScrcpyFloatToUint16NumberType: NumberFieldType = { size: 2, signed: false, deserialize(array, littleEndian) { - const value = NumberFieldType.Uint16.deserialize(array, littleEndian); + const value = getUint16(array, 0, littleEndian); // https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L22 return value === 0xffff ? 1 : value / 0x10000; }, @@ -24,7 +26,7 @@ export const ScrcpyFloatToUint16NumberType: NumberFieldType = { // https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L51 value = clamp(value, -1, 1); value = value === 1 ? 0xffff : value * 0x10000; - NumberFieldType.Uint16.serialize(dataView, offset, value, littleEndian); + dataView.setUint16(offset, value, littleEndian); }, }; diff --git a/libraries/scrcpy/src/options/1_16/options.ts b/libraries/scrcpy/src/options/1_16/options.ts index 7f416448..ef11d33e 100644 --- a/libraries/scrcpy/src/options/1_16/options.ts +++ b/libraries/scrcpy/src/options/1_16/options.ts @@ -5,7 +5,7 @@ import { TransformStream, } from "@yume-chan/stream-extra"; import type { AsyncExactReadable, ValueOrPromise } from "@yume-chan/struct"; -import { NumberFieldType, decodeUtf8 } from "@yume-chan/struct"; +import { decodeUtf8 } from "@yume-chan/struct"; import type { ScrcpyBackOrScreenOnControlMessage, @@ -23,6 +23,10 @@ import { ScrcpyVideoCodecId } from "../codec.js"; import type { ScrcpyDisplay, ScrcpyEncoder, ScrcpyOptions } from "../types.js"; import { toScrcpyOptionValue } from "../types.js"; +import { + getUint16BigEndian, + getUint32BigEndian, +} from "@yume-chan/no-data-view"; import { CodecOptions } from "./codec-options.js"; import type { ScrcpyOptionsInit1_16 } from "./init.js"; import { ScrcpyLogLevel1_16, ScrcpyVideoOrientation1_16 } from "./init.js"; @@ -84,13 +88,13 @@ export class ScrcpyOptions1_16 implements ScrcpyOptions { } static async parseUint16BE(stream: AsyncExactReadable): Promise { - const buffer = await stream.readExactly(NumberFieldType.Uint16.size); - return NumberFieldType.Uint16.deserialize(buffer, false); + const buffer = await stream.readExactly(2); + return getUint16BigEndian(buffer, 0); } static async parseUint32BE(stream: AsyncExactReadable): Promise { - const buffer = await stream.readExactly(NumberFieldType.Uint32.size); - return NumberFieldType.Uint32.deserialize(buffer, false); + const buffer = await stream.readExactly(4); + return getUint32BigEndian(buffer, 0); } value: Required; diff --git a/libraries/scrcpy/src/options/1_25/scroll.ts b/libraries/scrcpy/src/options/1_25/scroll.ts index 92380f70..118ddc11 100644 --- a/libraries/scrcpy/src/options/1_25/scroll.ts +++ b/libraries/scrcpy/src/options/1_25/scroll.ts @@ -1,7 +1,6 @@ -import Struct, { - NumberFieldDefinition, - NumberFieldType, -} from "@yume-chan/struct"; +import { getInt16 } from "@yume-chan/no-data-view"; +import type { NumberFieldType } from "@yume-chan/struct"; +import Struct, { NumberFieldDefinition } from "@yume-chan/struct"; import type { ScrcpyInjectScrollControlMessage } from "../../control/index.js"; import { ScrcpyControlMessageType } from "../../control/index.js"; @@ -12,7 +11,7 @@ export const ScrcpyFloatToInt16NumberType: NumberFieldType = { size: 2, signed: true, deserialize(array, littleEndian) { - const value = NumberFieldType.Int16.deserialize(array, littleEndian); + const value = getInt16(array, 0, littleEndian); // https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/server/src/main/java/com/genymobile/scrcpy/Binary.java#L34 return value === 0x7fff ? 1 : value / 0x8000; }, @@ -20,7 +19,7 @@ export const ScrcpyFloatToInt16NumberType: NumberFieldType = { // https://github.com/Genymobile/scrcpy/blob/1f138aef41de651668043b32c4effc2d4adbfc44/app/src/util/binary.h#L65 value = clamp(value, -1, 1); value = value === 1 ? 0x7fff : value * 0x8000; - NumberFieldType.Int16.serialize(dataView, offset, value, littleEndian); + dataView.setInt16(offset, value, littleEndian); }, }; diff --git a/libraries/scrcpy/src/options/2_0.ts b/libraries/scrcpy/src/options/2_0.ts index 090bd127..47b57a63 100644 --- a/libraries/scrcpy/src/options/2_0.ts +++ b/libraries/scrcpy/src/options/2_0.ts @@ -4,13 +4,14 @@ import { PushReadableStream, } from "@yume-chan/stream-extra"; import type { ValueOrPromise } from "@yume-chan/struct"; -import Struct, { NumberFieldType, placeholder } from "@yume-chan/struct"; +import Struct, { placeholder } from "@yume-chan/struct"; import type { AndroidMotionEventAction, ScrcpyInjectTouchControlMessage, } from "../control/index.js"; +import { getUint32BigEndian } from "@yume-chan/no-data-view"; import { CodecOptions, ScrcpyFloatToUint16FieldDefinition, @@ -256,14 +257,9 @@ export class ScrcpyOptions2_0 extends ScrcpyOptionsBase< ): ValueOrPromise { return (async (): Promise => { const buffered = new BufferedReadableStream(stream); - const buffer = await buffered.readExactly( - NumberFieldType.Uint32.size, - ); + const buffer = await buffered.readExactly(4); - const codecMetadataValue = NumberFieldType.Uint32.deserialize( - buffer, - false, - ); + const codecMetadataValue = getUint32BigEndian(buffer, 0); // Server will send `0x00_00_00_00` and `0x00_00_00_01` even if `sendCodecMeta` is false switch (codecMetadataValue) { case 0x00_00_00_00: diff --git a/libraries/stream-extra/package.json b/libraries/stream-extra/package.json index a79531ba..9a983e89 100644 --- a/libraries/stream-extra/package.json +++ b/libraries/stream-extra/package.json @@ -33,8 +33,7 @@ }, "dependencies": { "@yume-chan/async": "^2.2.0", - "@yume-chan/struct": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/struct": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/struct/package.json b/libraries/struct/package.json index 446415fb..05d6f065 100644 --- a/libraries/struct/package.json +++ b/libraries/struct/package.json @@ -34,8 +34,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@yume-chan/dataview-bigint-polyfill": "workspace:^0.0.23", - "tslib": "^2.6.2" + "@yume-chan/no-data-view": "workspace:^0.0.23" }, "devDependencies": { "@jest/globals": "^30.0.0-alpha.3", diff --git a/libraries/struct/src/basic/field-value.spec.ts b/libraries/struct/src/basic/field-value.spec.ts index d1ae396b..5c172259 100644 --- a/libraries/struct/src/basic/field-value.spec.ts +++ b/libraries/struct/src/basic/field-value.spec.ts @@ -13,8 +13,13 @@ describe("StructFieldValue", () => { describe(".constructor", () => { it("should save parameters", () => { class MockStructFieldValue extends StructFieldValue { - serialize(dataView: DataView, offset: number): void { + serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { void dataView; + void array; void offset; throw new Error("Method not implemented."); } @@ -78,8 +83,13 @@ describe("StructFieldValue", () => { } class MockStructFieldValue extends StructFieldValue { - serialize(dataView: DataView, offset: number): void { + serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { void dataView; + void array; void offset; throw new Error("Method not implemented."); } @@ -99,8 +109,13 @@ describe("StructFieldValue", () => { describe("#set", () => { it("should update its internal value", () => { class MockStructFieldValue extends StructFieldValue { - serialize(dataView: DataView, offset: number): void { + serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { void dataView; + void array; void offset; throw new Error("Method not implemented."); } diff --git a/libraries/struct/src/basic/field-value.ts b/libraries/struct/src/basic/field-value.ts index 417bd37a..2eb60af8 100644 --- a/libraries/struct/src/basic/field-value.ts +++ b/libraries/struct/src/basic/field-value.ts @@ -67,5 +67,9 @@ export abstract class StructFieldValue< /** * When implemented in derived classes, serializes this field into `dataView` at `offset` */ - abstract serialize(dataView: DataView, offset: number): void; + abstract serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void; } diff --git a/libraries/struct/src/struct.ts b/libraries/struct/src/struct.ts index 69c7c0b0..dcdc7efb 100644 --- a/libraries/struct/src/struct.ts +++ b/libraries/struct/src/struct.ts @@ -697,7 +697,7 @@ export class Struct< ); let offset = 0; for (const { fieldValue, size } of fieldsInfo) { - fieldValue.serialize(dataView, offset); + fieldValue.serialize(dataView, output, offset); offset += size; } diff --git a/libraries/struct/src/types/bigint.ts b/libraries/struct/src/types/bigint.ts index dff626c5..84e775f0 100644 --- a/libraries/struct/src/types/bigint.ts +++ b/libraries/struct/src/types/bigint.ts @@ -1,10 +1,9 @@ import { - getBigInt64, - getBigUint64, - setBigInt64, - setBigUint64, -} from "@yume-chan/dataview-bigint-polyfill/esm/fallback.js"; - + getInt64, + getUint64, + setInt64, + setUint64, +} from "@yume-chan/no-data-view"; import type { AsyncExactReadable, ExactReadable, @@ -15,17 +14,17 @@ import { StructFieldDefinition, StructFieldValue } from "../basic/index.js"; import { SyncPromise } from "../sync-promise.js"; import type { ValueOrPromise } from "../utils.js"; -type DataViewBigInt64Getter = ( - dataView: DataView, +type GetBigInt64 = ( + array: Uint8Array, byteOffset: number, - littleEndian: boolean | undefined, + littleEndian: boolean, ) => bigint; -type DataViewBigInt64Setter = ( - dataView: DataView, +type SetBigInt64 = ( + array: Uint8Array, byteOffset: number, value: bigint, - littleEndian: boolean | undefined, + littleEndian: boolean, ) => void; export class BigIntFieldType { @@ -33,23 +32,19 @@ export class BigIntFieldType { readonly size: number; - readonly getter: DataViewBigInt64Getter; + readonly getter: GetBigInt64; - readonly setter: DataViewBigInt64Setter; + readonly setter: SetBigInt64; - constructor( - size: number, - getter: DataViewBigInt64Getter, - setter: DataViewBigInt64Setter, - ) { + constructor(size: number, getter: GetBigInt64, setter: SetBigInt64) { this.size = size; this.getter = getter; this.setter = setter; } - static readonly Int64 = new BigIntFieldType(8, getBigInt64, setBigInt64); + static readonly Int64 = new BigIntFieldType(8, getInt64, setInt64); - static readonly Uint64 = new BigIntFieldType(8, getBigUint64, setBigUint64); + static readonly Uint64 = new BigIntFieldType(8, getUint64, setUint64); } export class BigIntFieldDefinition< @@ -95,12 +90,7 @@ export class BigIntFieldDefinition< return stream.readExactly(this.getSize()); }) .then((array) => { - const view = new DataView( - array.buffer, - array.byteOffset, - array.byteLength, - ); - const value = this.type.getter(view, 0, options.littleEndian); + const value = this.type.getter(array, 0, options.littleEndian); return this.create(options, struct, value as never); }) .valueOrPromise(); @@ -110,9 +100,13 @@ export class BigIntFieldDefinition< export class BigIntFieldValue< TDefinition extends BigIntFieldDefinition, > extends StructFieldValue { - serialize(dataView: DataView, offset: number): void { + override serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { this.definition.type.setter( - dataView, + array, offset, this.value as never, this.options.littleEndian, diff --git a/libraries/struct/src/types/buffer/base.spec.ts b/libraries/struct/src/types/buffer/base.spec.ts index b2bc60d2..6d688aca 100644 --- a/libraries/struct/src/types/buffer/base.spec.ts +++ b/libraries/struct/src/types/buffer/base.spec.ts @@ -189,7 +189,7 @@ describe("Types", () => { const targetArray = new Uint8Array(size); const targetView = new DataView(targetArray.buffer); - fieldValue.serialize(targetView, 0); + fieldValue.serialize(targetView, targetArray, 0); expect(targetArray).toEqual(sourceArray); }); @@ -219,7 +219,7 @@ describe("Types", () => { const targetArray = new Uint8Array(size); const targetView = new DataView(targetArray.buffer); - fieldValue.serialize(targetView, 0); + fieldValue.serialize(targetView, targetArray, 0); expect(targetArray).toEqual(sourceArray); }); diff --git a/libraries/struct/src/types/buffer/base.ts b/libraries/struct/src/types/buffer/base.ts index 7dab8647..d5d1549e 100644 --- a/libraries/struct/src/types/buffer/base.ts +++ b/libraries/struct/src/types/buffer/base.ts @@ -186,15 +186,12 @@ export class BufferLikeFieldValue< this.array = undefined; } - serialize(dataView: DataView, offset: number): void { - if (!this.array) { - this.array = this.definition.type.toBuffer(this.value); - } - - new Uint8Array( - dataView.buffer, - dataView.byteOffset, - dataView.byteLength, - ).set(this.array, offset); + override serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { + this.array ??= this.definition.type.toBuffer(this.value); + array.set(this.array, offset); } } diff --git a/libraries/struct/src/types/buffer/variable-length.spec.ts b/libraries/struct/src/types/buffer/variable-length.spec.ts index 9131e67d..32ee5f9f 100644 --- a/libraries/struct/src/types/buffer/variable-length.spec.ts +++ b/libraries/struct/src/types/buffer/variable-length.spec.ts @@ -39,10 +39,12 @@ class MockLengthFieldValue extends StructFieldValue { void value; }); - serialize = jest.fn((dataView: DataView, offset: number): void => { - void dataView; - void offset; - }); + serialize = jest.fn( + (dataView: DataView, array: Uint8Array, offset: number): void => { + void dataView; + void offset; + }, + ); } describe("Types", () => { @@ -62,8 +64,13 @@ describe("Types", () => { override getSize = jest.fn(() => this.size); - serialize(dataView: DataView, offset: number): void { + serialize( + dataView: DataView, + array: Uint8Array, + offset: number, + ): void { void dataView; + void array; void offset; throw new Error("Method not implemented."); } @@ -181,11 +188,12 @@ describe("Types", () => { ); const dataView = 0 as any; + const array = 2 as any; const offset = 1 as any; mockOriginalFieldValue.value = 10; mockArrayBufferFieldValue.size = 0; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.get).toHaveReturnedWith(10); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); @@ -195,13 +203,14 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); mockOriginalFieldValue.set.mockClear(); mockOriginalFieldValue.serialize.mockClear(); mockArrayBufferFieldValue.size = 100; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.set).toHaveBeenCalledWith(100); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledTimes( @@ -209,6 +218,7 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); }); @@ -224,11 +234,12 @@ describe("Types", () => { ); const dataView = 0 as any; + const array = 2 as any; const offset = 1 as any; mockOriginalFieldValue.value = "10"; mockArrayBufferFieldValue.size = 0; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.get).toHaveReturnedWith("10"); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); @@ -238,13 +249,14 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); mockOriginalFieldValue.set.mockClear(); mockOriginalFieldValue.serialize.mockClear(); mockArrayBufferFieldValue.size = 100; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.set).toHaveBeenCalledWith("100"); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledTimes( @@ -252,6 +264,7 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); }); @@ -271,11 +284,12 @@ describe("Types", () => { radix; const dataView = 0 as any; + const array = 2 as any; const offset = 1 as any; mockOriginalFieldValue.value = "10"; mockArrayBufferFieldValue.size = 0; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.get).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.get).toHaveReturnedWith("10"); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); @@ -285,13 +299,14 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); mockOriginalFieldValue.set.mockClear(); mockOriginalFieldValue.serialize.mockClear(); mockArrayBufferFieldValue.size = 100; - lengthFieldValue.serialize(dataView, offset); + lengthFieldValue.serialize(dataView, array, offset); expect(mockOriginalFieldValue.set).toHaveBeenCalledTimes(1); expect(mockOriginalFieldValue.set).toHaveBeenCalledWith( (100).toString(radix), @@ -301,6 +316,7 @@ describe("Types", () => { ); expect(mockOriginalFieldValue.serialize).toHaveBeenCalledWith( dataView, + array, offset, ); }); diff --git a/libraries/struct/src/types/buffer/variable-length.ts b/libraries/struct/src/types/buffer/variable-length.ts index 9d646a69..85845720 100644 --- a/libraries/struct/src/types/buffer/variable-length.ts +++ b/libraries/struct/src/types/buffer/variable-length.ts @@ -180,8 +180,8 @@ export class VariableLengthBufferLikeFieldLengthValue extends StructFieldValue< // It will always be in sync with the buffer size } - serialize(dataView: DataView, offset: number) { + serialize(dataView: DataView, array: Uint8Array, offset: number) { this.originalField.set(this.get()); - this.originalField.serialize(dataView, offset); + this.originalField.serialize(dataView, array, offset); } } diff --git a/libraries/struct/src/types/number.spec.ts b/libraries/struct/src/types/number.spec.ts index 591ec165..95519c77 100644 --- a/libraries/struct/src/types/number.spec.ts +++ b/libraries/struct/src/types/number.spec.ts @@ -316,7 +316,7 @@ describe("Types", () => { const array = new Uint8Array(10); const dataView = new DataView(array.buffer); - value.serialize(dataView, 2); + value.serialize(dataView, array, 2); expect(Array.from(array)).toEqual([ 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, diff --git a/libraries/struct/src/types/number.ts b/libraries/struct/src/types/number.ts index 10575911..f1c6696a 100644 --- a/libraries/struct/src/types/number.ts +++ b/libraries/struct/src/types/number.ts @@ -1,3 +1,9 @@ +import { + getInt16, + getInt32, + getUint16, + getUint32, +} from "@yume-chan/no-data-view"; import type { AsyncExactReadable, ExactReadable, @@ -51,9 +57,7 @@ export namespace NumberFieldType { // PERF: Creating many `DataView`s over small buffers is 90% slower // than this. Even if the `DataView` is cached, `DataView#getUint16` // is still 1% slower than this. - const a = (array[1]! << 8) | array[0]!; - const b = (array[0]! << 8) | array[1]!; - return littleEndian ? a : b; + return getUint16(array, 0, littleEndian); }, serialize(dataView, offset, value, littleEndian) { dataView.setUint16(offset, value, littleEndian); @@ -64,8 +68,7 @@ export namespace NumberFieldType { signed: true, size: 2, deserialize(array, littleEndian) { - const value = Uint16.deserialize(array, littleEndian); - return (value << 16) >> 16; + return getInt16(array, 0, littleEndian); }, serialize(dataView, offset, value, littleEndian) { dataView.setInt16(offset, value, littleEndian); @@ -76,8 +79,7 @@ export namespace NumberFieldType { signed: false, size: 4, deserialize(array, littleEndian) { - const value = Int32.deserialize(array, littleEndian); - return value >>> 0; + return getUint32(array, 0, littleEndian); }, serialize(dataView, offset, value, littleEndian) { dataView.setUint32(offset, value, littleEndian); @@ -88,17 +90,7 @@ export namespace NumberFieldType { signed: true, size: 4, deserialize(array, littleEndian) { - const a = - (array[3]! << 24) | - (array[2]! << 16) | - (array[1]! << 8) | - array[0]!; - const b = - (array[0]! << 24) | - (array[1]! << 16) | - (array[2]! << 8) | - array[3]!; - return littleEndian ? a : b; + return getInt32(array, 0, littleEndian); }, serialize(dataView, offset, value, littleEndian) { dataView.setInt32(offset, value, littleEndian); @@ -162,7 +154,7 @@ export class NumberFieldDefinition< export class NumberFieldValue< TDefinition extends NumberFieldDefinition, > extends StructFieldValue { - serialize(dataView: DataView, offset: number): void { + serialize(dataView: DataView, array: Uint8Array, offset: number): void { this.definition.type.serialize( dataView, offset, diff --git a/libraries/struct/tsconfig.json b/libraries/struct/tsconfig.json index 85fc5a7a..04f9aba7 100644 --- a/libraries/struct/tsconfig.json +++ b/libraries/struct/tsconfig.json @@ -1,5 +1,8 @@ { "references": [ + { + "path": "../no-data-view/tsconfig.build.json" + }, { "path": "./tsconfig.test.json" }, diff --git a/rush.json b/rush.json index d77c21be..036d2f0c 100644 --- a/rush.json +++ b/rush.json @@ -437,6 +437,12 @@ "shouldPublish": true, "versionPolicyName": "adb" }, + { + "packageName": "@yume-chan/no-data-view", + "projectFolder": "libraries/no-data-view", + "shouldPublish": true, + "versionPolicyName": "adb" + }, { "packageName": "@yume-chan/scrcpy-decoder-tinyh264", "projectFolder": "libraries/scrcpy-decoder-tinyh264", diff --git a/toolchain/tsconfig/tsconfig.base.json b/toolchain/tsconfig/tsconfig.base.json index c780f6a7..9eb0b51a 100644 --- a/toolchain/tsconfig/tsconfig.base.json +++ b/toolchain/tsconfig/tsconfig.base.json @@ -16,7 +16,7 @@ "stripInternal": true, // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "noEmit": true, /* Do not emit outputs. */ - "importHelpers": true, // /* Import emit helpers from 'tslib'. */ + // "importHelpers": true, // /* Import emit helpers from 'tslib'. */ "skipLibCheck": true, // /* Skip type checking of all declaration files (*.d.ts). */ /* Strict Type-Checking Options */ "strict": true, // /* Enable all strict type-checking options. */