// Copyright 2022 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. /** Decodes arguments and formats them with the provided format string. */ import Long from 'long'; const SPECIFIER_REGEX = /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g; // Conversion specifiers by type; n is not supported. const SIGNED_INT = 'di'.split(''); const UNSIGNED_INT = 'oxXup'.split(''); const FLOATING_POINT = 'fFeEaAgG'.split(''); enum DecodedStatusFlags { // Status flags for a decoded argument. These values should match the // DecodingStatus enum in pw_tokenizer/internal/decode.h. OK = 0, // decoding was successful MISSING = 1, // the argument was not present in the data TRUNCATED = 2, // the argument was truncated during encoding DECODE_ERROR = 4, // an error occurred while decoding the argument SKIPPED = 8, // argument was skipped due to a previous error } interface DecodedArg { size: number; value: string | number | Long | null; } // ZigZag decode function from protobuf's wire_format module. function zigzagDecode(value: Long, unsigned = false): Long { // 64 bit math is: // signmask = (zigzag & 1) ? -1 : 0; // twosComplement = (zigzag >> 1) ^ signmask; // // To work with 32 bit, we can operate on both but "carry" the lowest bit // from the high word by shifting it up 31 bits to be the most significant bit // of the low word. let bitsLow = value.low, bitsHigh = value.high; const signFlipMask = -(bitsLow & 1); bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask; bitsHigh = (bitsHigh >>> 1) ^ signFlipMask; return new Long(bitsLow, bitsHigh, unsigned); } export class PrintfDecoder { // Reads a unicode string from the encoded data. private decodeString(args: Uint8Array): DecodedArg { if (args.length === 0) return { size: 0, value: null }; let sizeAndStatus = args[0]; let status = DecodedStatusFlags.OK; if (sizeAndStatus & 0x80) { status |= DecodedStatusFlags.TRUNCATED; sizeAndStatus &= 0x7f; } const rawData = args.slice(0, sizeAndStatus + 1); const data = rawData.slice(1); if (data.length < sizeAndStatus) { status |= DecodedStatusFlags.DECODE_ERROR; } const decoded = new TextDecoder().decode(data); return { size: rawData.length, value: decoded }; } private decodeSignedInt(args: Uint8Array): DecodedArg { return this._decodeInt(args); } private _decodeInt(args: Uint8Array, unsigned = false): DecodedArg { if (args.length === 0) return { size: 0, value: null }; let count = 0; let result = new Long(0); let shift = 0; for (count = 0; count < args.length; count++) { const byte = args[count]; result = result.or( Long.fromInt(byte, unsigned).and(0x7f).shiftLeft(shift), ); if (!(byte & 0x80)) { return { value: zigzagDecode(result, unsigned), size: count + 1 }; } shift += 7; if (shift >= 64) break; } return { size: 0, value: null }; } private decodeUnsignedInt( args: Uint8Array, lengthSpecifier: string, ): DecodedArg { const arg = this._decodeInt(args, true); const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32; // Since ZigZag encoding is used, unsigned integers must be masked off to // their original bit length. if (arg.value !== null) { let num = arg.value as Long; if (bits === 32) { num = num.and(Long.fromInt(1).shiftLeft(bits).add(-1)); } else { num = num.and(-1); } arg.value = num.toString(); } return arg; } private decodeChar(args: Uint8Array): DecodedArg { const arg = this.decodeSignedInt(args); if (arg.value !== null) { const num = arg.value as Long; arg.value = String.fromCharCode(num.toInt()); } return arg; } private decodeFloat(args: Uint8Array, precision: string): DecodedArg { if (args.length < 4) return { size: 0, value: '' }; const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32( 0, true, ); if (precision) return { size: 4, value: floatValue.toFixed(parseInt(precision)) }; return { size: 4, value: floatValue }; } private format( specifierType: string, args: Uint8Array, precision: string, lengthSpecifier: string, ): DecodedArg { if (specifierType == '%') return { size: 0, value: '%' }; // literal % if (specifierType === 's') { return this.decodeString(args); } if (specifierType === 'c') { return this.decodeChar(args); } if (SIGNED_INT.indexOf(specifierType) !== -1) { return this.decodeSignedInt(args); } if (UNSIGNED_INT.indexOf(specifierType) !== -1) { return this.decodeUnsignedInt(args, lengthSpecifier); } if (FLOATING_POINT.indexOf(specifierType) !== -1) { return this.decodeFloat(args, precision); } // Unsupported specifier, return as-is return { size: 0, value: '%' + specifierType }; } decode(formatString: string, args: Uint8Array): string { return formatString.replace( SPECIFIER_REGEX, ( _specifier, _precisionFull, precision, lengthSpecifier, specifierType, ) => { const decodedArg = this.format( specifierType, args, precision, lengthSpecifier, ); args = args.slice(decodedArg.size); if (decodedArg === null) return ''; return String(decodedArg.value); }, ); } }