Skip to content

Commit 06e5487

Browse files
committed
Update jsbt. Improve error messages and type checks.
1 parent db41224 commit 06e5487

8 files changed

Lines changed: 137 additions & 71 deletions

File tree

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ on:
44
types: [created]
55
jobs:
66
release-js:
7-
name: jsbt v0.5.1
8-
uses: paulmillr/jsbt/.github/workflows/release.yml@231cc389fe2f825b6531285ad8c275584bf76588
7+
name: jsbt v0.5.2
8+
uses: paulmillr/jsbt/.github/workflows/release.yml@3b6bf563e416e9c6047db19410988922084e1e92
99
permissions:
1010
contents: read
1111
id-token: write

.github/workflows/test-ts.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
- pull_request
55
jobs:
66
test-ts:
7-
name: jsbt v0.5.1
8-
uses: paulmillr/jsbt/.github/workflows/test.yml@231cc389fe2f825b6531285ad8c275584bf76588
7+
name: jsbt v0.5.2
8+
uses: paulmillr/jsbt/.github/workflows/test.yml@3b6bf563e416e9c6047db19410988922084e1e92
99
with:
1010
runs-on: 'ubuntu-24.04'

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,15 @@
2020
"LICENSE-MIT"
2121
],
2222
"devDependencies": {
23-
"@paulmillr/jsbt": "0.5.0",
23+
"@paulmillr/jsbt": "0.5.2",
2424
"@types/node": "25.3.0",
2525
"prettier": "3.6.2",
2626
"typescript": "6.0.2"
2727
},
2828
"scripts": {
2929
"build": "tsc",
30-
"build:release": "npx --no @paulmillr/jsbt esbuild test/build",
30+
"build:release": "npx --no @paulmillr/jsbt bundle test/build",
3131
"check": "npx --no @paulmillr/jsbt check package.json",
32-
"check:readme": "npx --no @paulmillr/jsbt readme package.json",
33-
"check:treeshake": "npx --no @paulmillr/jsbt treeshake package.json test/build/out-treeshake",
34-
"check:jsdoc": "npx --no @paulmillr/jsbt tsdoc package.json",
3532
"bench": "cd test/benchmark && npm ci && node index.ts",
3633
"format": "prettier --write src",
3734
"test": "cd test; npm ci; node --experimental-strip-types --no-warnings index.ts",

src/decode.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ limitations under the License.
2020
*/
2121

2222
import type { EncodingType, ErrorCorrection, Image, Mask, Point, TArg, TRet } from './index.ts';
23-
import { Bitmap, utils } from './index.ts';
23+
import { asafenumber, Bitmap, utils, validateObject } from './index.ts';
2424

2525
// Constants
2626
const MAX_BITS_ERROR = 3; // Up to 3 bit errors in version/format
@@ -1338,11 +1338,12 @@ function cropToSquare(img: TArg<Image>) {
13381338
* ```
13391339
*/
13401340
export function decodeQR(img: TArg<Image>, opts: TArg<DecodeOpts> = {}): string {
1341-
let image = img as Image;
1342-
const options = opts as DecodeOpts;
1341+
// Validate public wrappers before field reads hide which top-level argument was wrong.
1342+
let image = validateObject(img, 'img') as Image;
1343+
const options = validateObject(opts, 'opts') as DecodeOpts;
13431344
for (const field of ['height', 'width'] as const) {
1344-
if (!Number.isSafeInteger(image[field]) || image[field] <= 0)
1345-
throw new Error(`invalid img.${field}=${image[field]} (${typeof image[field]})`);
1345+
asafenumber(image[field], `img.${field}`);
1346+
if (image[field] <= 0) throw new RangeError(`invalid img.${field}=${image[field]}`);
13461347
}
13471348
const { data } = image;
13481349
if (!Array.isArray(data) && !isBytes(data))

src/index.ts

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,38 @@ export interface Coder<F, T> {
157157
decode(to: T): F;
158158
}
159159

160+
export function anumber(n: number, title: string): number {
161+
if (typeof n !== 'number')
162+
throw new TypeError(`"${title}" expected number, got type=${typeof n}`);
163+
return n;
164+
}
165+
166+
export function asafenumber(n: number, title: string): number {
167+
anumber(n, title);
168+
if (!Number.isSafeInteger(n))
169+
throw new RangeError(`"${title}" expected safe integer, got ${n}`);
170+
return n;
171+
}
172+
173+
export function astring(s: string, title: string): string {
174+
if (typeof s !== 'string')
175+
throw new TypeError(`"${title}" expected string, got type=${typeof s}`);
176+
return s;
177+
}
178+
179+
export function validateObject<T>(object: TArg<T>, title: string): T {
180+
if (object === null || typeof object !== 'object' || Array.isArray(object))
181+
throw new TypeError(`"${title}" expected object, got type=${typeof object}`);
182+
return object as T;
183+
}
184+
160185
function assertNumber(n: number) {
161186
if (!Number.isSafeInteger(n)) throw new Error(`integer expected: ${n}`);
162187
}
163188

164189
function validateVersion(ver: Version): void {
165-
if (!Number.isSafeInteger(ver) || ver < 1 || ver > 40)
166-
throw new Error(`Invalid version=${ver}. Expected number [1..40]`);
190+
asafenumber(ver, 'ver');
191+
if (ver < 1 || ver > 40) throw new RangeError(`Invalid version=${ver}. Expected number [1..40]`);
167192
}
168193

169194
function bin(dec: number, pad: number): string {
@@ -348,10 +373,13 @@ type ReadFn = (c: Point, curr: DrawValue) => void;
348373
export class Bitmap {
349374
private static size(size: Size | number, limit?: Size) {
350375
if (typeof size === 'number') size = { height: size, width: size };
376+
size = validateObject(size, 'size');
377+
anumber(size.height, 'size.height');
378+
anumber(size.width, 'size.width');
351379
if (!Number.isSafeInteger(size.height) && size.height !== Infinity)
352-
throw new Error(`Bitmap: invalid height=${size.height} (${typeof size.height})`);
380+
throw new RangeError(`Bitmap: invalid height=${size.height} (${typeof size.height})`);
353381
if (!Number.isSafeInteger(size.width) && size.width !== Infinity)
354-
throw new Error(`Bitmap: invalid width=${size.width} (${typeof size.width})`);
382+
throw new RangeError(`Bitmap: invalid width=${size.width} (${typeof size.width})`);
355383
if (limit !== undefined) {
356384
// Clamp length, so it won't overflow, also allows to use Infinity, so we draw until end
357385
size = {
@@ -361,7 +389,15 @@ export class Bitmap {
361389
}
362390
return size;
363391
}
392+
private static point(p: Point, title: string): Point {
393+
p = validateObject(p, title);
394+
if (!('x' in p) || !('y' in p)) throw new TypeError(`Bitmap: "${title}" expected { x, y }`);
395+
asafenumber(p.x, title + '.x');
396+
asafenumber(p.y, title + '.y');
397+
return p;
398+
}
364399
static fromString(s: string): Bitmap {
400+
astring(s, 's');
365401
// Remove linebreaks on start and end, so we draw in `` section
366402
// Fixture strings use LF-delimited rows of X / space / ? characters; callers
367403
// must normalize CRLF input before handing it to this debug parser.
@@ -413,7 +449,9 @@ export class Bitmap {
413449
this.fullWords = Math.floor(width / 32) | 0;
414450
this.value = new Uint32Array(this.words * height);
415451
this.defined = new Uint32Array(this.value.length);
416-
if (data) {
452+
if (data !== undefined) {
453+
if (!Array.isArray(data))
454+
throw new TypeError(`"data" expected array, got type=${typeof data}`);
417455
// accept same semantics as old version
418456
if (data.length !== height)
419457
throw new Error(`Bitmap: data height mismatch: exp=${height} got=${data.length}`);
@@ -426,6 +464,7 @@ export class Bitmap {
426464
}
427465
}
428466
point(p: Point): DrawValue {
467+
p = Bitmap.point(p, 'p');
429468
// The storage docs above say "undefined is used as a marker whether cell
430469
// was written or not"; `point()` is the detector's dark-module read and
431470
// intentionally treats both undefined and false as not-dark. Use
@@ -434,17 +473,17 @@ export class Bitmap {
434473
}
435474
// Raw bounds check for scan loops; unlike `xy()`, this does not wrap or normalize coordinates.
436475
isInside(p: Point): boolean {
476+
p = Bitmap.point(p, 'p');
437477
return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
438478
}
439479
size(offset?: Point | number): { height: number; width: number } {
440480
if (!offset) return { height: this.height, width: this.width };
441-
const { x, y } = this.xy(offset);
481+
const { x, y } = this.xy(offset, 'offset');
442482
return { height: this.height - y, width: this.width - x };
443483
}
444-
private xy(c: Point | number) {
484+
private xy(c: Point | number, title = 'c') {
445485
if (typeof c === 'number') c = { x: c, y: c };
446-
if (!Number.isSafeInteger(c.x)) throw new Error(`Bitmap: invalid x=${c.x}`);
447-
if (!Number.isSafeInteger(c.y)) throw new Error(`Bitmap: invalid y=${c.y}`);
486+
c = Bitmap.point(c, title);
448487
// Bitmap's class docs say "For most `draw` calls, structure is mutable";
449488
// coordinate objects follow that hot-path policy too and are normalized in place.
450489
c.x = mod(c.x, this.width);
@@ -586,8 +625,8 @@ export class Bitmap {
586625
border(border = 2, value: DrawValue): Bitmap {
587626
// `border` is used both as output-size delta and as embed coordinate; keep
588627
// it a positive safe integer before those paths allocate or normalize.
589-
if (!Number.isSafeInteger(border) || border <= 0)
590-
throw new Error(`Bitmap.border: invalid size=${border}`);
628+
asafenumber(border, 'border');
629+
if (border <= 0) throw new RangeError(`Bitmap.border: invalid size=${border}`);
591630
const height = this.height + 2 * border;
592631
const width = this.width + 2 * border;
593632
const out = new Bitmap({ height, width });
@@ -687,8 +726,8 @@ export class Bitmap {
687726
}
688727
// Each pixel size is multiplied by factor
689728
scale(factor: number): Bitmap {
690-
if (!Number.isSafeInteger(factor) || factor > 1024)
691-
throw new Error(`invalid scale factor: ${factor}`);
729+
asafenumber(factor, 'factor');
730+
if (factor <= 0 || factor > 1024) throw new RangeError(`invalid scale factor: ${factor}`);
692731
const { height, width } = this;
693732
// Bitmap storage docs say "undefined is used as a marker whether cell was
694733
// written or not"; `scale()` is an output materialization path and samples
@@ -722,11 +761,12 @@ export class Bitmap {
722761
countPatternInRow(y: number, patternLen: number, ...patterns: number[]): number {
723762
// Penalty scanning only passes Table 11 windows over bounded symbol rows;
724763
// validate this public helper before JS shifts / typed-array reads coerce bad inputs.
725-
if (!Number.isSafeInteger(patternLen) || patternLen <= 0 || patternLen >= 32)
726-
throw new Error('wrong patternLen');
764+
asafenumber(y, 'y');
765+
asafenumber(patternLen, 'patternLen');
766+
if (patternLen <= 0 || patternLen >= 32) throw new RangeError('wrong patternLen');
727767
const mask = (1 << patternLen) - 1;
728768
const { height, width, value, words } = this;
729-
if (!Number.isSafeInteger(y) || y < 0 || y >= height) return 0;
769+
if (y < 0 || y >= height) return 0;
730770
let count = 0;
731771
const rowBase = this.wordIndex(0, y);
732772
for (let i = 0, window = 0; i < words; i++) {
@@ -750,7 +790,8 @@ export class Bitmap {
750790
// ISO/IEC 18004:2024 §7.8.3.1 N1 scans adjacent modules in bounded rows
751791
// and columns; validate this public helper before missing typed-array rows
752792
// are coerced into all-light runs by bitwise operators.
753-
if (!Number.isSafeInteger(y) || y < 0 || y >= height) return;
793+
asafenumber(y, 'y');
794+
if (y < 0 || y >= height) return;
754795
let runLen = 0;
755796
let runValue: boolean | undefined;
756797
const rowBase = this.wordIndex(0, y);
@@ -785,7 +826,8 @@ export class Bitmap {
785826
const { height, width, words } = this;
786827
// ISO/IEC 18004:2024 §7.8.3.1 N2 counts 2 x 2 module blocks in bounded
787828
// rows; reject non-integer scan rows before bitwise coercions truncate them.
788-
if (width < 2 || !Number.isSafeInteger(y) || y < 0 || y + 1 >= height) return 0;
829+
asafenumber(y, 'y');
830+
if (width < 2 || y < 0 || y + 1 >= height) return 0;
789831
const base0 = this.wordIndex(0, y);
790832
const base1 = this.wordIndex(0, y + 1);
791833
// valid "left-edge" positions x in [0 .. W-2]
@@ -1005,7 +1047,10 @@ const ECC_BLOCKS = {
10051047
// ISO/IEC 18004:2024 sections 5.3/7.4/7.5/7.9/7.10: QR layout, segment, format/version, and capacity helpers.
10061048
const info = /* @__PURE__ */ Object.freeze({
10071049
size: /* @__PURE__ */ Object.freeze({
1008-
encode: (ver: Version) => 21 + 4 * (ver - 1), // ver1 = 21, ver40 = 177 modules per side
1050+
encode: (ver: Version) => {
1051+
validateVersion(ver);
1052+
return 21 + 4 * (ver - 1); // ver1 = 21, ver40 = 177 modules per side
1053+
},
10091054
decode: (size: number) => (size - 17) / 4,
10101055
} as Coder<Version, number>),
10111056
// ISO/IEC 18004:2024 Table 3: map version ranges 1-9, 10-26, and 27-40 to count-width indexes.
@@ -1519,7 +1564,7 @@ declare const TextEncoder: any;
15191564
// non-default initial ECI data starts with an ECI header. Keep UTF-8 bytes
15201565
// without that header for compatibility with existing emoji/qrcode fixtures.
15211566
export function utf8ToBytes(str: string): TRet<Uint8Array> {
1522-
if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
1567+
astring(str, 'str');
15231568
return new Uint8Array(new TextEncoder().encode(str)) as TRet<Uint8Array>; // https://bugzil.la/1681809
15241569
}
15251570

@@ -1756,7 +1801,10 @@ export function encodeQR(
17561801
output: Output = 'raw',
17571802
opts: TArg<QrOpts & SvgQrOpts> = {}
17581803
) {
1759-
const _opts = opts as QrOpts & SvgQrOpts;
1804+
// Public guards run before QR mode detection and option reads so bad inputs keep argument names.
1805+
astring(text, 'text');
1806+
astring(output, 'output');
1807+
const _opts = validateObject(opts, 'opts') as QrOpts & SvgQrOpts;
17601808
const ecc = _opts.ecc !== undefined ? _opts.ecc : 'medium';
17611809
validateECC(ecc);
17621810
const encoding = _opts.encoding !== undefined ? _opts.encoding : detectType(text);
@@ -1789,7 +1837,8 @@ export function encodeQR(
17891837
// 2-module default to avoid changing encoder output; callers that need a
17901838
// standards-conformant quiet zone must pass `border: 4` explicitly.
17911839
const border = _opts.border === undefined ? 2 : _opts.border;
1792-
if (!Number.isSafeInteger(border) || border <= 0) throw new Error(`invalid border=${border}`);
1840+
asafenumber(border, 'opts.border');
1841+
if (border <= 0) throw new RangeError(`invalid border=${border}`);
17931842
res = res.border(border, false); // Add border
17941843
if (_opts.scale !== undefined) res = res.scale(_opts.scale); // Scale image
17951844
if (output === 'raw') return res.toRaw();

0 commit comments

Comments
 (0)