summaryrefslogtreecommitdiffstats
path: root/src/zdle.js
blob: 989222eb4c0ea7e1151f3a1b968b5d37a6c5e18a (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
"use strict";

var Zmodem = module.exports;

Object.assign(
    Zmodem,
    require("./zmlib")
);

//encode() variables - declare them here so we don’t
//create them in the function.
var encode_cur, encode_todo;

const ZDLE = Zmodem.ZMLIB.ZDLE;

/**
 * Class that handles ZDLE encoding and decoding.
 * Encoding is subject to a given configuration--specifically, whether
 * we want to escape all control characters. Decoding is static; however
 * a given string is encoded we can always decode it.
 */
Zmodem.ZDLE = class ZmodemZDLE {
    /**
     * Create a ZDLE encoder.
     *
     * @param {object} [config] - The initial configuration.
     * @param {object} config.escape_ctrl_chars - Whether the ZDLE encoder
     *  should escape control characters.
     */
    constructor(config) {
        this._config = {};
        if (config) {
            this.set_escape_ctrl_chars(!!config.escape_ctrl_chars);
        }
    }

    /**
     * Enable or disable control-character escaping.
     * You should probably enable this for sender sessions.
     *
     * @param {boolean} value - Whether to enable (true) or disable (false).
     */
    set_escape_ctrl_chars(value) {
        if (typeof value !== "boolean") throw "need boolean!";

        if (value !== this._config.escape_ctrl_chars) {
            this._config.escape_ctrl_chars = value;
            this._setup_zdle_table();
        }
    }

    /**
     * Whether or not control-character escaping is enabled.
     *
     * @return {boolean} Whether the escaping is on (true) or off (false).
     */
    escapes_ctrl_chars() {
        return !!this._config.escape_ctrl_chars;
    }

    //I don’t know of any Zmodem implementations that use ZESC8
    //(“escape_8th_bit”)??

    /*
    ZMODEM software escapes ZDLE, 020, 0220, 021, 0221, 023, and 0223.  If
    preceded by 0100 or 0300 (@), 015 and 0215 are also escaped to protect the
    Telenet command escape CR-@-CR.
    */

    /**
     * Encode an array of octet values and return it.
     * This will mutate the given array.
     *
     * @param {number[]} octets - The octet values to transform.
     *      Each array member should be an 8-bit unsigned integer (0-255).
     *      This object is mutated in the function.
     *
     * @returns {number[]} The passed-in array, transformed. This is the
     *  same object that is passed in.
     */
    encode(octets) {
        //NB: Performance matters here!

        if (!this._zdle_table) throw "No ZDLE encode table configured!";

        var zdle_table = this._zdle_table;

        var last_code = this._lastcode;

        var arrbuf = new ArrayBuffer( 2 * octets.length );
        var arrbuf_uint8 = new Uint8Array(arrbuf);

        var escctl_yn = this._config.escape_ctrl_chars;

        var arrbuf_i = 0;

        for (encode_cur=0; encode_cur<octets.length; encode_cur++) {

            encode_todo = zdle_table[octets[encode_cur]];
            if (!encode_todo) {
                console.trace();
                console.error("bad encode() call:", JSON.stringify(octets));
                this._lastcode = last_code;
                throw( "Invalid octet: " + octets[encode_cur] );
            }

            last_code = octets[encode_cur];

            if (encode_todo === 1) {
                //Do nothing; we append last_code below.
            }

            //0x40 = '@'; i.e., only escape if the last
            //octet was '@'.
            else if (escctl_yn || (encode_todo === 2) || ((last_code & 0x7f) === 0x40)) {
                arrbuf_uint8[arrbuf_i] = ZDLE;
                arrbuf_i++;

                last_code ^= 0x40;   //0100
            }

            arrbuf_uint8[arrbuf_i] = last_code;

            arrbuf_i++;
        }

        this._lastcode = last_code;

        octets.splice(0);
        octets.push.apply(octets, new Uint8Array( arrbuf, 0, arrbuf_i ));

        return octets;
    }

    /**
     * Decode an array of octet values and return it.
     * This will mutate the given array.
     *
     * @param {number[]} octets - The octet values to transform.
     *      Each array member should be an 8-bit unsigned integer (0-255).
     *      This object is mutated in the function.
     *
     * @returns {number[]} The passed-in array.
     *  This is the same object that is passed in.
     */
    static decode(octets) {
        for (var o=octets.length-1; o>=0; o--) {
            if (octets[o] === ZDLE) {
                octets.splice( o, 2, octets[o+1] - 64 );
            }
        }

        return octets;
    }

    /**
     * Remove, ZDLE-decode, and return bytes from the passed-in array.
     * If the requested number of ZDLE-encoded bytes isn’t available,
     * then the passed-in array is unmodified (and the return is undefined).
     *
     * @param {number[]} octets - The octet values to transform.
     *      Each array member should be an 8-bit unsigned integer (0-255).
     *      This object is mutated in the function.
     *
     * @param {number} offset - The number of (undecoded) bytes to skip
     *      at the beginning of the “octets” array.
     *
     * @param {number} count - The number of bytes (octet values) to return.
     *
     * @returns {number[]|undefined} An array with the requested number of
     *      decoded octet values, or undefined if that number of decoded
     *      octets isn’t available (given the passed-in offset).
     */
    static splice(octets, offset, count) {
        var so_far = 0;

        if (!offset) offset = 0;

        for (var i = offset; i<octets.length && so_far<count; i++) {
            so_far++;

            if (octets[i] === ZDLE) i++;
        }

        if (so_far === count) {

            //Don’t accept trailing ZDLE. This check works
            //because of the i++ logic above.
            if (octets.length === (i - 1)) return;

            octets.splice(0, offset);
            return ZmodemZDLE.decode( octets.splice(0, i - offset) );
        }

        return;
    }

    _setup_zdle_table() {
        var zsendline_tab = new Array(256);
        for (var i=0; i<zsendline_tab.length; i++) {

            //1 = never escape
            //2 = always escape
            //3 = escape only if the previous byte was '@'

            //Never escape characters from 0x20 (32) to 0x7f (127).
            //This is the range of printable characters, plus DEL.
            //I guess ZMODEM doesn’t consider DEL to be a control character?
            if ( i & 0x60 ) {
                zsendline_tab[i] = 1;
            }
            else {
                switch(i) {
                    case ZDLE:  //NB: no (ZDLE | 0x80)
                    case Zmodem.ZMLIB.XOFF:
                    case Zmodem.ZMLIB.XON:
                    case (Zmodem.ZMLIB.XOFF | 0x80):
                    case (Zmodem.ZMLIB.XON | 0x80):
                        zsendline_tab[i] = 2;
                        break;

                    case 0x10:  // 020
                    case 0x90:  // 0220
                        zsendline_tab[i] = this._config.turbo_escape ? 1 : 2;
                        break;

                    case 0x0d:  // 015
                    case 0x8d:  // 0215
                        zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : !this._config.turbo_escape ? 3 : 1;
                        break;

                    default:
                        zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : 1;
                }
            }
        }

        this._zdle_table = zsendline_tab;
    }
}