summaryrefslogtreecommitdiffstats
path: root/src/zheader.js
blob: 56c22fc259e666632cf50fc5f4718f3fa9810c3a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
"use strict";

var Zmodem = module.exports;

Object.assign(
    Zmodem,
    require("./encode"),
    require("./zdle"),
    require("./zmlib"),
    require("./zcrc"),
    require("./zerror")
);

const ZPAD = '*'.charCodeAt(0),
    ZBIN = 'A'.charCodeAt(0),
    ZHEX = 'B'.charCodeAt(0),
    ZBIN32 = 'C'.charCodeAt(0)
;

//NB: lrzsz uses \x8a rather than \x0a where the specs
//say to use LF. For simplicity, we avoid that and just use
//the 7-bit LF character.
const HEX_HEADER_CRLF = [ 0x0d, 0x0a ];
const HEX_HEADER_CRLF_XON = HEX_HEADER_CRLF.slice(0).concat( [Zmodem.ZMLIB.XON] );

//These are more or less duplicated by the logic in trim_leading_garbage().
//
//"**" + ZDLE_CHAR + "B"
const HEX_HEADER_PREFIX = [ ZPAD, ZPAD, Zmodem.ZMLIB.ZDLE, ZHEX ];
const BINARY16_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN ];
const BINARY32_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN32 ];

/** Class that represents a ZMODEM header. */
Zmodem.Header = class ZmodemHeader {

    //lrzsz’s “sz” command sends a random (?) CR/0x0d byte
    //after ZEOF. Let’s accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
    //
    //Also, when you skip a file, sz outputs a message about it.
    //
    //It appears that we’re supposed to ignore anything until
    //[ ZPAD, ZDLE ] when we’re looking for a header.

    /**
     * Weed out the leading bytes that aren’t valid to start a ZMODEM header.
     *
     * @param {number[]} ibuffer - The octet values to parse.
     *      Each array member should be an 8-bit unsigned integer (0-255).
     *      This object is mutated in the function.
     *
     * @returns {number[]} The octet values that were removed from the start
     *      of “ibuffer”. Order is preserved.
     */
    static trim_leading_garbage(ibuffer) {
        //Since there’s no escaping of the output it’s possible
        //that the garbage could trip us up, e.g., by having a filename
        //be a legit ZMODEM header. But that’s pretty unlikely.

        //Everything up to the first ZPAD: garbage
        //If first ZPAD has asterisk + ZDLE

        var garbage = [];

        var discard_all, parser, next_ZPAD_at_least = 0;

      TRIM_LOOP:
        while (ibuffer.length && !parser) {
            var first_ZPAD = ibuffer.indexOf(ZPAD);

            //No ZPAD? Then we purge the input buffer cuz it’s all garbage.
            if (first_ZPAD === -1) {
                discard_all = true;
                break TRIM_LOOP;
            }
            else {
                garbage.push.apply( garbage, ibuffer.splice(0, first_ZPAD) );

                //buffer has only an asterisk … gotta see about more
                if (ibuffer.length < 2) {
                    break TRIM_LOOP;
                }
                else if (ibuffer[1] === ZPAD) {
                    //Two leading ZPADs should be a hex header.

                    //We’re assuming the length of the header is 4 in
                    //this logic … but ZMODEM isn’t likely to change, so.
                    if (ibuffer.length < HEX_HEADER_PREFIX.length) {
                        if (ibuffer.join() === HEX_HEADER_PREFIX.slice(0, ibuffer.length).join()) {
                            //We have an incomplete fragment that matches
                            //HEX_HEADER_PREFIX. So don’t trim any more.
                            break TRIM_LOOP;
                        }

                        //Otherwise, we’ll discard one.
                    }
                    else if ((ibuffer[2] === HEX_HEADER_PREFIX[2]) && (ibuffer[3] === HEX_HEADER_PREFIX[3])) {
                        parser = _parse_hex;
                    }
                }
                else if (ibuffer[1] === Zmodem.ZMLIB.ZDLE) {
                    //ZPAD + ZDLE should be a binary header.
                    if (ibuffer.length < BINARY16_HEADER_PREFIX.length) {
                        break TRIM_LOOP;
                    }

                    if (ibuffer[2] === BINARY16_HEADER_PREFIX[2]) {
                        parser = _parse_binary16;
                    }
                    else if (ibuffer[2] === BINARY32_HEADER_PREFIX[2]) {
                        parser = _parse_binary32;
                    }
                }

                if (!parser) {
                    garbage.push( ibuffer.shift() );
                }
            }
        }

        if (discard_all) {
            garbage.push.apply( garbage, ibuffer.splice(0) );
        }

        //For now we’ll throw away the parser.
        //It’s not hard for parse() to discern anyway.

        return garbage;
    }

    /**
     * Parse out a Header object from a given array of octet values.
     *
     * An exception is thrown if the given bytes are definitively invalid
     * as header values.
     *
     * @param {number[]} octets - The octet values to parse.
     *      Each array member should be an 8-bit unsigned integer (0-255).
     *      This object is mutated in the function.
     *
     * @returns {Header|undefined} An instance of the appropriate Header
     *      subclass, or undefined if not enough octet values are given
     *      to determine whether there is a valid header here or not.
     */
    static parse(octets) {
        var hdr;
        if (octets[1] === ZPAD) {
            hdr = _parse_hex(octets);
            return hdr && [ hdr, 16 ];
        }

        else if (octets[2] === ZBIN) {
            hdr = _parse_binary16(octets, 3);
            return hdr && [ hdr, 16 ];
        }

        else if (octets[2] === ZBIN32) {
            hdr = _parse_binary32(octets);
            return hdr && [ hdr, 32 ];
        }

        if (octets.length < 3) return;

        throw( "Unrecognized/unsupported octets: " + octets.join() );
    }

    /**
     * Build a Header subclass given a name and arguments.
     *
     * @param {string} name - The header type name, e.g., “ZRINIT”.
     *
     * @param {...*} args - The arguments to pass to the appropriate
     *      subclass constructor. These aren’t documented currently
     *      but are pretty easy to glean from the code.
     *
     * @returns {Header} An instance of the appropriate Header subclass.
     */
    static build(name /*, args */) {
        var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));

        //TODO: make this better
        var Ctr = FRAME_NAME_CREATOR[name];
        if (!Ctr) throw("No frame class “" + name + "” is defined!");

        args.shift();

        //Plegh!
        //https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
        var hdr = new (Ctr.bind.apply(Ctr, [null].concat(args)));

        return hdr;
    }

    /**
     * Return the octet values array that represents the object
     * in ZMODEM hex encoding.
     *
     * @returns {number[]} An array of octet values suitable for sending
     *      as binary data.
     */
    to_hex() {
        var to_crc = this._crc_bytes();

        return HEX_HEADER_PREFIX.concat(
            Zmodem.ENCODELIB.octets_to_hex( to_crc.concat( Zmodem.CRC.crc16(to_crc) ) ),
            this._hex_header_ending
        );
    }

    /**
     * Return the octet values array that represents the object
     * in ZMODEM binary encoding with a 16-bit CRC.
     *
     * @param {ZDLE} zencoder - A ZDLE instance to use for
     *      ZDLE encoding.
     *
     * @returns {number[]} An array of octet values suitable for sending
     *      as binary data.
     */
    to_binary16(zencoder) {
        return this._to_binary(zencoder, BINARY16_HEADER_PREFIX, Zmodem.CRC.crc16);
    }

    /**
     * Return the octet values array that represents the object
     * in ZMODEM binary encoding with a 32-bit CRC.
     *
     * @param {ZDLE} zencoder - A ZDLE instance to use for
     *      ZDLE encoding.
     *
     * @returns {number[]} An array of octet values suitable for sending
     *      as binary data.
     */
    to_binary32(zencoder) {
        return this._to_binary(zencoder, BINARY32_HEADER_PREFIX, Zmodem.CRC.crc32);
    }

    //This is never called directly, but only as super().
    constructor() {
        if (!this._bytes4) {
            this._bytes4 = [0, 0, 0, 0];
        }
    }

    _to_binary(zencoder, prefix, crc_func) {
        var to_crc = this._crc_bytes();

        //Both the 4-byte payload and the CRC bytes are ZDLE-encoded.
        var octets = prefix.concat(
            zencoder.encode( to_crc.concat( crc_func(to_crc) ) )
        );

        return octets;
    }

    _crc_bytes() {
        return [ this.TYPENUM ].concat(this._bytes4);
    }
}
Zmodem.Header.prototype._hex_header_ending = HEX_HEADER_CRLF_XON;

class ZRQINIT_HEADER extends Zmodem.Header {};

//----------------------------------------------------------------------

const ZRINIT_FLAG = {

    //----------------------------------------------------------------------
    // Bit Masks for ZRINIT flags byte ZF0
    //----------------------------------------------------------------------
    CANFDX: 0x01,  // Rx can send and receive true FDX
    CANOVIO: 0x02, // Rx can receive data during disk I/O
    CANBRK: 0x04,  // Rx can send a break signal
    CANCRY: 0x08,  // Receiver can decrypt -- nothing does this
    CANLZW: 0x10,  // Receiver can uncompress -- nothing does this
    CANFC32: 0x20, // Receiver can use 32 bit Frame Check
    ESCCTL: 0x40,  // Receiver expects ctl chars to be escaped
    ESC8: 0x80,    // Receiver expects 8th bit to be escaped
};

function _get_ZRINIT_flag_num(fl) {
    if (!ZRINIT_FLAG[fl]) {
        throw new Zmodem.Error("Invalid ZRINIT flag: " + fl);
    }
    return ZRINIT_FLAG[fl];
}

class ZRINIT_HEADER extends Zmodem.Header {
    constructor(flags_arr, bufsize) {
        super();
        var flags_num = 0;
        if (!bufsize) bufsize = 0;

        flags_arr.forEach( function(fl) {
            flags_num |= _get_ZRINIT_flag_num(fl);
        } );

        this._bytes4 = [
            bufsize & 0xff,
            bufsize >> 8,
            0,
            flags_num,
        ];
    }

    //undefined if nonstop I/O is allowed
    get_buffer_size() {
        return Zmodem.ENCODELIB.unpack_u16_be( this._bytes4.slice(0, 2) ) || undefined;
    }

    //Unimplemented:
    //  can_decrypt
    //  can_decompress

    //----------------------------------------------------------------------
    //function names taken from Jacques Mattheij’s implementation,
    //as used in syncterm.

    can_full_duplex() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.CANFDX );
    }

    can_overlap_io() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.CANOVIO );
    }

    can_break() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.CANBRK );
    }

    can_fcs_32() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.CANFC32 );
    }

    escape_ctrl_chars() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.ESCCTL );
    }

    //Is this used? I don’t see it used in lrzsz or syncterm
    //Looks like it was a “foreseen” feature that Forsberg
    //never implemented. (The need for it went away, maybe?)
    escape_8th_bit() {
        return !!( this._bytes4[3] & ZRINIT_FLAG.ESC8 );
    }
};

//----------------------------------------------------------------------

//Since context makes clear what’s going on, we use these
//rather than the T-prefixed constants in the specification.
const ZSINIT_FLAG = {
    ESCCTL: 0x40,  // Transmitter will escape ctl chars
    ESC8: 0x80,    // Transmitter will escape 8th bit
};

function _get_ZSINIT_flag_num(fl) {
    if (!ZSINIT_FLAG[fl]) {
        throw("Invalid ZSINIT flag: " + fl);
    }
    return ZSINIT_FLAG[fl];
}

class ZSINIT_HEADER extends Zmodem.Header {
    constructor( flags_arr, attn_seq_arr ) {
        super();
        var flags_num = 0;

        flags_arr.forEach( function(fl) {
            flags_num |= _get_ZSINIT_flag_num(fl);
        } );

        this._bytes4 = [ 0, 0, 0, flags_num ];

        if (attn_seq_arr) {
            if (attn_seq_arr.length > 31) {
                throw("Attn sequence must be <= 31 bytes");
            }
            if (attn_seq_arr.some( function(num) { return num > 255 } )) {
                throw("Attn sequence (" + attn_seq_arr + ") must be <256");
            }
            this._data = attn_seq_arr.concat([0]);
        }
    }

    escape_ctrl_chars() {
        return !!( this._bytes4[3] & ZSINIT_FLAG.ESCCTL );
    }

    //Is this used? I don’t see it used in lrzsz or syncterm
    escape_8th_bit() {
        return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
    }
}

//Thus far it doesn’t seem we really need this header except to respond
//to ZSINIT, which doesn’t require a payload.
class ZACK_HEADER extends Zmodem.Header {
    constructor(payload4) {
        super();

        if (payload4) {
            this._bytes4 = payload4.slice();
        }
    }
}
ZACK_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;

//----------------------------------------------------------------------

const ZFILE_VALUES = {

    //ZF3 (i.e., first byte)
    extended: {
        sparse: 0x40,   //ZXSPARS
    },

    //ZF2
    transport: [
        undefined,
        "compress",         //ZTLZW
        "encrypt",          //ZTCRYPT
        "rle",              //ZTRLE
    ],

    //ZF1
    management: [
        undefined,
        "newer_or_longer",  //ZF1_ZMNEWL
        "crc",              //ZF1_ZMCRC
        "append",           //ZF1_ZMAPND
        "clobber",          //ZF1_ZMCLOB
        "newer",            //ZF1_ZMNEW
        "mtime_or_length",  //ZF1_ZMNEW
        "protect",          //ZF1_ZMPROT
        "rename",           //ZF1_ZMPROT
    ],

    //ZF0 (i.e., last byte)
    conversion: [
        undefined,
        "binary",           //ZCBIN
        "text",             //ZCNL
        "resume",           //ZCRESUM
    ],
};

const ZFILE_ORDER = ["extended", "transport", "management", "conversion"];

const ZMSKNOLOC = 0x80,
    MANAGEMENT_MASK = 0x1f,
    ZXSPARS = 0x40
;

class ZFILE_HEADER extends Zmodem.Header {

    //TODO: allow options on instantiation
    get_options() {
        var opts = {
            sparse: !!(this._bytes4[0] & ZXSPARS),
        };

        var bytes_copy = this._bytes4.slice(0);

        ZFILE_ORDER.forEach( function(key, i) {
            if (ZFILE_VALUES[key] instanceof Array) {
                if (key === "management") {
                    opts.skip_if_absent = !!(bytes_copy[i] & ZMSKNOLOC);
                    bytes_copy[i] &= MANAGEMENT_MASK;
                }

                opts[key] = ZFILE_VALUES[key][ bytes_copy[i] ];
            }
            else {
                for (var extkey in ZFILE_VALUES[key]) {
                    opts[extkey] = !!(bytes_copy[i] & ZFILE_VALUES[key][extkey]);
                    if (opts[extkey]) {
                        bytes_copy[i] ^= ZFILE_VALUES[key][extkey]
                    }
                }
            }

            if (!opts[key] && bytes_copy[i]) {
                opts[key] = "unknown:" + bytes_copy[i];
            }
        } );

        return opts;
    }
}

//----------------------------------------------------------------------

//Empty headers - in addition to ZRQINIT
class ZSKIP_HEADER extends Zmodem.Header {}
//No need for ZNAK
class ZABORT_HEADER extends Zmodem.Header {}
class ZFIN_HEADER extends Zmodem.Header {}
class ZFERR_HEADER extends Zmodem.Header {}

ZFIN_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;

class ZOffsetHeader extends Zmodem.Header {
    constructor(offset) {
        super();
        this._bytes4 = Zmodem.ENCODELIB.pack_u32_le(offset);
    }

    get_offset() {
        return Zmodem.ENCODELIB.unpack_u32_le(this._bytes4);
    }
}

class ZRPOS_HEADER extends ZOffsetHeader {};
class ZDATA_HEADER extends ZOffsetHeader {};
class ZEOF_HEADER extends ZOffsetHeader {};

//As request, receiver creates.
/* UNIMPLEMENTED FOR NOW
class ZCRC_HEADER extends ZHeader {
    constructor(crc_le_bytes) {
        super();
        if (crc_le_bytes) {  //response, sender creates
            this._bytes4 = crc_le_bytes;
        }
    }
}
*/

//No ZCHALLENGE implementation

//class ZCOMPL_HEADER extends ZHeader {}
//class ZCAN_HEADER extends Zmodem.Header {}

//As described, this header represents an information disclosure.
//It could be interpreted, I suppose, merely as “this is how much space
//I have FOR YOU.”
//TODO: implement if needed/requested
//class ZFREECNT_HEADER extends ZmodemHeader {}

//----------------------------------------------------------------------

const FRAME_CLASS_TYPES = [
    [ ZRQINIT_HEADER, "ZRQINIT" ],
    [ ZRINIT_HEADER, "ZRINIT" ],
    [ ZSINIT_HEADER, "ZSINIT" ],
    [ ZACK_HEADER, "ZACK" ],
    [ ZFILE_HEADER, "ZFILE" ],
    [ ZSKIP_HEADER, "ZSKIP" ],
    undefined, // [ ZNAK_HEADER, "ZNAK" ],
    [ ZABORT_HEADER, "ZABORT" ],
    [ ZFIN_HEADER, "ZFIN" ],
    [ ZRPOS_HEADER, "ZRPOS" ],
    [ ZDATA_HEADER, "ZDATA" ],
    [ ZEOF_HEADER, "ZEOF" ],
    [ ZFERR_HEADER, "ZFERR" ],  //see note
    undefined, //[ ZCRC_HEADER, "ZCRC" ],
    undefined, //[ ZCHALLENGE_HEADER, "ZCHALLENGE" ],
    undefined, //[ ZCOMPL_HEADER, "ZCOMPL" ],
    undefined, //[ ZCAN_HEADER, "ZCAN" ],
    undefined, //[ ZFREECNT_HEADER, "ZFREECNT" ],
    undefined, //[ ZCOMMAND_HEADER, "ZCOMMAND" ],
    undefined, //[ ZSTDERR_HEADER, "ZSTDERR" ],
];

/*
ZFERR is described as “error in reading or writing file”. It’s really
not a good idea from a security angle for the endpoint to expose this
information. We should parse this and handle it as ZABORT but never send it.

Likewise with ZFREECNT: the sender shouldn’t ask how much space is left
on the other box; rather, the receiver should decide what to do with the
file size as the sender reports it.
*/

var FRAME_NAME_CREATOR = {};

for (var fc=0; fc<FRAME_CLASS_TYPES.length; fc++) {
    if (!FRAME_CLASS_TYPES[fc]) continue;

    FRAME_NAME_CREATOR[ FRAME_CLASS_TYPES[fc][1] ] = FRAME_CLASS_TYPES[fc][0];

    Object.assign(
        FRAME_CLASS_TYPES[fc][0].prototype,
        {
            TYPENUM: fc,
            NAME: FRAME_CLASS_TYPES[fc][1],
        }
    );
}

//----------------------------------------------------------------------

const CREATORS = [
    ZRQINIT_HEADER,
    ZRINIT_HEADER,
    ZSINIT_HEADER,
    ZACK_HEADER,
    ZFILE_HEADER,
    ZSKIP_HEADER,
    'ZNAK',
    ZABORT_HEADER,
    ZFIN_HEADER,
    ZRPOS_HEADER,
    ZDATA_HEADER,
    ZEOF_HEADER,
    ZFERR_HEADER,
    'ZCRC', //ZCRC_HEADER, -- leaving unimplemented?
    'ZCHALLENGE',
    'ZCOMPL',
    'ZCAN',
    'ZFREECNT', // ZFREECNT_HEADER,
    'ZCOMMAND',
    'ZSTDERR',
];

function _get_blank_header(typenum) {
    var creator = CREATORS[typenum];
    if (typeof(creator) === "string") {
        throw( "Received unsupported header: " + creator );
    }

    /*
    if (creator === ZCRC_HEADER) {
        return new creator([0, 0, 0, 0]);
    }
    */

    return _get_blank_header_from_constructor(creator);
}

//referenced outside TODO
function _get_blank_header_from_constructor(creator) {
    if (creator.prototype instanceof ZOffsetHeader) {
        return new creator(0);
    }

    return new creator([]);
}

function _parse_binary16(bytes_arr) {

    //The max length of a ZDLE-encoded binary header w/ 16-bit CRC is:
    //  3 initial bytes, NOT ZDLE-encoded
    //  2 typenum bytes     (1 decoded)
    //  8 data bytes        (4 decoded)
    //  4 CRC bytes         (2 decoded)

    //A 16-bit payload has 7 ZDLE-encoded octets.
    //The ZDLE-encoded octets follow the initial prefix.
    var zdle_decoded = Zmodem.ZDLE.splice( bytes_arr, BINARY16_HEADER_PREFIX.length, 7 );

    return zdle_decoded && _parse_non_zdle_binary16(zdle_decoded);
}

function _parse_non_zdle_binary16(decoded) {
    Zmodem.CRC.verify16(
        decoded.slice(0, 5),
        decoded.slice(5)
    );

    var typenum = decoded[0];
    var hdr = _get_blank_header(typenum);
    hdr._bytes4 = decoded.slice( 1, 5 );

    return hdr;
}

function _parse_binary32(bytes_arr) {

    //Same deal as with 16-bit CRC except there are two more
    //potentially ZDLE-encoded bytes, for a total of 9.
    var zdle_decoded = Zmodem.ZDLE.splice(
        bytes_arr,     //omit the leading "*", ZDLE, and "C"
        BINARY32_HEADER_PREFIX.length,
        9
    );

    if (!zdle_decoded) return;

    Zmodem.CRC.verify32(
        zdle_decoded.slice(0, 5),
        zdle_decoded.slice(5)
    );

    var typenum = zdle_decoded[0];
    var hdr = _get_blank_header(typenum);
    hdr._bytes4 = zdle_decoded.slice( 1, 5 );

    return hdr;
}

function _parse_hex(bytes_arr) {

    //A hex header always has:
    //  4 bytes for the ** . ZDLE . 'B'
    //  2 hex bytes for the header type
    //  8 hex bytes for the header content
    //  4 hex bytes for the CRC
    //  1-2 bytes for (CR/)LF
    //  (...and at this point the trailing XON is already stripped)
    //
    //----------------------------------------------------------------------
    //A carriage return and line feed are sent with HEX headers.  The
    //receive routine expects to see at least one of these characters, two
    //if the first is CR.
    //----------------------------------------------------------------------
    //
    //^^ I guess it can be either CR/LF or just LF … though those two
    //sentences appear to be saying contradictory things.

    var lf_pos = bytes_arr.indexOf( 0x8a );     //lrzsz sends this

    if (-1 === lf_pos) {
        lf_pos = bytes_arr.indexOf( 0x0a );
    }

    var hdr_err, hex_bytes;

    if (-1 === lf_pos) {
        if (bytes_arr.length > 11) {
            hdr_err = "Invalid hex header - no LF detected within 12 bytes!";
        }

        //incomplete header
        return;
    }
    else {
        hex_bytes = bytes_arr.splice( 0, lf_pos );

        //Trim off the LF
        bytes_arr.shift();

        if ( hex_bytes.length === 19 ) {

            //NB: The spec says CR but seems to treat high-bit variants
            //of control characters the same as the regulars; should we
            //also allow 0x8d?
            var preceding = hex_bytes.pop();
            if ( preceding !== 0x0d && preceding !== 0x8d ) {
                hdr_err = "Invalid hex header: (CR/)LF doesn’t have CR!";
            }
        }
        else if ( hex_bytes.length !== 18 ) {
            hdr_err = "Invalid hex header: invalid number of bytes before LF!";
        }
    }

    if (hdr_err) {
        hdr_err += " (" + hex_bytes.length + " bytes: " + hex_bytes.join() + ")";
        throw hdr_err;
    }

    hex_bytes.splice(0, 4);

    //Should be 7 bytes ultimately:
    //  1 for typenum
    //  4 for header data
    //  2 for CRC
    var octets = Zmodem.ENCODELIB.parse_hex_octets(hex_bytes);

    return _parse_non_zdle_binary16(octets);
}

Zmodem.Header.parse_hex = _parse_hex;