Repository: denoland/deno
Pull request: fix(ext/node): truncate first non-hex value on Buffer.from

Closes #31083

The solution can actually be fixed by replacing the `AND` operator with `OR` here:
https://github.com/denoland/deno/blob/58f7435270bd018386ddb8f35ddc2494a0e8695f/ext/node/polyfills/internal_binding/_utils.ts#L68

But I favor the solution of using a `unhexTable` because it reflects the Node.js implementation and cheaper to execute.

Deno v2.5.6:
```
| benchmark                         | time/iter (avg) |        iter/s |      (min … max)      |      p75 |      p99 |     p995 |
| --------------------------------- | --------------- | ------------- | --------------------- | -------- | -------- | -------- |
| Buffer.from hex 20 str length     |          1.3 µs |       792,900 | (  1.2 µs …   1.7 µs) |   1.3 µs |   1.7 µs |   1.7 µs |
| Buffer.from hex 200 str length    |          7.5 µs |       133,800 | (  7.2 µs …   8.0 µs) |   7.6 µs |   8.0 µs |   8.0 µs |
| Buffer.from hex 2000 str length   |         70.6 µs |        14,170 | ( 65.2 µs …   2.0 ms) |  70.9 µs | 109.0 µs | 136.0 µs |
```

This PR:
```
| benchmark                         | time/iter (avg) |        iter/s |      (min … max)      |      p75 |      p99 |     p995 |
| --------------------------------- | --------------- | ------------- | --------------------- | -------- | -------- | -------- |
| Buffer.from hex 20 str length     |        478.6 ns |     2,089,000 | (380.0 ns … 905.4 ns) | 477.6 ns | 877.2 ns | 905.4 ns |
| Buffer.from hex 200 str length    |        804.4 ns |     1,243,000 | (715.6 ns …   1.1 µs) | 804.9 ns |   1.1 µs |   1.1 µs |
| Buffer.from hex 2000 str length   |          4.4 µs |       227,200 | (  4.0 µs …   6.3 µs) |   4.4 µs |   6.3 µs |   6.3 µs |
```

Changed files:
filename: ext/node/polyfills/internal_binding/_utils.ts
patch: @@ -58,14 +58,35 @@ export function base64UrlToBytes(str: string) {
   return forgivingBase64UrlDecode(str);
 }
 
+// https://github.com/nodejs/node/blob/591ba692bfe30408e6a67397e7d18bfa1b9c3561/deps/nbytes/src/nbytes.cpp#L144-L158
+// deno-fmt-ignore
+export const unhexTable = new Int8Array([
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
+   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1, // 48 - 63
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255
+]);
+
 export function hexToBytes(str: string) {
   const length = str.length >>> 1;
   const byteArray = new Uint8Array(length);
   let i: number;
   for (i = 0; i < length; i++) {
-    const a = Number.parseInt(str[i * 2], 16);
-    const b = Number.parseInt(str[i * 2 + 1], 16);
-    if (Number.isNaN(a) && Number.isNaN(b)) {
+    const a = unhexTable[str.charCodeAt(i * 2) & 0xff];
+    const b = unhexTable[str.charCodeAt(i * 2 + 1) & 0xff];
+    if (!~a || !~b) {
       break;
     }
     byteArray[i] = (a << 4) | b;

filename: ext/node/polyfills/querystring.js
patch: @@ -5,6 +5,7 @@
 
 import { Buffer } from "node:buffer";
 import { encodeStr, hexTable } from "ext:deno_node/internal/querystring.ts";
+import { unhexTable } from "ext:deno_node/internal_binding/_utils.ts";
 
 /**
  * Alias of querystring.parse()
@@ -393,26 +394,6 @@ export function stringify(
   return "";
 }
 
-// deno-fmt-ignore
-const unhexTable = new Int8Array([
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
-  +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63
-  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
-  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255
-]);
-
 /**
  * A safe fast alternative to decodeURIComponent
  */

filename: tests/unit_node/buffer_test.ts
patch: @@ -688,3 +688,18 @@ Deno.test({
     );
   },
 });
+
+Deno.test({
+  name:
+    "[node/buffer] Buffer.from with hex encoding should truncate first non-hex character",
+  fn() {
+    const buf = Buffer.from("00aafffz", "hex");
+    assertEquals(buf, Buffer.from([0x00, 0xaa, 0xff]));
+
+    const buf2 = Buffer.from("zz34", "hex");
+    assertEquals(buf2, Buffer.from([]));
+
+    const buf3 = Buffer.from("123😁aa", "hex");
+    assertEquals(buf3, Buffer.from([0x12]));
+  },
+});