pw_web: Fix printf decoder to work with varints

Change-Id: I039242331ba7253333c6374c2d1854ac33e921da
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/129588
Commit-Queue: Asad Memon <asadmemon@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/package-lock.json b/package-lock.json
index fbc143a..d00b93c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
 {
   "name": "pigweedjs",
-  "version": "0.0.5",
+  "version": "0.0.7",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "pigweedjs",
-      "version": "0.0.5",
+      "version": "0.0.7",
       "license": "Apache-2.0",
       "dependencies": {
         "@protobuf-ts/protoc": "^2.7.0",
         "google-protobuf": "^3.17.3",
+        "long": "^5.2.1",
         "object-path": "^0.11.8",
         "ts-protoc-gen": "^0.15.0"
       },
@@ -784,6 +785,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/@grpc/proto-loader/node_modules/long": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+      "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+      "dev": true
+    },
     "node_modules/@grpc/proto-loader/node_modules/protobufjs": {
       "version": "6.11.2",
       "dev": true,
@@ -7032,9 +7039,9 @@
       "license": "MIT"
     },
     "node_modules/long": {
-      "version": "4.0.0",
-      "dev": true,
-      "license": "Apache-2.0"
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+      "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
     },
     "node_modules/loose-envify": {
       "version": "1.4.0",
@@ -10604,6 +10611,12 @@
         "yargs": "^16.1.1"
       },
       "dependencies": {
+        "long": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+          "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+          "dev": true
+        },
         "protobufjs": {
           "version": "6.11.2",
           "dev": true,
@@ -15068,8 +15081,9 @@
       "dev": true
     },
     "long": {
-      "version": "4.0.0",
-      "dev": true
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+      "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
     },
     "loose-envify": {
       "version": "1.4.0",
diff --git a/package.json b/package.json
index 837da7f..dc85bc9 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
   "dependencies": {
     "@protobuf-ts/protoc": "^2.7.0",
     "google-protobuf": "^3.17.3",
+    "long": "^5.2.1",
     "object-path": "^0.11.8",
     "ts-protoc-gen": "^0.15.0"
   },
diff --git a/pw_tokenizer/ts/detokenizer.ts b/pw_tokenizer/ts/detokenizer.ts
index 90bff2b..fe6ea91 100644
--- a/pw_tokenizer/ts/detokenizer.ts
+++ b/pw_tokenizer/ts/detokenizer.ts
@@ -115,7 +115,7 @@
       data.byteOffset,
       4
     ).getUint32(0, true);
-    const args = new Uint8Array(data.buffer.slice(4));
+    const args = new Uint8Array(data.buffer.slice(data.byteOffset + 4));
 
     return {token, args};
   }
diff --git a/pw_tokenizer/ts/int_testdata.ts b/pw_tokenizer/ts/int_testdata.ts
new file mode 100644
index 0000000..36b5a21
--- /dev/null
+++ b/pw_tokenizer/ts/int_testdata.ts
@@ -0,0 +1,52 @@
+// Copyright 2023 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.
+
+const IntDB = [
+  ["%d", "-128", "%u", "4294967168", '\xff\x01'],
+  ["%d", "-10", "%u", "4294967286", '\x13'],
+  ["%d", "-9", "%u", "4294967287", '\x11'],
+  ["%d", "-8", "%u", "4294967288", '\x0f'],
+  ["%d", "-7", "%u", "4294967289", '\x0d'],
+  ["%d", "-6", "%u", "4294967290", '\x0b'],
+  ["%d", "-5", "%u", "4294967291", '\x09'],
+  ["%d", "-4", "%u", "4294967292", '\x07'],
+  ["%d", "-3", "%u", "4294967293", '\x05'],
+  ["%d", "-2", "%u", "4294967294", '\x03'],
+  ["%d", "-1", "%u", "4294967295", '\x01'],
+  ["%d", "0", "%u", "0", '\x00'],
+  ["%d", "1", "%u", "1", '\x02'],
+  ["%d", "2", "%u", "2", '\x04'],
+  ["%d", "3", "%u", "3", '\x06'],
+  ["%d", "4", "%u", "4", '\x08'],
+  ["%d", "5", "%u", "5", '\x0a'],
+  ["%d", "6", "%u", "6", '\x0c'],
+  ["%d", "7", "%u", "7", '\x0e'],
+  ["%d", "8", "%u", "8", '\x10'],
+  ["%d", "9", "%u", "9", '\x12'],
+  ["%d", "10", "%u", "10", '\x14'],
+  ["%d", "127", "%u", "127", '\xfe\x01'],
+  ["%d", "-32768", "%u", "4294934528", '\xff\xff\x03'],
+  ["%d", "652344632", "%u", "652344632", '\xf0\xf4\x8f\xee\x04'],
+  ["%d", "18567", "%u", "18567", '\x8e\xa2\x02'],
+  ["%d", "-14", "%u", "4294967282", '\x1b'],
+  ["%d", "-2147483648", "%u", "2147483648", '\xff\xff\xff\xff\x0f'],
+  ["%ld", "-14", "%lu", "4294967282", '\x1b'],
+  ["%d", "2075650855", "%u", "2075650855", '\xce\xac\xbf\xbb\x0f'],
+  ["%lld", "5922204476835468009", "%llu", "5922204476835468009", '\xd2\xcb\x8c\x90\x86\xe6\xf2\xaf\xa4\x01'],
+  ["%lld", "-9223372036854775808", "%llu", "9223372036854775808", '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+  ["%lld", "3273441488341945355", "%llu", "3273441488341945355", '\x96\xb0\xae\x9a\x96\xec\xcc\xed\x5a'],
+  ["%lld", "-9223372036854775807", "%llu", "9223372036854775809", '\xfd\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+]
+
+export default IntDB;
diff --git a/pw_tokenizer/ts/printf_decoder.ts b/pw_tokenizer/ts/printf_decoder.ts
index 127d358..7b02030 100644
--- a/pw_tokenizer/ts/printf_decoder.ts
+++ b/pw_tokenizer/ts/printf_decoder.ts
@@ -13,9 +13,9 @@
 // the License.
 
 /** Decodes arguments and formats them with the provided format string. */
+import Long from "long";
 
-const SPECIFIER_REGEX = /%(\.([0-9]+))?([%csdioxXufFeEaAgGnp])/g;
-
+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('');
@@ -33,14 +33,24 @@
 
 interface DecodedArg {
   size: number;
-  value: string | number | null;
+  value: string | number | Long | null;
 }
 
 // ZigZag decode function from protobuf's wire_format module.
-function zigzagDecode(value: number): number {
-  if (!(value & 0x1)) return value >> 1;
-  return (value >> 1) ^ ~0;
-}
+function zigzagDecode(value: Long, unsigned: boolean = 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.
+  var bitsLow = value.low, bitsHigh = value.high;
+  var 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.
@@ -65,16 +75,19 @@
   }
 
   private decodeSignedInt(args: Uint8Array): DecodedArg {
-    if (args.length === 0) return {size: 0, value: null};
+    return this._decodeInt(args);
+  }
 
+  private _decodeInt(args: Uint8Array, unsigned: boolean = false): DecodedArg {
+    if (args.length === 0) return {size: 0, value: null};
     let count = 0;
-    let result = 0;
+    let result = new Long(0);
     let shift = 0;
     for (count = 0; count < args.length; count++) {
       const byte = args[count];
-      result |= (byte & 0x7f) << shift;
+      result = result.or((Long.fromInt(byte, unsigned).and(0x7f)).shiftLeft(shift));
       if (!(byte & 0x80)) {
-        return {value: zigzagDecode(result), size: count + 1};
+        return {value: zigzagDecode(result, unsigned), size: count + 1};
       }
       shift += 7;
       if (shift >= 64) break;
@@ -83,15 +96,21 @@
     return {size: 0, value: null};
   }
 
-  private decodeUnsignedInt(args: Uint8Array): DecodedArg {
-    const arg = this.decodeSignedInt(args);
+  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 number;
-      num = num >>> 0;
-      arg.value = num;
+      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;
   }
@@ -100,8 +119,8 @@
     const arg = this.decodeSignedInt(args);
 
     if (arg.value !== null) {
-      const num = arg.value as number;
-      arg.value = String.fromCharCode(num);
+      const num = arg.value as Long;
+      arg.value = String.fromCharCode(num.toInt());
     }
     return arg;
   }
@@ -116,7 +135,7 @@
     return {size: 4, value: floatValue};
   }
 
-  private format(specifierType: string, args: Uint8Array, precision: string): DecodedArg {
+  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);
@@ -128,7 +147,7 @@
       return this.decodeSignedInt(args);
     }
     if (UNSIGNED_INT.indexOf(specifierType) !== -1) {
-      return this.decodeUnsignedInt(args);
+      return this.decodeUnsignedInt(args, lengthSpecifier);
     }
     if (FLOATING_POINT.indexOf(specifierType) !== -1) {
       return this.decodeFloat(args, precision);
@@ -141,11 +160,11 @@
   decode(formatString: string, args: Uint8Array): string {
     return formatString.replace(
       SPECIFIER_REGEX,
-      (_specifier, _precisionFull, precision, specifierType) => {
-        const decodedArg = this.format(specifierType, args, precision);
-      args = args.slice(decodedArg.size);
-      if (decodedArg === null) return '';
-      return String(decodedArg.value);
-    });
+      (_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);
+      });
   }
 }
diff --git a/pw_tokenizer/ts/printf_decoder_test.ts b/pw_tokenizer/ts/printf_decoder_test.ts
index 80814ff..db28e48 100644
--- a/pw_tokenizer/ts/printf_decoder_test.ts
+++ b/pw_tokenizer/ts/printf_decoder_test.ts
@@ -14,6 +14,7 @@
 
 /* eslint-env browser */
 import {PrintfDecoder} from './printf_decoder';
+import IntDB from './int_testdata';
 
 function argFromString(arg: string): Uint8Array {
   const data = new TextEncoder().encode(arg);
@@ -52,6 +53,41 @@
     ).toEqual('Hello Mac and PC');
   });
 
+  it('formats string + number correctly', () => {
+    expect(
+      printfDecoder.decode(
+        'Hello %s and %u',
+        argsConcat(argFromString('Computer'), argFromStringBinary('\xff\xff\x03'))
+      )).toEqual(
+        'Hello Computer and 4294934528');
+  });
+
+  it('formats integers correctly', () => {
+    for (let index = 0; index < IntDB.length; index++) {
+      const testcase = IntDB[index];
+      // Test signed
+      expect(
+        printfDecoder
+          .decode(testcase[0], argFromStringBinary(testcase[4])))
+        .toEqual(testcase[1]);
+
+      // Test unsigned
+      expect(
+        printfDecoder
+          .decode(testcase[2], argFromStringBinary(testcase[4])))
+        .toEqual(testcase[3]);
+    }
+  });
+
+  it('formats string correctly', () => {
+    expect(
+      printfDecoder.decode(
+        'Hello %s and %s',
+        argsConcat(argFromString('Mac'), argFromString('PC'))
+      )
+    ).toEqual('Hello Mac and PC');
+  });
+
   it('formats varint correctly', () => {
     const arg = argFromStringBinary('\xff\xff\x03');
     expect(printfDecoder.decode('Number %d', arg)).toEqual('Number -32768');