UNPKG

cs2-inspect-lib

Version:

Enhanced CS2 Inspect URL library with full protobuf support, validation, and error handling

430 lines 16.8 kB
"use strict"; /** * Enhanced protobuf writer with error handling and validation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ProtobufWriter = void 0; const types_1 = require("./types"); const errors_1 = require("./errors"); const validation_1 = require("./validation"); /** * Utility functions */ function floatToBytes(floatValue) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setFloat32(0, floatValue, false); // false for big-endian return view.getUint32(0, false); } function processRarity(rarityValue) { if (typeof rarityValue === 'number') { return rarityValue; } else if (typeof rarityValue === 'string') { const enumKey = rarityValue.toUpperCase(); if (enumKey in types_1.ItemRarity) { return types_1.ItemRarity[enumKey]; } return types_1.ItemRarity.STOCK; } return rarityValue; } /** * Enhanced protobuf writer with comprehensive error handling */ class ProtobufWriter { constructor(initialCapacity = 1024, config = {}) { this.pos = 0; this.config = { ...types_1.DEFAULT_CONFIG, ...config }; this.capacity = initialCapacity; this.buffer = new Uint8Array(this.capacity); } /** * Ensures buffer has enough capacity, growing if necessary */ ensureCapacity(needed) { if (this.pos + needed > this.capacity) { const newCapacity = Math.max(this.capacity * 2, this.pos + needed); const newBuffer = new Uint8Array(newCapacity); newBuffer.set(this.buffer.subarray(0, this.pos)); this.buffer = newBuffer; this.capacity = newCapacity; } } /** * Writes a varint with bounds checking */ writeVarint(value) { if (value < 0) { throw new errors_1.EncodingError('Cannot encode negative number as varint', { value }); } this.ensureCapacity(5); // Max 5 bytes for 32-bit varint while (value > 0x7F) { this.buffer[this.pos++] = (value & 0x7F) | 0x80; value >>>= 7; } this.buffer[this.pos++] = value; } /** * Writes a 64-bit varint */ writeVarint64(value) { const bigValue = typeof value === 'bigint' ? value : BigInt(value); if (bigValue < 0n) { throw new errors_1.EncodingError('Cannot encode negative number as varint64', { value: value.toString() }); } this.ensureCapacity(10); // Max 10 bytes for 64-bit varint let val = bigValue; while (val > 0x7fn) { this.buffer[this.pos++] = Number((val & 0x7fn) | 0x80n); val >>= 7n; } this.buffer[this.pos++] = Number(val); } /** * Writes a signed 32-bit integer using ZigZag encoding */ writeSInt32(value) { if (!Number.isInteger(value)) { throw new errors_1.EncodingError('SInt32 value must be an integer', { value }); } // ZigZag encoding for signed integers const encoded = (value << 1) ^ (value >> 31); this.writeVarint(encoded >>> 0); } /** * Writes a protobuf tag */ writeTag(fieldNumber, wireType) { if (fieldNumber < 1 || fieldNumber > 536870911) { // 2^29 - 1 throw new errors_1.EncodingError('Field number out of valid range', { fieldNumber, validRange: '1 to 536870911' }); } if (wireType < 0 || wireType > 5) { throw new errors_1.EncodingError('Invalid wire type', { wireType, validRange: '0 to 5' }); } this.writeVarint((fieldNumber << 3) | wireType); } /** * Writes a float value */ writeFloat(value) { if (!Number.isFinite(value)) { throw new errors_1.EncodingError('Float value must be finite', { value }); } this.ensureCapacity(4); const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setFloat32(0, value, true); // true for little-endian for (let i = 0; i < 4; i++) { this.buffer[this.pos++] = view.getUint8(i); } } /** * Writes a string with length prefix */ writeString(value) { if (typeof value !== 'string') { throw new errors_1.EncodingError('Value must be a string', { type: typeof value }); } if (value.length > this.config.maxCustomNameLength) { throw new errors_1.EncodingError(`String too long: ${value.length} > ${this.config.maxCustomNameLength}`, { length: value.length, maxLength: this.config.maxCustomNameLength }); } try { const encoder = new globalThis.TextEncoder(); const bytes = encoder.encode(value); this.writeVarint(bytes.length); this.ensureCapacity(bytes.length); this.buffer.set(bytes, this.pos); this.pos += bytes.length; } catch (error) { throw new errors_1.EncodingError('Failed to encode string', { string: value, originalError: error }); } } /** * Writes length-delimited bytes */ writeLengthDelimited(bytes) { if (!(bytes instanceof Uint8Array)) { throw new errors_1.EncodingError('Value must be Uint8Array', { type: typeof bytes }); } this.writeVarint(bytes.length); this.ensureCapacity(bytes.length); this.buffer.set(bytes, this.pos); this.pos += bytes.length; } /** * Returns the encoded bytes */ getBytes() { return this.buffer.subarray(0, this.pos); } /** * Gets current position */ getPosition() { return this.pos; } /** * Resets the writer for reuse */ reset() { this.pos = 0; } /** * Encodes a sticker to protobuf bytes */ static encodeSticker(sticker, config = {}) { if (config.validateInput) { const validation = validation_1.Validator.validateSticker(sticker); if (!validation.valid) { throw new errors_1.ValidationError(`Sticker validation failed: ${validation.errors.join(', ')}`, { errors: validation.errors, warnings: validation.warnings }); } } const writer = new ProtobufWriter(256, config); try { // Required fields writer.writeTag(1, 0); // slot writer.writeVarint(sticker.slot); writer.writeTag(2, 0); // sticker_id writer.writeVarint(sticker.sticker_id); // Optional fields if (typeof sticker.wear === 'number') { writer.writeTag(3, 5); // wear (float) writer.writeFloat(sticker.wear); } if (typeof sticker.scale === 'number') { writer.writeTag(4, 5); // scale (float) writer.writeFloat(sticker.scale); } if (typeof sticker.rotation === 'number') { writer.writeTag(5, 5); // rotation (float) writer.writeFloat(sticker.rotation); } if (typeof sticker.tint_id === 'number') { writer.writeTag(6, 0); // tint_id (varint) writer.writeVarint(sticker.tint_id); } if (typeof sticker.offset_x === 'number') { writer.writeTag(7, 5); // offset_x (float) writer.writeFloat(sticker.offset_x); } if (typeof sticker.offset_y === 'number') { writer.writeTag(8, 5); // offset_y (float) writer.writeFloat(sticker.offset_y); } if (typeof sticker.offset_z === 'number') { writer.writeTag(9, 5); // offset_z (float) writer.writeFloat(sticker.offset_z); } if (typeof sticker.pattern === 'number') { writer.writeTag(10, 0); // pattern (varint) writer.writeVarint(sticker.pattern); } if (typeof sticker.highlight_reel === 'number') { writer.writeTag(11, 0); // highlight_reel (varint) writer.writeVarint(sticker.highlight_reel); } return writer.getBytes(); } catch (error) { if (error instanceof errors_1.EncodingError || error instanceof errors_1.ValidationError) { throw error; } throw new errors_1.EncodingError('Failed to encode sticker', { sticker, originalError: error }); } } /** * Encodes an EconItem to protobuf bytes */ static encodeItemData(item, config = {}) { if (config.validateInput) { validation_1.Validator.assertValid(item); } const writer = new ProtobufWriter(2048, config); try { // Write fields in ascending order by field number // Field 1: accountid (optional) if (typeof item.accountid !== 'undefined') { writer.writeTag(1, 0); writer.writeVarint(item.accountid); } // Field 2: itemid (optional, uint64) if (typeof item.itemid !== 'undefined') { writer.writeTag(2, 0); writer.writeVarint64(item.itemid); } // Field 3: defindex (required) writer.writeTag(3, 0); writer.writeVarint(typeof item.defindex === 'number' ? item.defindex : item.defindex); // Field 4: paintindex (required) writer.writeTag(4, 0); writer.writeVarint(item.paintindex); // Field 5: rarity (optional) if (typeof item.rarity !== 'undefined') { writer.writeTag(5, 0); writer.writeVarint(processRarity(item.rarity)); } // Field 6: quality (optional) if (typeof item.quality !== 'undefined') { writer.writeTag(6, 0); writer.writeVarint(item.quality); } // Field 7: paintwear (required) writer.writeTag(7, 0); writer.writeVarint(floatToBytes(item.paintwear)); // Field 8: paintseed (required) writer.writeTag(8, 0); writer.writeVarint(item.paintseed); // Field 9: killeaterscoretype (optional) if (typeof item.killeaterscoretype !== 'undefined') { writer.writeTag(9, 0); writer.writeVarint(item.killeaterscoretype); } // Field 10: killeatervalue (optional) if (typeof item.killeatervalue !== 'undefined') { writer.writeTag(10, 0); writer.writeVarint(item.killeatervalue); } // Field 11: customname (optional) if (item.customname) { writer.writeTag(11, 2); writer.writeString(item.customname); } // Field 12: stickers (repeated) if (item.stickers && item.stickers.length > 0) { for (const sticker of item.stickers) { writer.writeTag(12, 2); const stickerBytes = this.encodeSticker(sticker, config); writer.writeLengthDelimited(stickerBytes); } } // Field 13: inventory (optional) if (typeof item.inventory !== 'undefined') { writer.writeTag(13, 0); writer.writeVarint(item.inventory); } // Field 14: origin (optional) if (typeof item.origin !== 'undefined') { writer.writeTag(14, 0); writer.writeVarint(item.origin); } // Field 15: questid (optional) if (typeof item.questid !== 'undefined') { writer.writeTag(15, 0); writer.writeVarint(item.questid); } // Field 16: dropreason (optional) if (typeof item.dropreason !== 'undefined') { writer.writeTag(16, 0); writer.writeVarint(item.dropreason); } // Field 17: musicindex (optional) if (typeof item.musicindex !== 'undefined') { writer.writeTag(17, 0); writer.writeVarint(item.musicindex); } // Field 18: entindex (optional, signed int32) if (typeof item.entindex !== 'undefined') { writer.writeTag(18, 0); writer.writeSInt32(item.entindex); } // Field 19: petindex (optional) if (typeof item.petindex !== 'undefined') { writer.writeTag(19, 0); writer.writeVarint(item.petindex); } // Field 20: keychains (repeated) if (item.keychains && item.keychains.length > 0) { for (const keychain of item.keychains) { writer.writeTag(20, 2); const keychainBytes = this.encodeSticker(keychain, config); writer.writeLengthDelimited(keychainBytes); } } // Field 21: style (optional) if (typeof item.style !== 'undefined') { writer.writeTag(21, 0); writer.writeVarint(item.style); } // Field 22: variations (repeated) if (item.variations && item.variations.length > 0) { for (const variation of item.variations) { writer.writeTag(22, 2); const variationBytes = this.encodeSticker(variation, config); writer.writeLengthDelimited(variationBytes); } } // Field 23: upgrade_level (optional) if (typeof item.upgrade_level !== 'undefined') { writer.writeTag(23, 0); writer.writeVarint(item.upgrade_level); } return writer.getBytes(); } catch (error) { if (error instanceof errors_1.EncodingError || error instanceof errors_1.ValidationError) { throw error; } throw new errors_1.EncodingError('Failed to encode item data', { item, originalError: error }); } } /** * CRC32 calculation for checksum */ static crc32(data) { let crc = -1; const table = new Int32Array(256); // Generate CRC table for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = ((c & 1) ? (-306674912 ^ (c >>> 1)) : (c >>> 1)); } table[i] = c; } // Calculate CRC for (let i = 0; i < data.length; i++) { crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xFF]; } return (crc ^ (-1)) >>> 0; } /** * Creates a complete inspect URL from an EconItem */ static createInspectUrl(item, config = {}) { const INSPECT_BASE = "steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20"; try { const protoData = this.encodeItemData(item, config); // Create buffer with null byte prefix and space for checksum const buffer = new Uint8Array(protoData.length + 5); buffer[0] = 0; // Null byte prefix buffer.set(protoData, 1); // Calculate checksum const crc = this.crc32(buffer.subarray(0, buffer.length - 4)); const xoredCrc = (crc & 0xFFFF) ^ (protoData.length * crc); // Add checksum (big-endian) const view = new DataView(buffer.buffer); view.setUint32(buffer.length - 4, xoredCrc, false); // Convert to hex string const hexString = Array.from(buffer) .map(b => b.toString(16).padStart(2, '0')) .join('') .toUpperCase(); const finalUrl = `${INSPECT_BASE}${hexString}`; // Validate final URL length if (finalUrl.length > config.maxUrlLength) { throw new errors_1.EncodingError(`Generated URL exceeds maximum length: ${finalUrl.length} > ${config.maxUrlLength}`, { urlLength: finalUrl.length, maxLength: config.maxUrlLength }); } return finalUrl; } catch (error) { if (error instanceof errors_1.EncodingError || error instanceof errors_1.ValidationError) { throw error; } throw new errors_1.EncodingError('Failed to create inspect URL', { item, originalError: error }); } } } exports.ProtobufWriter = ProtobufWriter; //# sourceMappingURL=protobuf-writer.js.map