1// Copyright 2022 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15/** Decodes arguments and formats them with the provided format string. */ 16import Long from "long"; 17 18const SPECIFIER_REGEX = /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g; 19// Conversion specifiers by type; n is not supported. 20const SIGNED_INT = 'di'.split(''); 21const UNSIGNED_INT = 'oxXup'.split(''); 22const FLOATING_POINT = 'fFeEaAgG'.split(''); 23 24enum DecodedStatusFlags { 25 // Status flags for a decoded argument. These values should match the 26 // DecodingStatus enum in pw_tokenizer/internal/decode.h. 27 OK = 0, // decoding was successful 28 MISSING = 1, // the argument was not present in the data 29 TRUNCATED = 2, // the argument was truncated during encoding 30 DECODE_ERROR = 4, // an error occurred while decoding the argument 31 SKIPPED = 8, // argument was skipped due to a previous error 32} 33 34interface DecodedArg { 35 size: number; 36 value: string | number | Long | null; 37} 38 39// ZigZag decode function from protobuf's wire_format module. 40function zigzagDecode(value: Long, unsigned: boolean = false): Long { 41 // 64 bit math is: 42 // signmask = (zigzag & 1) ? -1 : 0; 43 // twosComplement = (zigzag >> 1) ^ signmask; 44 // 45 // To work with 32 bit, we can operate on both but "carry" the lowest bit 46 // from the high word by shifting it up 31 bits to be the most significant bit 47 // of the low word. 48 var bitsLow = value.low, bitsHigh = value.high; 49 var signFlipMask = -(bitsLow & 1); 50 bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask; 51 bitsHigh = (bitsHigh >>> 1) ^ signFlipMask; 52 return new Long(bitsLow, bitsHigh, unsigned); 53}; 54 55export class PrintfDecoder { 56 // Reads a unicode string from the encoded data. 57 private decodeString(args: Uint8Array): DecodedArg { 58 if (args.length === 0) return {size: 0, value: null}; 59 let sizeAndStatus = args[0]; 60 let status = DecodedStatusFlags.OK; 61 62 if (sizeAndStatus & 0x80) { 63 status |= DecodedStatusFlags.TRUNCATED; 64 sizeAndStatus &= 0x7f; 65 } 66 67 const rawData = args.slice(0, sizeAndStatus + 1); 68 const data = rawData.slice(1); 69 if (data.length < sizeAndStatus) { 70 status |= DecodedStatusFlags.DECODE_ERROR; 71 } 72 73 const decoded = new TextDecoder().decode(data); 74 return {size: rawData.length, value: decoded}; 75 } 76 77 private decodeSignedInt(args: Uint8Array): DecodedArg { 78 return this._decodeInt(args); 79 } 80 81 private _decodeInt(args: Uint8Array, unsigned: boolean = false): DecodedArg { 82 if (args.length === 0) return {size: 0, value: null}; 83 let count = 0; 84 let result = new Long(0); 85 let shift = 0; 86 for (count = 0; count < args.length; count++) { 87 const byte = args[count]; 88 result = result.or((Long.fromInt(byte, unsigned).and(0x7f)).shiftLeft(shift)); 89 if (!(byte & 0x80)) { 90 return {value: zigzagDecode(result, unsigned), size: count + 1}; 91 } 92 shift += 7; 93 if (shift >= 64) break; 94 } 95 96 return {size: 0, value: null}; 97 } 98 99 private decodeUnsignedInt(args: Uint8Array, lengthSpecifier: string): DecodedArg { 100 const arg = this._decodeInt(args, true); 101 const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32; 102 103 // Since ZigZag encoding is used, unsigned integers must be masked off to 104 // their original bit length. 105 if (arg.value !== null) { 106 let num = arg.value as Long; 107 if (bits === 32) { 108 num = num.and((Long.fromInt(1).shiftLeft(bits)).add(-1)); 109 } 110 else { 111 num = num.and(-1); 112 } 113 arg.value = num.toString(); 114 } 115 return arg; 116 } 117 118 private decodeChar(args: Uint8Array): DecodedArg { 119 const arg = this.decodeSignedInt(args); 120 121 if (arg.value !== null) { 122 const num = arg.value as Long; 123 arg.value = String.fromCharCode(num.toInt()); 124 } 125 return arg; 126 } 127 128 private decodeFloat(args: Uint8Array, precision: string): DecodedArg { 129 if (args.length < 4) return {size: 0, value: ''}; 130 const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32( 131 0, 132 true 133 ); 134 if (precision) return {size: 4, value: floatValue.toFixed(parseInt(precision))} 135 return {size: 4, value: floatValue}; 136 } 137 138 private format(specifierType: string, args: Uint8Array, precision: string, lengthSpecifier: string): DecodedArg { 139 if (specifierType == '%') return {size: 0, value: '%'}; // literal % 140 if (specifierType === 's') { 141 return this.decodeString(args); 142 } 143 if (specifierType === 'c') { 144 return this.decodeChar(args); 145 } 146 if (SIGNED_INT.indexOf(specifierType) !== -1) { 147 return this.decodeSignedInt(args); 148 } 149 if (UNSIGNED_INT.indexOf(specifierType) !== -1) { 150 return this.decodeUnsignedInt(args, lengthSpecifier); 151 } 152 if (FLOATING_POINT.indexOf(specifierType) !== -1) { 153 return this.decodeFloat(args, precision); 154 } 155 156 // Unsupported specifier, return as-is 157 return {size: 0, value: '%' + specifierType}; 158 } 159 160 decode(formatString: string, args: Uint8Array): string { 161 return formatString.replace( 162 SPECIFIER_REGEX, 163 (_specifier, _precisionFull, precision, lengthSpecifier, specifierType) => { 164 const decodedArg = this.format(specifierType, args, precision, lengthSpecifier); 165 args = args.slice(decodedArg.size); 166 if (decodedArg === null) return ''; 167 return String(decodedArg.value); 168 }); 169 } 170} 171