|
1 | 1 | import { describe, it, expect, beforeAll } from 'vitest'; |
| 2 | +import { zlibSync } from 'fflate'; |
2 | 3 | import { parseZPL } from './zplParser'; |
3 | 4 | import { props } from '../test/helpers'; |
4 | 5 |
|
| 6 | +/** CRC-16/XMODEM — same variant used by the parser to validate |
| 7 | + * :B64:/:Z64: wrappers (poly 0x1021, init 0x0000). Duplicated here so |
| 8 | + * tests can build valid CRC values without exporting the parser's |
| 9 | + * internal helper. */ |
| 10 | +function testCrc16(s: string): string { |
| 11 | + let crc = 0; |
| 12 | + for (const ch of s) { |
| 13 | + crc ^= ch.charCodeAt(0) << 8; |
| 14 | + for (let j = 0; j < 8; j++) { |
| 15 | + crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff; |
| 16 | + } |
| 17 | + } |
| 18 | + return crc.toString(16).padStart(4, '0').toUpperCase(); |
| 19 | +} |
| 20 | + |
| 21 | +function makeZ64Field(bytes: Uint8Array): string { |
| 22 | + const deflated = zlibSync(bytes); |
| 23 | + let bin = ''; |
| 24 | + for (const b of deflated) bin += String.fromCharCode(b); |
| 25 | + const b64 = btoa(bin); |
| 26 | + return `:Z64:${b64}:${testCrc16(b64)}`; |
| 27 | +} |
| 28 | + |
5 | 29 | // ── label config ────────────────────────────────────────────────────────────── |
6 | 30 |
|
7 | 31 | describe('parseZPL — label config', () => { |
@@ -543,6 +567,89 @@ describe('parseZPL — ^GFA graphic field', () => { |
543 | 567 | expect(props(objects[0])._gfaCache).toContain('^GFA,'); |
544 | 568 | }); |
545 | 569 |
|
| 570 | + it('imports a :B64:-wrapped ^GFA payload as an image (CRC valid)', () => { |
| 571 | + // 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → base64 "AAAA//8AAAA=" |
| 572 | + // CRC-16/CCITT-FALSE over "AAAA//8AAAA=" = 0xDFF8 |
| 573 | + const { objects, importReport } = parseZPL( |
| 574 | + '^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ', |
| 575 | + 8, |
| 576 | + ); |
| 577 | + expect(objects).toHaveLength(1); |
| 578 | + expect(objects[0]?.type).toBe('image'); |
| 579 | + expect(props(objects[0]).widthDots).toBe(8); |
| 580 | + expect(importReport.partial).not.toContain('^GF'); |
| 581 | + }); |
| 582 | + |
| 583 | + it('still renders a :B64: payload with mismatched CRC but flags as partial', () => { |
| 584 | + const { objects, importReport } = parseZPL( |
| 585 | + '^XA^FO0,0^GFA,8,8,1,:B64:AAAA//8AAAA=:0000^FS^XZ', |
| 586 | + 8, |
| 587 | + ); |
| 588 | + expect(objects).toHaveLength(1); |
| 589 | + expect(importReport.partial).toContain('^GF'); |
| 590 | + }); |
| 591 | + |
| 592 | + it('accepts :B64: wrapper on ^GFB and ^GFC (no raw-binary path needed)', () => { |
| 593 | + for (const fmt of ['B', 'C'] as const) { |
| 594 | + const { objects } = parseZPL( |
| 595 | + `^XA^FO0,0^GF${fmt},8,8,1,:B64:AAAA//8AAAA=:DFF8^FS^XZ`, |
| 596 | + 8, |
| 597 | + ); |
| 598 | + expect(objects).toHaveLength(1); |
| 599 | + expect(objects[0]?.type).toBe('image'); |
| 600 | + } |
| 601 | + }); |
| 602 | + |
| 603 | + it('tolerates embedded whitespace inside a :B64: base64 payload', () => { |
| 604 | + // ZPL generators often line-break long base64 blocks every N chars. |
| 605 | + // Labelary accepts this; we should too. |
| 606 | + const zpl = |
| 607 | + '^XA^FO0,0^GFA,8,8,1,:B64:AAAA\n//8AAAA=:DFF8^FS^XZ'; |
| 608 | + const { objects, importReport } = parseZPL(zpl, 8); |
| 609 | + expect(objects).toHaveLength(1); |
| 610 | + expect(importReport.partial).not.toContain('^GF'); |
| 611 | + }); |
| 612 | + |
| 613 | + it('tolerates trailing whitespace on wrapped GF payloads', () => { |
| 614 | + // Real-world ZPL is often line-broken between commands; the tokenizer |
| 615 | + // preserves the trailing newline on the field body, so the regex needs |
| 616 | + // to accommodate that. |
| 617 | + const zplWithNewline = |
| 618 | + '^XA\n^FO0,0\n^GFA,8,8,1,:B64:AAAA//8AAAA=:DFF8\n^FS\n^XZ'; |
| 619 | + const { objects, importReport } = parseZPL(zplWithNewline, 8); |
| 620 | + expect(objects).toHaveLength(1); |
| 621 | + expect(objects[0]?.type).toBe('image'); |
| 622 | + expect(importReport.browserLimit).toHaveLength(0); |
| 623 | + }); |
| 624 | + |
| 625 | + it('imports a :Z64:-wrapped ^GFC payload by inflating zlib data', () => { |
| 626 | + // 8 bytes = [0,0,0,0xFF,0xFF,0,0,0] → zlib-compressed → base64 → CRC. |
| 627 | + const bytes = new Uint8Array([0, 0, 0, 0xff, 0xff, 0, 0, 0]); |
| 628 | + const field = makeZ64Field(bytes); |
| 629 | + const { objects, importReport } = parseZPL( |
| 630 | + `^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`, |
| 631 | + 8, |
| 632 | + ); |
| 633 | + expect(objects).toHaveLength(1); |
| 634 | + expect(objects[0]?.type).toBe('image'); |
| 635 | + expect(props(objects[0]).widthDots).toBe(8); |
| 636 | + expect(importReport.partial).not.toContain('^GF'); |
| 637 | + }); |
| 638 | + |
| 639 | + it('records :Z64: with corrupt deflate stream as browserLimit', () => { |
| 640 | + // Valid base64 but garbage bytes that fflate will reject as a deflate |
| 641 | + // stream. CRC must match so we know the failure is in inflate, not the |
| 642 | + // wrapper-shape detection. |
| 643 | + const b64 = btoa('not a valid zlib stream'); |
| 644 | + const field = `:Z64:${b64}:${testCrc16(b64)}`; |
| 645 | + const { objects, importReport } = parseZPL( |
| 646 | + `^XA^FO0,0^GFC,8,8,1,${field}^FS^XZ`, |
| 647 | + 8, |
| 648 | + ); |
| 649 | + expect(objects).toHaveLength(0); |
| 650 | + expect(importReport.browserLimit.some((s) => s.startsWith('^GF'))).toBe(true); |
| 651 | + }); |
| 652 | + |
546 | 653 | it('creates an image object from compressed ^GFA data', () => { |
547 | 654 | // G=1 repeat → "GF" = repeat 'F' once, basically just 'F' |
548 | 655 | // bytesPerRow=1, so we need 2 nibbles per row |
|
0 commit comments