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 = 19 /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g; 20// Conversion specifiers by type; n is not supported. 21const SIGNED_INT = 'di'.split(''); 22const UNSIGNED_INT = 'oxXup'.split(''); 23const FLOATING_POINT = 'fFeEaAgG'.split(''); 24 25enum DecodedStatusFlags { 26 // Status flags for a decoded argument. These values should match the 27 // DecodingStatus enum in pw_tokenizer/internal/decode.h. 28 OK = 0, // decoding was successful 29 MISSING = 1, // the argument was not present in the data 30 TRUNCATED = 2, // the argument was truncated during encoding 31 DECODE_ERROR = 4, // an error occurred while decoding the argument 32 SKIPPED = 8, // argument was skipped due to a previous error 33} 34 35interface DecodedArg { 36 size: number; 37 value: string | number | Long | null; 38} 39 40// ZigZag decode function from protobuf's wire_format module. 41function zigzagDecode(value: Long, unsigned = false): Long { 42 // 64 bit math is: 43 // signmask = (zigzag & 1) ? -1 : 0; 44 // twosComplement = (zigzag >> 1) ^ signmask; 45 // 46 // To work with 32 bit, we can operate on both but "carry" the lowest bit 47 // from the high word by shifting it up 31 bits to be the most significant bit 48 // of the low word. 49 let bitsLow = value.low, 50 bitsHigh = value.high; 51 const signFlipMask = -(bitsLow & 1); 52 bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask; 53 bitsHigh = (bitsHigh >>> 1) ^ signFlipMask; 54 return new Long(bitsLow, bitsHigh, unsigned); 55} 56 57export class PrintfDecoder { 58 // Reads a unicode string from the encoded data. 59 private decodeString(args: Uint8Array): DecodedArg { 60 if (args.length === 0) return { size: 0, value: null }; 61 let sizeAndStatus = args[0]; 62 let status = DecodedStatusFlags.OK; 63 64 if (sizeAndStatus & 0x80) { 65 status |= DecodedStatusFlags.TRUNCATED; 66 sizeAndStatus &= 0x7f; 67 } 68 69 const rawData = args.slice(0, sizeAndStatus + 1); 70 const data = rawData.slice(1); 71 if (data.length < sizeAndStatus) { 72 status |= DecodedStatusFlags.DECODE_ERROR; 73 } 74 75 const decoded = new TextDecoder().decode(data); 76 return { size: rawData.length, value: decoded }; 77 } 78 79 private decodeSignedInt(args: Uint8Array): DecodedArg { 80 return this._decodeInt(args); 81 } 82 83 private _decodeInt(args: Uint8Array, unsigned = false): DecodedArg { 84 if (args.length === 0) return { size: 0, value: null }; 85 let count = 0; 86 let result = new Long(0); 87 let shift = 0; 88 for (count = 0; count < args.length; count++) { 89 const byte = args[count]; 90 result = result.or( 91 Long.fromInt(byte, unsigned).and(0x7f).shiftLeft(shift), 92 ); 93 if (!(byte & 0x80)) { 94 return { value: zigzagDecode(result, unsigned), size: count + 1 }; 95 } 96 shift += 7; 97 if (shift >= 64) break; 98 } 99 100 return { size: 0, value: null }; 101 } 102 103 private decodeUnsignedInt( 104 args: Uint8Array, 105 lengthSpecifier: string, 106 ): DecodedArg { 107 const arg = this._decodeInt(args, true); 108 const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32; 109 110 // Since ZigZag encoding is used, unsigned integers must be masked off to 111 // their original bit length. 112 if (arg.value !== null) { 113 let num = arg.value as Long; 114 if (bits === 32) { 115 num = num.and(Long.fromInt(1).shiftLeft(bits).add(-1)); 116 } else { 117 num = num.and(-1); 118 } 119 arg.value = num.toString(); 120 } 121 return arg; 122 } 123 124 private decodeChar(args: Uint8Array): DecodedArg { 125 const arg = this.decodeSignedInt(args); 126 127 if (arg.value !== null) { 128 const num = arg.value as Long; 129 arg.value = String.fromCharCode(num.toInt()); 130 } 131 return arg; 132 } 133 134 private decodeFloat(args: Uint8Array, precision: string): DecodedArg { 135 if (args.length < 4) return { size: 0, value: '' }; 136 const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32( 137 0, 138 true, 139 ); 140 if (precision) 141 return { size: 4, value: floatValue.toFixed(parseInt(precision)) }; 142 return { size: 4, value: floatValue }; 143 } 144 145 private format( 146 specifierType: string, 147 args: Uint8Array, 148 precision: string, 149 lengthSpecifier: string, 150 ): DecodedArg { 151 if (specifierType == '%') return { size: 0, value: '%' }; // literal % 152 if (specifierType === 's') { 153 return this.decodeString(args); 154 } 155 if (specifierType === 'c') { 156 return this.decodeChar(args); 157 } 158 if (SIGNED_INT.indexOf(specifierType) !== -1) { 159 return this.decodeSignedInt(args); 160 } 161 if (UNSIGNED_INT.indexOf(specifierType) !== -1) { 162 return this.decodeUnsignedInt(args, lengthSpecifier); 163 } 164 if (FLOATING_POINT.indexOf(specifierType) !== -1) { 165 return this.decodeFloat(args, precision); 166 } 167 168 // Unsupported specifier, return as-is 169 return { size: 0, value: '%' + specifierType }; 170 } 171 172 decode(formatString: string, args: Uint8Array): string { 173 return formatString.replace( 174 SPECIFIER_REGEX, 175 ( 176 _specifier, 177 _precisionFull, 178 precision, 179 lengthSpecifier, 180 specifierType, 181 ) => { 182 const decodedArg = this.format( 183 specifierType, 184 args, 185 precision, 186 lengthSpecifier, 187 ); 188 args = args.slice(decodedArg.size); 189 if (decodedArg === null) return ''; 190 return String(decodedArg.value); 191 }, 192 ); 193 } 194} 195