|
|
import { isUint8Array } from './parser/utils'; |
|
|
import type { EJSONOptions } from './extended_json'; |
|
|
import { BSONError } from './error'; |
|
|
import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants'; |
|
|
import { ByteUtils } from './utils/byte_utils'; |
|
|
import { BSONValue } from './bson_value'; |
|
|
|
|
|
|
|
|
export type BinarySequence = Uint8Array | number[]; |
|
|
|
|
|
|
|
|
export interface BinaryExtendedLegacy { |
|
|
$type: string; |
|
|
$binary: string; |
|
|
} |
|
|
|
|
|
|
|
|
export interface BinaryExtended { |
|
|
$binary: { |
|
|
subType: string; |
|
|
base64: string; |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class Binary extends BSONValue { |
|
|
get _bsontype(): 'Binary' { |
|
|
return 'Binary'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly BSON_BINARY_SUBTYPE_DEFAULT = 0; |
|
|
|
|
|
|
|
|
static readonly BUFFER_SIZE = 256; |
|
|
|
|
|
static readonly SUBTYPE_DEFAULT = 0; |
|
|
|
|
|
static readonly SUBTYPE_FUNCTION = 1; |
|
|
|
|
|
static readonly SUBTYPE_BYTE_ARRAY = 2; |
|
|
|
|
|
static readonly SUBTYPE_UUID_OLD = 3; |
|
|
|
|
|
static readonly SUBTYPE_UUID = 4; |
|
|
|
|
|
static readonly SUBTYPE_MD5 = 5; |
|
|
|
|
|
static readonly SUBTYPE_ENCRYPTED = 6; |
|
|
|
|
|
static readonly SUBTYPE_COLUMN = 7; |
|
|
|
|
|
static readonly SUBTYPE_USER_DEFINED = 128; |
|
|
|
|
|
buffer!: Uint8Array; |
|
|
sub_type!: number; |
|
|
position!: number; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(buffer?: string | BinarySequence, subType?: number) { |
|
|
super(); |
|
|
if ( |
|
|
!(buffer == null) && |
|
|
!(typeof buffer === 'string') && |
|
|
!ArrayBuffer.isView(buffer) && |
|
|
!(buffer instanceof ArrayBuffer) && |
|
|
!Array.isArray(buffer) |
|
|
) { |
|
|
throw new BSONError( |
|
|
'Binary can only be constructed from string, Buffer, TypedArray, or Array<number>' |
|
|
); |
|
|
} |
|
|
|
|
|
this.sub_type = subType ?? Binary.BSON_BINARY_SUBTYPE_DEFAULT; |
|
|
|
|
|
if (buffer == null) { |
|
|
|
|
|
this.buffer = ByteUtils.allocate(Binary.BUFFER_SIZE); |
|
|
this.position = 0; |
|
|
} else { |
|
|
if (typeof buffer === 'string') { |
|
|
|
|
|
this.buffer = ByteUtils.fromISO88591(buffer); |
|
|
} else if (Array.isArray(buffer)) { |
|
|
|
|
|
this.buffer = ByteUtils.fromNumberArray(buffer); |
|
|
} else { |
|
|
|
|
|
this.buffer = ByteUtils.toLocalBufferType(buffer); |
|
|
} |
|
|
|
|
|
this.position = this.buffer.byteLength; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
put(byteValue: string | number | Uint8Array | number[]): void { |
|
|
|
|
|
if (typeof byteValue === 'string' && byteValue.length !== 1) { |
|
|
throw new BSONError('only accepts single character String'); |
|
|
} else if (typeof byteValue !== 'number' && byteValue.length !== 1) |
|
|
throw new BSONError('only accepts single character Uint8Array or Array'); |
|
|
|
|
|
|
|
|
let decodedByte: number; |
|
|
if (typeof byteValue === 'string') { |
|
|
decodedByte = byteValue.charCodeAt(0); |
|
|
} else if (typeof byteValue === 'number') { |
|
|
decodedByte = byteValue; |
|
|
} else { |
|
|
decodedByte = byteValue[0]; |
|
|
} |
|
|
|
|
|
if (decodedByte < 0 || decodedByte > 255) { |
|
|
throw new BSONError('only accepts number in a valid unsigned byte range 0-255'); |
|
|
} |
|
|
|
|
|
if (this.buffer.byteLength > this.position) { |
|
|
this.buffer[this.position++] = decodedByte; |
|
|
} else { |
|
|
const newSpace = ByteUtils.allocate(Binary.BUFFER_SIZE + this.buffer.length); |
|
|
newSpace.set(this.buffer, 0); |
|
|
this.buffer = newSpace; |
|
|
this.buffer[this.position++] = decodedByte; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write(sequence: string | BinarySequence, offset: number): void { |
|
|
offset = typeof offset === 'number' ? offset : this.position; |
|
|
|
|
|
|
|
|
if (this.buffer.byteLength < offset + sequence.length) { |
|
|
const newSpace = ByteUtils.allocate(this.buffer.byteLength + sequence.length); |
|
|
newSpace.set(this.buffer, 0); |
|
|
|
|
|
|
|
|
this.buffer = newSpace; |
|
|
} |
|
|
|
|
|
if (ArrayBuffer.isView(sequence)) { |
|
|
this.buffer.set(ByteUtils.toLocalBufferType(sequence), offset); |
|
|
this.position = |
|
|
offset + sequence.byteLength > this.position ? offset + sequence.length : this.position; |
|
|
} else if (typeof sequence === 'string') { |
|
|
const bytes = ByteUtils.fromISO88591(sequence); |
|
|
this.buffer.set(bytes, offset); |
|
|
this.position = |
|
|
offset + sequence.length > this.position ? offset + sequence.length : this.position; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
read(position: number, length: number): BinarySequence { |
|
|
length = length && length > 0 ? length : this.position; |
|
|
|
|
|
|
|
|
return this.buffer.slice(position, position + length); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
value(asRaw?: boolean): string | BinarySequence { |
|
|
asRaw = !!asRaw; |
|
|
|
|
|
|
|
|
if (asRaw && this.buffer.length === this.position) { |
|
|
return this.buffer; |
|
|
} |
|
|
|
|
|
|
|
|
if (asRaw) { |
|
|
return this.buffer.slice(0, this.position); |
|
|
} |
|
|
|
|
|
return ByteUtils.toISO88591(this.buffer.subarray(0, this.position)); |
|
|
} |
|
|
|
|
|
|
|
|
length(): number { |
|
|
return this.position; |
|
|
} |
|
|
|
|
|
toJSON(): string { |
|
|
return ByteUtils.toBase64(this.buffer); |
|
|
} |
|
|
|
|
|
toString(encoding?: 'hex' | 'base64' | 'utf8' | 'utf-8'): string { |
|
|
if (encoding === 'hex') return ByteUtils.toHex(this.buffer); |
|
|
if (encoding === 'base64') return ByteUtils.toBase64(this.buffer); |
|
|
if (encoding === 'utf8' || encoding === 'utf-8') |
|
|
return ByteUtils.toUTF8(this.buffer, 0, this.buffer.byteLength); |
|
|
return ByteUtils.toUTF8(this.buffer, 0, this.buffer.byteLength); |
|
|
} |
|
|
|
|
|
|
|
|
toExtendedJSON(options?: EJSONOptions): BinaryExtendedLegacy | BinaryExtended { |
|
|
options = options || {}; |
|
|
const base64String = ByteUtils.toBase64(this.buffer); |
|
|
|
|
|
const subType = Number(this.sub_type).toString(16); |
|
|
if (options.legacy) { |
|
|
return { |
|
|
$binary: base64String, |
|
|
$type: subType.length === 1 ? '0' + subType : subType |
|
|
}; |
|
|
} |
|
|
return { |
|
|
$binary: { |
|
|
base64: base64String, |
|
|
subType: subType.length === 1 ? '0' + subType : subType |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
toUUID(): UUID { |
|
|
if (this.sub_type === Binary.SUBTYPE_UUID) { |
|
|
return new UUID(this.buffer.slice(0, this.position)); |
|
|
} |
|
|
|
|
|
throw new BSONError( |
|
|
`Binary sub_type "${this.sub_type}" is not supported for converting to UUID. Only "${Binary.SUBTYPE_UUID}" is currently supported.` |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
static createFromHexString(hex: string, subType?: number): Binary { |
|
|
return new Binary(ByteUtils.fromHex(hex), subType); |
|
|
} |
|
|
|
|
|
|
|
|
static createFromBase64(base64: string, subType?: number): Binary { |
|
|
return new Binary(ByteUtils.fromBase64(base64), subType); |
|
|
} |
|
|
|
|
|
|
|
|
static fromExtendedJSON( |
|
|
doc: BinaryExtendedLegacy | BinaryExtended | UUIDExtended, |
|
|
options?: EJSONOptions |
|
|
): Binary { |
|
|
options = options || {}; |
|
|
let data: Uint8Array | undefined; |
|
|
let type; |
|
|
if ('$binary' in doc) { |
|
|
if (options.legacy && typeof doc.$binary === 'string' && '$type' in doc) { |
|
|
type = doc.$type ? parseInt(doc.$type, 16) : 0; |
|
|
data = ByteUtils.fromBase64(doc.$binary); |
|
|
} else { |
|
|
if (typeof doc.$binary !== 'string') { |
|
|
type = doc.$binary.subType ? parseInt(doc.$binary.subType, 16) : 0; |
|
|
data = ByteUtils.fromBase64(doc.$binary.base64); |
|
|
} |
|
|
} |
|
|
} else if ('$uuid' in doc) { |
|
|
type = 4; |
|
|
data = UUID.bytesFromString(doc.$uuid); |
|
|
} |
|
|
if (!data) { |
|
|
throw new BSONError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`); |
|
|
} |
|
|
return type === BSON_BINARY_SUBTYPE_UUID_NEW ? new UUID(data) : new Binary(data, type); |
|
|
} |
|
|
|
|
|
|
|
|
[Symbol.for('nodejs.util.inspect.custom')](): string { |
|
|
return this.inspect(); |
|
|
} |
|
|
|
|
|
inspect(): string { |
|
|
const base64 = ByteUtils.toBase64(this.buffer.subarray(0, this.position)); |
|
|
return `Binary.createFromBase64("${base64}", ${this.sub_type})`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export type UUIDExtended = { |
|
|
$uuid: string; |
|
|
}; |
|
|
|
|
|
const UUID_BYTE_LENGTH = 16; |
|
|
const UUID_WITHOUT_DASHES = /^[0-9A-F]{32}$/i; |
|
|
const UUID_WITH_DASHES = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class UUID extends Binary { |
|
|
|
|
|
static cacheHexString = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(input?: string | Uint8Array | UUID) { |
|
|
let bytes: Uint8Array; |
|
|
if (input == null) { |
|
|
bytes = UUID.generate(); |
|
|
} else if (input instanceof UUID) { |
|
|
bytes = ByteUtils.toLocalBufferType(new Uint8Array(input.buffer)); |
|
|
} else if (ArrayBuffer.isView(input) && input.byteLength === UUID_BYTE_LENGTH) { |
|
|
bytes = ByteUtils.toLocalBufferType(input); |
|
|
} else if (typeof input === 'string') { |
|
|
bytes = UUID.bytesFromString(input); |
|
|
} else { |
|
|
throw new BSONError( |
|
|
'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).' |
|
|
); |
|
|
} |
|
|
super(bytes, BSON_BINARY_SUBTYPE_UUID_NEW); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get id(): Uint8Array { |
|
|
return this.buffer; |
|
|
} |
|
|
|
|
|
set id(value: Uint8Array) { |
|
|
this.buffer = value; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toHexString(includeDashes = true): string { |
|
|
if (includeDashes) { |
|
|
return [ |
|
|
ByteUtils.toHex(this.buffer.subarray(0, 4)), |
|
|
ByteUtils.toHex(this.buffer.subarray(4, 6)), |
|
|
ByteUtils.toHex(this.buffer.subarray(6, 8)), |
|
|
ByteUtils.toHex(this.buffer.subarray(8, 10)), |
|
|
ByteUtils.toHex(this.buffer.subarray(10, 16)) |
|
|
].join('-'); |
|
|
} |
|
|
return ByteUtils.toHex(this.buffer); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toString(encoding?: 'hex' | 'base64'): string { |
|
|
if (encoding === 'hex') return ByteUtils.toHex(this.id); |
|
|
if (encoding === 'base64') return ByteUtils.toBase64(this.id); |
|
|
return this.toHexString(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toJSON(): string { |
|
|
return this.toHexString(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
equals(otherId: string | Uint8Array | UUID): boolean { |
|
|
if (!otherId) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
if (otherId instanceof UUID) { |
|
|
return ByteUtils.equals(otherId.id, this.id); |
|
|
} |
|
|
|
|
|
try { |
|
|
return ByteUtils.equals(new UUID(otherId).id, this.id); |
|
|
} catch { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toBinary(): Binary { |
|
|
return new Binary(this.id, Binary.SUBTYPE_UUID); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static generate(): Uint8Array { |
|
|
const bytes = ByteUtils.randomBytes(UUID_BYTE_LENGTH); |
|
|
|
|
|
|
|
|
|
|
|
bytes[6] = (bytes[6] & 0x0f) | 0x40; |
|
|
bytes[8] = (bytes[8] & 0x3f) | 0x80; |
|
|
|
|
|
return bytes; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static isValid(input: string | Uint8Array | UUID | Binary): boolean { |
|
|
if (!input) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
if (typeof input === 'string') { |
|
|
return UUID.isValidUUIDString(input); |
|
|
} |
|
|
|
|
|
if (isUint8Array(input)) { |
|
|
return input.byteLength === UUID_BYTE_LENGTH; |
|
|
} |
|
|
|
|
|
return ( |
|
|
input._bsontype === 'Binary' && |
|
|
input.sub_type === this.SUBTYPE_UUID && |
|
|
input.buffer.byteLength === 16 |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static override createFromHexString(hexString: string): UUID { |
|
|
const buffer = UUID.bytesFromString(hexString); |
|
|
return new UUID(buffer); |
|
|
} |
|
|
|
|
|
|
|
|
static override createFromBase64(base64: string): UUID { |
|
|
return new UUID(ByteUtils.fromBase64(base64)); |
|
|
} |
|
|
|
|
|
|
|
|
static bytesFromString(representation: string) { |
|
|
if (!UUID.isValidUUIDString(representation)) { |
|
|
throw new BSONError( |
|
|
'UUID string representation must be 32 hex digits or canonical hyphenated representation' |
|
|
); |
|
|
} |
|
|
return ByteUtils.fromHex(representation.replace(/-/g, '')); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static isValidUUIDString(representation: string) { |
|
|
return UUID_WITHOUT_DASHES.test(representation) || UUID_WITH_DASHES.test(representation); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Symbol.for('nodejs.util.inspect.custom')](): string { |
|
|
return this.inspect(); |
|
|
} |
|
|
|
|
|
inspect(): string { |
|
|
return `new UUID("${this.toHexString()}")`; |
|
|
} |
|
|
} |
|
|
|