cs2-inspect-lib
Version:
Enhanced CS2 Inspect URL library with full protobuf support, validation, and error handling
441 lines • 19.5 kB
JavaScript
"use strict";
/**
* Enhanced protobuf reader with error handling and validation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtobufReader = void 0;
const types_1 = require("./types");
const errors_1 = require("./errors");
const validation_1 = require("./validation");
/**
* Utility functions
*/
function hexToBytes(hexStr) {
validation_1.Validator.assertValidHexData(hexStr);
const bytes = new Uint8Array(hexStr.length / 2);
for (let i = 0; i < bytes.length; i++) {
const hex = hexStr.substr(i * 2, 2);
const byte = parseInt(hex, 16);
if (isNaN(byte)) {
throw new errors_1.DecodingError(`Invalid hex byte: ${hex}`, { position: i * 2, hex });
}
bytes[i] = byte;
}
return bytes;
}
function bytesToFloat(intValue) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, intValue, false); // false for big-endian
return view.getFloat32(0, false);
}
/**
* Enhanced protobuf reader with comprehensive error handling
*/
class ProtobufReader {
constructor(buffer, config = {}) {
this.buffer = buffer;
this.pos = 0;
this.config = { ...types_1.DEFAULT_CONFIG, ...config };
this.view = new DataView(buffer.buffer);
if (buffer.length === 0) {
throw new errors_1.DecodingError('Buffer cannot be empty');
}
if (buffer.length > 10 * 1024 * 1024) { // 10MB limit
throw new errors_1.DecodingError('Buffer too large', { size: buffer.length, maxSize: 10 * 1024 * 1024 });
}
}
/**
* Safely reads a varint with bounds checking
*/
readVarint() {
if (this.pos >= this.buffer.length) {
throw new errors_1.DecodingError('Unexpected end of buffer while reading varint', { position: this.pos, bufferLength: this.buffer.length });
}
let result = 0;
let shift = 0;
let bytesRead = 0;
while (this.pos < this.buffer.length && bytesRead < 5) { // Max 5 bytes for 32-bit
const byte = this.buffer[this.pos++];
result |= (byte & 0x7F) << shift;
bytesRead++;
if ((byte & 0x80) === 0) {
return result >>> 0;
}
shift += 7;
}
throw new errors_1.DecodingError('Invalid varint encoding - too many bytes or unexpected end', { position: this.pos, bytesRead });
}
/**
* Safely reads a 64-bit varint
*/
readVarint64() {
if (this.pos >= this.buffer.length) {
throw new errors_1.DecodingError('Unexpected end of buffer while reading varint64', { position: this.pos, bufferLength: this.buffer.length });
}
let result = 0n;
let shift = 0n;
let bytesRead = 0;
while (this.pos < this.buffer.length && bytesRead < 10) { // Max 10 bytes for 64-bit
const byte = BigInt(this.buffer[this.pos++]);
result |= (byte & 0x7fn) << shift;
bytesRead++;
if ((byte & 0x80n) === 0n) {
return result;
}
shift += 7n;
}
throw new errors_1.DecodingError('Invalid varint64 encoding - too many bytes or unexpected end', { position: this.pos, bytesRead });
}
/**
* Safely reads a signed 32-bit integer (ZigZag decoded)
*/
readSInt32() {
const encoded = this.readVarint();
return (encoded >>> 1) ^ (-(encoded & 1));
}
/**
* Safely reads a float value
*/
readFloat() {
if (this.pos + 4 > this.buffer.length) {
throw new errors_1.DecodingError('Buffer underrun while reading float', { position: this.pos, needed: 4, available: this.buffer.length - this.pos });
}
const value = this.view.getFloat32(this.pos, true);
this.pos += 4;
return value;
}
/**
* Safely reads a string with length validation
*/
readString() {
const length = this.readVarint();
if (length > this.config.maxCustomNameLength) {
throw new errors_1.DecodingError(`String length ${length} exceeds maximum allowed length ${this.config.maxCustomNameLength}`, { length, maxLength: this.config.maxCustomNameLength });
}
if (this.pos + length > this.buffer.length) {
throw new errors_1.DecodingError('String extends beyond buffer boundary', { position: this.pos, length, bufferLength: this.buffer.length });
}
try {
const value = new globalThis.TextDecoder('utf-8', { fatal: true })
.decode(this.buffer.slice(this.pos, this.pos + length));
this.pos += length;
return value;
}
catch (error) {
throw new errors_1.DecodingError('Invalid UTF-8 string encoding', { position: this.pos, length, originalError: error });
}
}
/**
* Safely reads length-delimited bytes
*/
readBytes() {
const length = this.readVarint();
if (length > 1024) { // Reasonable limit for embedded data
throw new errors_1.DecodingError(`Bytes length ${length} exceeds reasonable limit`, { length, maxLength: 1024 });
}
if (this.pos + length > this.buffer.length) {
throw new errors_1.DecodingError('Bytes extend beyond buffer boundary', { position: this.pos, length, bufferLength: this.buffer.length });
}
const bytes = this.buffer.slice(this.pos, this.pos + length);
this.pos += length;
return bytes;
}
/**
* Safely reads and parses a protobuf tag
*/
readTag() {
const tag = this.readVarint();
const fieldNumber = tag >>> 3;
const wireType = tag & 0x7;
// Validate wire type
if (wireType > 5) {
throw new errors_1.DecodingError(`Invalid wire type: ${wireType}`, { fieldNumber, wireType, tag });
}
// Validate field number range
if (fieldNumber < 1 || fieldNumber > 50) {
if (this.config.enableLogging) {
console.warn(`Unusual field number: ${fieldNumber}`);
}
}
return [fieldNumber, wireType];
}
/**
* Safely skips a field based on wire type
*/
skipField(wireType) {
switch (wireType) {
case 0: // varint
this.readVarint();
break;
case 1: // 64-bit
if (this.pos + 8 > this.buffer.length) {
throw new errors_1.DecodingError('Buffer underrun while skipping 64-bit field', { position: this.pos, needed: 8, available: this.buffer.length - this.pos });
}
this.pos += 8;
break;
case 2: // length-delimited
const length = this.readVarint();
if (this.pos + length > this.buffer.length) {
throw new errors_1.DecodingError('Buffer underrun while skipping length-delimited field', { position: this.pos, length, available: this.buffer.length - this.pos });
}
this.pos += length;
break;
case 5: // 32-bit
if (this.pos + 4 > this.buffer.length) {
throw new errors_1.DecodingError('Buffer underrun while skipping 32-bit field', { position: this.pos, needed: 4, available: this.buffer.length - this.pos });
}
this.pos += 4;
break;
default:
throw new errors_1.DecodingError(`Cannot skip unknown wire type: ${wireType}`, { wireType });
}
}
/**
* Checks if there's more data to read
*/
hasMore() {
return this.pos < this.buffer.length;
}
/**
* Gets current position for debugging
*/
getPosition() {
return this.pos;
}
/**
* Gets remaining bytes count
*/
getRemainingBytes() {
return this.buffer.length - this.pos;
}
/**
* Decodes a sticker from protobuf data
*/
static decodeSticker(reader) {
const sticker = {
slot: 0,
sticker_id: 0
};
let fieldsProcessed = 0;
const maxFields = 20; // Prevent infinite loops
while (reader.hasMore() && fieldsProcessed < maxFields) {
const [fieldNumber, wireType] = reader.readTag();
fieldsProcessed++;
try {
switch (fieldNumber) {
case 1: // slot
if (wireType !== 0)
throw new errors_1.DecodingError(`Invalid wire type for slot: ${wireType}`);
sticker.slot = reader.readVarint();
break;
case 2: // sticker_id
if (wireType !== 0)
throw new errors_1.DecodingError(`Invalid wire type for sticker_id: ${wireType}`);
sticker.sticker_id = reader.readVarint();
break;
case 3: // wear
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for wear: ${wireType}`);
sticker.wear = reader.readFloat();
break;
case 4: // scale
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for scale: ${wireType}`);
sticker.scale = reader.readFloat();
break;
case 5: // rotation
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for rotation: ${wireType}`);
sticker.rotation = reader.readFloat();
break;
case 6: // tint_id
if (wireType !== 0)
throw new errors_1.DecodingError(`Invalid wire type for tint_id: ${wireType}`);
sticker.tint_id = reader.readVarint();
break;
case 7: // offset_x
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for offset_x: ${wireType}`);
sticker.offset_x = reader.readFloat();
break;
case 8: // offset_y
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for offset_y: ${wireType}`);
sticker.offset_y = reader.readFloat();
break;
case 9: // offset_z
if (wireType !== 5)
throw new errors_1.DecodingError(`Invalid wire type for offset_z: ${wireType}`);
sticker.offset_z = reader.readFloat();
break;
case 10: // pattern
if (wireType !== 0)
throw new errors_1.DecodingError(`Invalid wire type for pattern: ${wireType}`);
sticker.pattern = reader.readVarint();
break;
case 11: // highlight_reel
if (wireType !== 0)
throw new errors_1.DecodingError(`Invalid wire type for highlight_reel: ${wireType}`);
sticker.highlight_reel = reader.readVarint();
break;
default:
reader.skipField(wireType);
break;
}
}
catch (error) {
throw new errors_1.DecodingError(`Error decoding sticker field ${fieldNumber}`, { fieldNumber, wireType, originalError: error });
}
}
if (fieldsProcessed >= maxFields) {
throw new errors_1.DecodingError('Too many fields in sticker, possible corruption', { fieldsProcessed });
}
return sticker;
}
/**
* Decodes masked protobuf data into an EconItem
*/
static decodeMaskedData(hexData, config = {}) {
try {
// Validate and process hex data
let processedHex = hexData.trim().toUpperCase();
if (processedHex.startsWith('00')) {
processedHex = processedHex.slice(2);
}
if (processedHex.length < 16) {
throw new errors_1.DecodingError('Hex data too short after processing', { originalLength: hexData.length, processedLength: processedHex.length });
}
// Remove CRC checksum (last 4 bytes)
processedHex = processedHex.slice(0, -8);
const bytes = hexToBytes(processedHex);
const reader = new ProtobufReader(bytes, config);
const decoded = {
defindex: 0,
paintindex: 0,
paintseed: 0,
paintwear: 0,
stickers: [],
keychains: [],
variations: []
};
let fieldsProcessed = 0;
const maxFields = 100; // Prevent infinite loops
while (reader.hasMore() && fieldsProcessed < maxFields) {
const [fieldNumber, wireType] = reader.readTag();
fieldsProcessed++;
try {
switch (fieldNumber) {
case 1: // accountid
decoded.accountid = reader.readVarint();
break;
case 2: // itemid (uint64)
decoded.itemid = reader.readVarint64();
break;
case 3: // defindex
decoded.defindex = reader.readVarint();
break;
case 4: // paintindex
decoded.paintindex = reader.readVarint();
break;
case 5: // rarity
decoded.rarity = reader.readVarint();
break;
case 6: // quality
decoded.quality = reader.readVarint();
break;
case 7: // paintwear
const wearBytes = reader.readVarint();
decoded.paintwear = bytesToFloat(wearBytes);
break;
case 8: // paintseed
decoded.paintseed = reader.readVarint();
break;
case 9: // killeaterscoretype
decoded.killeaterscoretype = reader.readVarint();
break;
case 10: // killeatervalue
decoded.killeatervalue = reader.readVarint();
break;
case 11: // customname
decoded.customname = reader.readString();
break;
case 12: // stickers
const stickerBytes = reader.readBytes();
const stickerReader = new ProtobufReader(stickerBytes, config);
const sticker = this.decodeSticker(stickerReader);
decoded.stickers.push(sticker);
break;
case 13: // inventory
decoded.inventory = reader.readVarint();
break;
case 14: // origin
decoded.origin = reader.readVarint();
break;
case 15: // questid
decoded.questid = reader.readVarint();
break;
case 16: // dropreason
decoded.dropreason = reader.readVarint();
break;
case 17: // musicindex
decoded.musicindex = reader.readVarint();
break;
case 18: // entindex (signed int32)
decoded.entindex = reader.readSInt32();
break;
case 19: // petindex
decoded.petindex = reader.readVarint();
break;
case 20: // keychains
const keychainBytes = reader.readBytes();
const keychainReader = new ProtobufReader(keychainBytes, config);
const keychain = this.decodeSticker(keychainReader);
decoded.keychains.push(keychain);
break;
case 21: // style
decoded.style = reader.readVarint();
break;
case 22: // variations
const variationBytes = reader.readBytes();
const variationReader = new ProtobufReader(variationBytes, config);
const variation = this.decodeSticker(variationReader);
decoded.variations.push(variation);
break;
case 23: // upgrade_level
decoded.upgrade_level = reader.readVarint();
break;
default:
if (config.enableLogging) {
console.warn(`Unknown field ${fieldNumber}, skipping`);
}
reader.skipField(wireType);
break;
}
}
catch (error) {
throw new errors_1.DecodingError(`Error decoding field ${fieldNumber}`, { fieldNumber, wireType, fieldsProcessed, originalError: error });
}
}
if (fieldsProcessed >= maxFields) {
throw new errors_1.DecodingError('Too many fields processed, possible infinite loop or corruption', { fieldsProcessed });
}
// Validate the decoded item if validation is enabled
if (config.validateInput) {
const validation = validation_1.Validator.validateEconItem(decoded);
if (!validation.valid) {
throw new errors_1.ValidationError(`Decoded item validation failed: ${validation.errors.join(', ')}`, { errors: validation.errors, warnings: validation.warnings });
}
}
return decoded;
}
catch (error) {
if (error instanceof errors_1.DecodingError || error instanceof errors_1.ValidationError) {
throw error;
}
throw new errors_1.DecodingError('Failed to decode masked data', { originalError: error, hexDataLength: hexData.length });
}
}
}
exports.ProtobufReader = ProtobufReader;
//# sourceMappingURL=protobuf-reader.js.map