• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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