export const ARUSNAVI_INTERNAL_SPECIFICATION_REF = "arusnavi.internal.protocol-sheet.gid-12.v1"; const HEADER2_LENGTH = 10; const HEADER_START = 0xff; const HEADER2_GPRS_VERSION = 0x23; const PACKAGE_START = 0x5b; const PACKAGE_END = 0x5d; const SERVER_COMMAND_START = 0x7b; const SERVER_COMMAND_END = 0x7d; const MIN_PACKAGE_NUMBER = 0x01; const MAX_PACKAGE_NUMBER = 0xfb; const PACKET_FIXED_LENGTH = 8; const MAX_PACKET_DATA_LENGTH = 32 * 1024; const MAX_PACKAGE_LENGTH = 64 * 1024; export const ARUSNAVI_B2_MODEL_PROFILE = deepFreeze({ schemaVersion: "nodedc.device-model-profile.v1", profileRef: "arusnavi.b2.internal.v1", vendor: "ARUSNAVI", model: "B2", deviceType: "tracker", protocol: "INTERNAL", monitoringServerSlots: 4, serverIdentity: { kind: "imei", source: "modem", trust: "claimed-not-ownership-proof", }, bootstrap: { operatorSurface: "ARUSNAVI_WEB_OR_LOCAL_CONFIGURATOR", platformCredentialRequired: false, preserveExistingRoutes: true, }, framing: { status: "verified-read-only", specificationRef: ARUSNAVI_INTERNAL_SPECIFICATION_REF, headerVersion: "HEADER2_GPRS_0x23", headerBytes: HEADER2_LENGTH, maxBufferedBytes: MAX_PACKAGE_LENGTH, }, acknowledgement: { header: "server-time-only", package: "package-number-only", }, commandTransport: { status: "disabled", exportedCommandBuilders: 0, }, routeCompatibility: { gelios: "parallel-preserved", automaticCommandFailover: false, }, }); export function tryParseB2Header2(input) { assertBuffer(input, "b2_header_buffer_required"); if (input.length < HEADER2_LENGTH) { return Object.freeze({ status: "incomplete", minimumBytes: HEADER2_LENGTH, }); } if (input[0] !== HEADER_START) { throw new TypeError("b2_header_start_invalid"); } if (input[1] !== HEADER2_GPRS_VERSION) { throw new TypeError("b2_header_version_unsupported"); } const identifier = input.readBigUInt64LE(2).toString(10); if (!/^\d{15}$/.test(identifier)) { throw new TypeError("b2_header_imei_invalid"); } return Object.freeze({ status: "complete", bytesConsumed: HEADER2_LENGTH, identifier: Object.freeze({ kind: "imei", value: identifier, trust: "claimed-not-ownership-proof", }), evidence: Object.freeze({ transport: "tcp", bytesObserved: HEADER2_LENGTH, framingStatus: "verified", specificationRef: ARUSNAVI_INTERNAL_SPECIFICATION_REF, }), }); } export function buildB2HeaderAcknowledgement(unixSeconds) { const timestamp = normalizeUInt32( unixSeconds, "b2_header_ack_timestamp_invalid", ); const commandData = Buffer.allocUnsafe(4); commandData.writeUInt32LE(timestamp); return Buffer.from([ SERVER_COMMAND_START, commandData.length, 0x00, checksum(commandData), ...commandData, SERVER_COMMAND_END, ]); } export function tryParseB2Package(input) { assertBuffer(input, "b2_package_buffer_required"); if (input.length === 0) { return Object.freeze({ status: "incomplete", minimumBytes: 1 }); } if (input[0] !== PACKAGE_START) { throw new TypeError("b2_package_start_invalid"); } if (input.length < 3) { return Object.freeze({ status: "incomplete", minimumBytes: 3 }); } const packageNumber = input[1]; if ( packageNumber < MIN_PACKAGE_NUMBER || packageNumber > MAX_PACKAGE_NUMBER ) { throw new TypeError("b2_package_number_invalid"); } let offset = 2; let packetCount = 0; while (true) { if (offset >= MAX_PACKAGE_LENGTH) { throw new TypeError("b2_package_length_exceeded"); } if (offset >= input.length) { return Object.freeze({ status: "incomplete", minimumBytes: offset + 1, }); } if (input[offset] === PACKAGE_END) { if (packetCount === 0) { throw new TypeError("b2_package_empty"); } return Object.freeze({ status: "complete", bytesConsumed: offset + 1, packageNumber, packetCount, }); } if (input.length - offset < 3) { return Object.freeze({ status: "incomplete", minimumBytes: offset + 3, }); } const dataLength = input.readUInt16LE(offset + 1); if (dataLength > MAX_PACKET_DATA_LENGTH) { throw new TypeError("b2_packet_data_length_exceeded"); } const packetLength = PACKET_FIXED_LENGTH + dataLength; const packetEnd = offset + packetLength; if (packetEnd + 1 > MAX_PACKAGE_LENGTH) { throw new TypeError("b2_package_length_exceeded"); } if (input.length < packetEnd) { return Object.freeze({ status: "incomplete", minimumBytes: packetEnd, }); } const expectedChecksum = checksum( input.subarray(offset + 3, packetEnd - 1), ); if (input[packetEnd - 1] !== expectedChecksum) { throw new TypeError("b2_packet_checksum_invalid"); } packetCount += 1; offset = packetEnd; } } export function buildB2PackageAcknowledgement(packageNumber) { const normalized = Number(packageNumber); if ( !Number.isSafeInteger(normalized) || normalized < MIN_PACKAGE_NUMBER || normalized > MAX_PACKAGE_NUMBER ) { throw new TypeError("b2_package_ack_number_invalid"); } return Buffer.from([ SERVER_COMMAND_START, 0x00, normalized, SERVER_COMMAND_END, ]); } export function assertB2ProfileInvariant(profile = ARUSNAVI_B2_MODEL_PROFILE) { if (profile.monitoringServerSlots !== 4) { throw new TypeError("b2_server_slot_count_invalid"); } if (profile.protocol !== "INTERNAL") { throw new TypeError("b2_protocol_invalid"); } if (profile.serverIdentity.kind !== "imei") { throw new TypeError("b2_identity_kind_invalid"); } if (profile.framing.status !== "verified-read-only") { throw new TypeError("b2_framing_must_be_verified"); } if ( profile.framing.specificationRef !== ARUSNAVI_INTERNAL_SPECIFICATION_REF ) { throw new TypeError("b2_framing_specification_invalid"); } if (profile.commandTransport.status !== "disabled") { throw new TypeError("b2_command_transport_must_be_disabled"); } if (profile.routeCompatibility.gelios !== "parallel-preserved") { throw new TypeError("b2_gelios_route_must_be_preserved"); } return true; } function checksum(input) { let value = 0; for (const byte of input) value = (value + byte) & 0xff; return value; } function normalizeUInt32(value, errorCode) { const normalized = Number(value); if ( !Number.isSafeInteger(normalized) || normalized < 0 || normalized > 0xffffffff ) { throw new TypeError(errorCode); } return normalized; } function assertBuffer(input, errorCode) { if (!Buffer.isBuffer(input)) { throw new TypeError(errorCode); } } function deepFreeze(value) { if (!value || typeof value !== "object" || Object.isFrozen(value)) { return value; } Object.values(value).forEach(deepFreeze); return Object.freeze(value); }