node: Replace the NPM 'uuid' package with a Rust-backed implementation

This commit is contained in:
Jordan Rose
2026-03-31 10:48:06 -07:00
parent c077c48393
commit f645178020
17 changed files with 115 additions and 54 deletions

5
Cargo.lock generated
View File

@@ -2886,14 +2886,19 @@ version = "0.91.0"
dependencies = [
"futures",
"libsignal-bridge",
"libsignal-bridge-macros",
"libsignal-bridge-testing",
"libsignal-protocol",
"linkme",
"log",
"log-panics",
"minidump",
"minidump-processor",
"minidump-unwind",
"neon",
"paste",
"rand 0.9.2",
"uuid",
]
[[package]]

24
node/package-lock.json generated
View File

@@ -11,8 +11,7 @@
"license": "AGPL-3.0-only",
"dependencies": {
"node-gyp-build": "^4.8.0",
"type-fest": "^4.26.0",
"uuid": "^11"
"type-fest": "^4.26.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
@@ -874,6 +873,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz",
"integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.0",
"@typescript-eslint/types": "8.44.0",
@@ -1071,6 +1071,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1473,6 +1474,7 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz",
"integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==",
"dev": true,
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2068,6 +2070,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2227,6 +2230,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4397,6 +4401,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -5120,6 +5125,7 @@
"resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
"integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
"dev": true,
"peer": true,
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "^13.0.5",
@@ -5543,6 +5549,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5726,6 +5733,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5834,18 +5842,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -34,8 +34,7 @@
},
"dependencies": {
"node-gyp-build": "^4.8.0",
"type-fest": "^4.26.0",
"uuid": "^11"
"type-fest": "^4.26.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",

View File

@@ -5,9 +5,8 @@
import * as Native from './Native.js';
import * as uuid from 'uuid';
import { Buffer } from 'node:buffer';
import { parseUuid } from './uuid.js';
import * as uuid from './uuid.js';
export enum ServiceIdKind {
Aci = 0,
@@ -116,7 +115,7 @@ export abstract class ServiceId extends Object {
}
getRawUuid(): string {
return uuid.stringify(this.serviceIdFixedWidthBinary, 1);
return uuid.stringify(this.getRawUuidBytes());
}
getRawUuidBytes(): Uint8Array<ArrayBuffer> {
@@ -158,7 +157,7 @@ export class Aci extends ServiceId {
private readonly __type?: never;
static fromUuid(uuidString: string): Aci {
return this.fromUuidBytes(parseUuid(uuidString));
return this.fromUuidBytes(uuid.parse(uuidString));
}
static fromUuidBytes(uuidBytes: ArrayLike<number>): Aci {
@@ -170,7 +169,7 @@ export class Pni extends ServiceId {
private readonly __type?: never;
static fromUuid(uuidString: string): Pni {
return this.fromUuidBytes(parseUuid(uuidString));
return this.fromUuidBytes(uuid.parse(uuidString));
}
static fromUuidBytes(uuidBytes: ArrayLike<number>): Pni {

View File

@@ -148,6 +148,9 @@ type NativeFunctions = {
initLogger: (maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void) => void
SealedSenderMultiRecipientMessage_Parse: (buffer: Uint8Array<ArrayBuffer>) => SealedSenderMultiRecipientMessage;
MinidumpToJSONString: (buffer: Uint8Array<ArrayBuffer>) => string;
uuid_to_string: (uuid: Uuid) => string;
uuid_from_string: (string: string) => Uuid | null;
uuid_new_v4: () => Uuid;
Aes256GcmSiv_New: (key: Uint8Array<ArrayBuffer>) => Aes256GcmSiv;
Aes256GcmSiv_Encrypt: (aesGcmSivObj: Wrapper<Aes256GcmSiv>, ptext: Uint8Array<ArrayBuffer>, nonce: Uint8Array<ArrayBuffer>, associatedData: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
Aes256GcmSiv_Decrypt: (aesGcmSiv: Wrapper<Aes256GcmSiv>, ctext: Uint8Array<ArrayBuffer>, nonce: Uint8Array<ArrayBuffer>, associatedData: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
@@ -703,6 +706,9 @@ const { registerErrors,
initLogger,
SealedSenderMultiRecipientMessage_Parse,
MinidumpToJSONString,
uuid_to_string,
uuid_from_string,
uuid_new_v4,
Aes256GcmSiv_New,
Aes256GcmSiv_Encrypt,
Aes256GcmSiv_Decrypt,
@@ -1260,6 +1266,9 @@ export { registerErrors,
initLogger,
SealedSenderMultiRecipientMessage_Parse,
MinidumpToJSONString,
uuid_to_string,
uuid_from_string,
uuid_new_v4,
Aes256GcmSiv_New,
Aes256GcmSiv_Encrypt,
Aes256GcmSiv_Decrypt,

View File

@@ -5,8 +5,6 @@
import { Buffer } from 'node:buffer';
import * as uuid from 'uuid';
import * as Errors from './Errors.js';
export * from './Errors.js';
@@ -20,8 +18,7 @@ import {
SignedPreKeyRecord,
} from './ProtocolTypes.js';
export * from './ProtocolTypes.js';
import { parseUuid, Uuid } from './uuid.js';
export * from './uuid.js';
import * as uuid from './uuid.js';
export * as usernames from './usernames.js';
@@ -36,6 +33,8 @@ import * as Native from './Native.js';
Native.registerErrors(Errors);
export type Uuid = uuid.Uuid;
// These enums must be kept in sync with their Rust counterparts.
export enum CiphertextMessageType {
@@ -742,7 +741,7 @@ export class SenderKeyDistributionMessage {
): Promise<SenderKeyDistributionMessage> {
const handle = await Native.SenderKeyDistributionMessage_Create(
sender,
parseUuid(distributionId),
uuid.parse(distributionId),
bridgeSenderKeyStore(store)
);
return new SenderKeyDistributionMessage(handle);
@@ -759,7 +758,7 @@ export class SenderKeyDistributionMessage {
return new SenderKeyDistributionMessage(
Native.SenderKeyDistributionMessage_New(
messageVersion,
parseUuid(distributionId),
uuid.parse(distributionId),
chainId,
iteration,
chainKey,
@@ -829,7 +828,7 @@ export class SenderKeyMessage {
return new SenderKeyMessage(
Native.SenderKeyMessage_New(
messageVersion,
parseUuid(distributionId),
uuid.parse(distributionId),
chainId,
iteration,
ciphertext,
@@ -1015,7 +1014,7 @@ export async function groupEncrypt(
return CiphertextMessage._fromNativeHandle(
await Native.GroupCipher_EncryptMessage(
sender,
parseUuid(distributionId),
uuid.parse(distributionId),
message,
bridgeSenderKeyStore(store)
)

View File

@@ -4,11 +4,10 @@
//
import { Buffer } from 'node:buffer';
import * as uuid from 'uuid';
import * as Native from '../../Native.js';
import { Aci } from '../../Address.js';
import { parseUuid, Uuid } from '../../uuid.js';
import * as uuid from '../../uuid.js';
import { RequestOptions, UnauthenticatedChatConnection } from '../Chat.js';
// For documentation
@@ -23,6 +22,8 @@ declare module '../Chat' {
interface UnauthenticatedChatConnection extends UnauthUsernamesService {}
}
type Uuid = uuid.Uuid;
export interface UnauthUsernamesService {
/**
* Looks up a username hash on the service, like that computed by {@link usernames.hash}.
@@ -94,7 +95,7 @@ UnauthenticatedChatConnection.prototype.lookUpUsernameLink = async function (
Native.UnauthenticatedChatConnection_look_up_username_link(
this._asyncContext,
this._chatService,
parseUuid(linkUuid),
uuid.parse(linkUuid),
entropy
)
);

View File

@@ -4,13 +4,13 @@
//
import { assert } from 'chai';
import * as uuid from 'uuid';
import { Buffer } from 'node:buffer';
import * as AccountKeys from '../AccountKeys.js';
import * as util from './util.js';
import { Aci } from '../Address.js';
import { assertArrayNotEquals } from './util.js';
import * as uuid from '../uuid.js';
util.initLogger();
@@ -67,7 +67,7 @@ describe('BackupKey', () => {
const pool = AccountKeys.AccountEntropyPool.generate();
const backupKey = AccountKeys.AccountEntropyPool.deriveBackupKey(pool);
const randomKey = AccountKeys.BackupKey.generateRandom();
const otherAci = Aci.fromUuid(uuid.v4());
const otherAci = Aci.fromUuidBytes(uuid.v4());
const backupId = backupKey.deriveBackupId(aci);
assert.equal(16, backupId.length);

View File

@@ -7,7 +7,7 @@ import { assert, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import * as Native from '../Native.js';
import { BridgedStringMap } from '../internal.js';
import { parseUuid } from '../uuid.js';
import * as uuid from '../uuid.js';
use(chaiAsPromised);
@@ -195,7 +195,7 @@ describe('bridge_fn', () => {
const present = Native.TESTING_ConvertOptionalUuid(true);
assert.deepEqual(
present,
parseUuid('abababab-1212-8989-baba-565656565656')
uuid.parse('abababab-1212-8989-baba-565656565656')
);
const absent = Native.TESTING_ConvertOptionalUuid(false);

View File

@@ -6,7 +6,6 @@
import { assert, config, expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { Buffer } from 'node:buffer';
import * as uuid from 'uuid';
import * as Native from '../../Native.js';
import * as util from '../util.js';
@@ -14,6 +13,7 @@ import { TokioAsyncContext, UnauthUsernamesService } from '../../net.js';
import { connectUnauth } from './ServiceTestUtils.js';
import { ErrorCode, LibSignalErrorBase } from '../../Errors.js';
import { Aci } from '../../Address.js';
import * as uuid from '../../uuid.js';
use(chaiAsPromised);

View File

@@ -8,9 +8,8 @@ import * as util from '../util.js';
import { assert, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import * as uuid from 'uuid';
import { Buffer } from 'node:buffer';
import { parseUuid } from '../../uuid.js';
import * as uuid from '../../uuid.js';
use(chaiAsPromised);
util.initLogger();
@@ -23,8 +22,8 @@ describe('ProtocolAddress', () => {
});
it('can round-trip ServiceIds', () => {
const newUuid = uuid.v4();
const aci = SignalClient.Aci.fromUuid(newUuid);
const pni = SignalClient.Pni.fromUuid(newUuid);
const aci = SignalClient.Aci.fromUuidBytes(newUuid);
const pni = SignalClient.Pni.fromUuidBytes(newUuid);
const aciAddr = SignalClient.ProtocolAddress.new(aci, 1);
const pniAddr = SignalClient.ProtocolAddress.new(pni, 1);
@@ -48,14 +47,14 @@ describe('ServiceId', () => {
const aci = SignalClient.Aci.fromUuid(testingUuid);
assert.instanceOf(aci, SignalClient.Aci);
assert.isTrue(
aci.isEqual(SignalClient.Aci.fromUuidBytes(parseUuid(testingUuid)))
aci.isEqual(SignalClient.Aci.fromUuidBytes(uuid.parse(testingUuid)))
);
assert.isFalse(aci.isEqual(SignalClient.Pni.fromUuid(testingUuid)));
assert.deepEqual(testingUuid, aci.getRawUuid());
assert.deepEqual(parseUuid(testingUuid), aci.getRawUuidBytes());
assert.deepEqual(uuid.parse(testingUuid), aci.getRawUuidBytes());
assert.deepEqual(testingUuid, aci.getServiceIdString());
assert.deepEqual(parseUuid(testingUuid), aci.getServiceIdBinary());
assert.deepEqual(uuid.parse(testingUuid), aci.getServiceIdBinary());
assert.deepEqual(`<ACI:${testingUuid}>`, `${aci}`);
{
@@ -86,12 +85,12 @@ describe('ServiceId', () => {
const pni = SignalClient.Pni.fromUuid(testingUuid);
assert.instanceOf(pni, SignalClient.Pni);
assert.isTrue(
pni.isEqual(SignalClient.Pni.fromUuidBytes(parseUuid(testingUuid)))
pni.isEqual(SignalClient.Pni.fromUuidBytes(uuid.parse(testingUuid)))
);
assert.isFalse(pni.isEqual(SignalClient.Aci.fromUuid(testingUuid)));
assert.deepEqual(testingUuid, pni.getRawUuid());
assert.deepEqual(parseUuid(testingUuid), pni.getRawUuidBytes());
assert.deepEqual(uuid.parse(testingUuid), pni.getRawUuidBytes());
assert.deepEqual(`PNI:${testingUuid}`, pni.getServiceIdString());
assert.deepEqual(
Buffer.concat([Buffer.of(0x01), pni.getRawUuidBytes()]),

View File

@@ -3,11 +3,29 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import * as uuid from 'uuid';
/**
* Shim for the `uuid` package on NPM, backed by Rust's `uuid` crate instead.
* @module uuid
*/
import * as Native from './Native.js';
export type Uuid = string;
export function parseUuid(input: string): Uint8Array<ArrayBuffer> {
// @ts-expect-error See https://github.com/uuidjs/uuid/pull/927
return uuid.parse(input);
export const NIL = '00000000-0000-0000-0000-000000000000';
export function stringify(input: Uint8Array<ArrayBuffer>): string {
return Native.uuid_to_string(input);
}
export function parse(input: string): Uint8Array<ArrayBuffer> {
const result = Native.uuid_from_string(input);
if (!result) {
throw new TypeError(`invalid UUID: '${input}'`);
}
return result;
}
export function v4(): Uint8Array<ArrayBuffer> {
return Native.uuid_new_v4();
}

View File

@@ -10,7 +10,9 @@ import BackupAuthCredentialRequest from './BackupAuthCredentialRequest.js';
import BackupAuthCredentialResponse from './BackupAuthCredentialResponse.js';
import BackupAuthCredential from './BackupAuthCredential.js';
import GenericServerPublicParams from '../GenericServerPublicParams.js';
import { parseUuid, type Uuid } from '../../index.js';
import * as uuid from '../../uuid.js';
type Uuid = uuid.Uuid;
export default class BackupAuthCredentialRequestContext extends ByteArray {
private readonly __type?: never;
@@ -27,7 +29,7 @@ export default class BackupAuthCredentialRequestContext extends ByteArray {
aci: Uuid
): BackupAuthCredentialRequestContext {
return new BackupAuthCredentialRequestContext(
Native.BackupAuthCredentialRequestContext_New(backupKey, parseUuid(aci))
Native.BackupAuthCredentialRequestContext_New(backupKey, uuid.parse(aci))
);
}

View File

@@ -17,15 +17,25 @@ workspace = true
name = "signal_node"
crate-type = ["cdylib"]
[features]
# Here for bridge_fn uniformity
node = []
default = ["node"]
[dependencies]
libsignal-bridge = { workspace = true, features = ["node", "signal-media"] }
libsignal-bridge-macros = { workspace = true }
libsignal-bridge-testing = { workspace = true, features = ["node", "signal-media"] }
libsignal-protocol = { workspace = true }
futures = { workspace = true }
linkme = { workspace = true }
log = { workspace = true }
log-panics = { workspace = true, features = ["with-backtrace"] }
minidump = { workspace = true }
minidump-processor = { workspace = true }
minidump-unwind = { workspace = true }
neon = { workspace = true, features = ["napi-6"] }
paste = { workspace = true }
rand = { workspace = true }
uuid = { workspace = true }

View File

@@ -7,6 +7,9 @@
use futures::executor;
use libsignal_bridge::node::{AssumedImmutableBuffer, ResultTypeInfo, SignalNodeError};
use libsignal_bridge::node_register;
use libsignal_bridge::support::*;
use libsignal_bridge_macros::bridge_fn;
use libsignal_protocol::SealedSenderV2SentMessage;
use minidump::Minidump;
use minidump_processor::ProcessorOptions;
@@ -14,6 +17,8 @@ use minidump_unwind::Symbolizer;
use minidump_unwind::symbols::string_symbol_supplier;
use neon::prelude::*;
use neon::types::buffer::TypedArray;
use rand::TryRngCore;
use uuid::Uuid;
mod logging;
@@ -162,3 +167,22 @@ fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
Ok(cx.string(std::str::from_utf8(&json).expect("Failed to convert JSON to utf8")))
}
#[bridge_fn(ffi = false, jni = false)]
fn uuid_to_string(uuid: Uuid) -> String {
uuid.as_hyphenated().to_string()
}
#[bridge_fn(ffi = false, jni = false)]
fn uuid_from_string(string: String) -> Option<Uuid> {
Uuid::try_parse(&string).ok()
}
#[bridge_fn(ffi = false, jni = false)]
fn uuid_new_v4() -> Uuid {
let mut bytes = [0; 16];
rand::rngs::OsRng
.try_fill_bytes(&mut bytes)
.expect("system RNG should always be available");
uuid::Builder::from_random_bytes(bytes).into_uuid()
}

View File

@@ -51,4 +51,4 @@ pub mod usernames;
#[cfg(feature = "signal-media")]
pub mod media;
pub(crate) mod support;
pub mod support;

View File

@@ -4,7 +4,7 @@
//
use libsignal_bridge_macros::bridge_fn;
pub(crate) use libsignal_bridge_types::support::*;
pub use libsignal_bridge_types::support::*;
use crate::*;