xref: /aosp_15_r20/external/pigweed/pw_tokenizer/ts/printf_decoder.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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