571 lines
12 KiB
JavaScript
571 lines
12 KiB
JavaScript
// Sshwifty - A Web SSH client
|
|
//
|
|
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import Exception from "./exception.js";
|
|
import * as subscribe from "./subscribe.js";
|
|
|
|
export class Buffer {
|
|
/**
|
|
* constructor
|
|
*
|
|
* @param {Uint8Array} buffer Array buffer
|
|
* @param {function} depleted Callback that will be called when the buffer
|
|
* is depleted
|
|
*/
|
|
constructor(buffer, depleted) {
|
|
this.buffer = buffer;
|
|
this.used = 0;
|
|
this.onDepleted = depleted;
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
* @param {number} maxLen Max search length
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
searchBuffer(byteData, maxLen) {
|
|
let searchLen = this.remains();
|
|
|
|
if (searchLen > maxLen) {
|
|
searchLen = maxLen;
|
|
}
|
|
|
|
for (let i = 0; i < searchLen; i++) {
|
|
if (this.buffer[i + this.used] !== byteData) {
|
|
continue;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
indexOf(byteData) {
|
|
return this.searchBuffer(byteData, this.remains());
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes in the source + buffer is still available to be
|
|
* read, return 0 when reader is depleted and thus can be ditched
|
|
*
|
|
* @returns {number} Remaining size
|
|
*
|
|
*/
|
|
remains() {
|
|
return this.buffer.length - this.used;
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes is still availale in the buffer.
|
|
*
|
|
* Note: This reader don't have renewable data source, so when buffer
|
|
* depletes, the reader is done
|
|
*
|
|
* @returns {number} Remaining size
|
|
*
|
|
*/
|
|
buffered() {
|
|
return this.remains();
|
|
}
|
|
|
|
/**
|
|
* Export max n bytes from current buffer
|
|
*
|
|
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
|
* if current buffer is deplated
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
* @throws {Exception} When reader has been depleted
|
|
*
|
|
*/
|
|
export(n) {
|
|
let remain = this.remains();
|
|
|
|
if (remain <= 0) {
|
|
throw new Exception("Reader has been depleted", false);
|
|
}
|
|
|
|
if (remain > n) {
|
|
remain = n;
|
|
}
|
|
|
|
let exported = this.buffer.slice(this.used, this.used + remain);
|
|
this.used += exported.length;
|
|
|
|
if (this.remains() <= 0) {
|
|
this.onDepleted();
|
|
}
|
|
|
|
return exported;
|
|
}
|
|
}
|
|
|
|
export class Multiple {
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {function} depleted Callback will be called when all reader is
|
|
* depleted
|
|
*
|
|
*/
|
|
constructor(depleted) {
|
|
this.reader = null;
|
|
this.depleted = depleted;
|
|
this.subscribe = new subscribe.Subscribe();
|
|
this.closed = false;
|
|
}
|
|
|
|
/**
|
|
* Add new reader as sub reader
|
|
*
|
|
* @param {Buffer} reader
|
|
* @param {function} depleted Callback that will be called when given reader
|
|
* is depleted
|
|
*
|
|
* @throws {Exception} When the reader is closed
|
|
*
|
|
*/
|
|
feed(reader, depleted) {
|
|
if (this.closed) {
|
|
throw new Exception("Reader is closed", false);
|
|
}
|
|
|
|
if (this.reader === null && this.subscribe.pendings() <= 0) {
|
|
this.reader = {
|
|
reader: reader,
|
|
depleted: depleted
|
|
};
|
|
|
|
return;
|
|
}
|
|
|
|
this.subscribe.resolve({
|
|
reader: reader,
|
|
depleted: depleted
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
* @param {number} maxLen Max search length
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*
|
|
*/
|
|
searchBuffer(byteData, maxLen) {
|
|
if (this.reader === null) {
|
|
return -1;
|
|
}
|
|
|
|
return this.reader.reader.searchBuffer(byteData, maxLen);
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
indexOf(byteData) {
|
|
return this.searchBuffer(byteData, this.buffered());
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes still available in the buffer (How many bytes of
|
|
* buffer is left for read before reloading from data source)
|
|
*
|
|
* @returns {number} How many bytes left in the current buffer
|
|
*/
|
|
buffered() {
|
|
if (this.reader == null) {
|
|
return 0;
|
|
}
|
|
|
|
return this.reader.reader.buffered();
|
|
}
|
|
|
|
/**
|
|
* close current reading
|
|
*
|
|
*/
|
|
close() {
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
|
|
this.closed = true;
|
|
this.subscribe.reject(new Exception("Reader is closed", false));
|
|
}
|
|
|
|
/**
|
|
* Export max n bytes from current buffer
|
|
*
|
|
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
|
* if current buffer is deplated
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
async export(n) {
|
|
for (;;) {
|
|
if (this.reader !== null) {
|
|
let exported = await this.reader.reader.export(n);
|
|
|
|
if (this.reader.reader.remains() <= 0) {
|
|
this.reader.depleted();
|
|
|
|
this.reader = null;
|
|
}
|
|
|
|
return exported;
|
|
}
|
|
|
|
this.depleted(this);
|
|
|
|
this.reader = await this.subscribe.subscribe();
|
|
}
|
|
}
|
|
}
|
|
|
|
export class Reader {
|
|
/**
|
|
* constructor
|
|
*
|
|
* @param {Multiple} multiple Source reader
|
|
* @param {function} bufferConverter Function convert
|
|
*
|
|
*/
|
|
constructor(multiple, bufferConverter) {
|
|
this.multiple = multiple;
|
|
this.buffers = new subscribe.Subscribe();
|
|
this.bufferConverter =
|
|
bufferConverter ||
|
|
(d => {
|
|
return d;
|
|
});
|
|
this.closed = false;
|
|
}
|
|
|
|
/**
|
|
* Add buffer into current reader
|
|
*
|
|
* @param {Uint8Array} buffer buffer to add
|
|
*
|
|
* @throws {Exception} When the reader is closed
|
|
*
|
|
*/
|
|
feed(buffer) {
|
|
if (this.closed) {
|
|
throw new Exception("Reader is closed, new data has been deined", false);
|
|
}
|
|
|
|
this.buffers.resolve(buffer);
|
|
}
|
|
|
|
async reader() {
|
|
if (this.closed) {
|
|
throw new Exception("Reader is closed, unable to read", false);
|
|
}
|
|
|
|
if (this.multiple.buffered() > 0) {
|
|
return this.multiple;
|
|
}
|
|
|
|
let self = this,
|
|
converted = await this.bufferConverter(await self.buffers.subscribe());
|
|
|
|
this.multiple.feed(new Buffer(converted, () => {}), () => {});
|
|
|
|
return this.multiple;
|
|
}
|
|
|
|
/**
|
|
* close current reading
|
|
*
|
|
*/
|
|
close() {
|
|
if (this.closed) {
|
|
return;
|
|
}
|
|
|
|
this.closed = true;
|
|
this.buffers.reject(
|
|
new Exception(
|
|
"Reader is closed, and thus " + "cannot be operated on",
|
|
false
|
|
)
|
|
);
|
|
|
|
return this.multiple.close();
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
* @param {number} maxLen Max search length
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
async searchBuffer(byteData, maxLen) {
|
|
return (await this.reader()).searchBuffer(byteData, maxLen);
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
async indexOf(byteData) {
|
|
return (await this.reader()).indexOf(byteData);
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes still available in the buffer (How many bytes of
|
|
* buffer is left for read before reloading from data source)
|
|
*
|
|
* @returns {number} How many bytes left in the current buffer
|
|
*/
|
|
async buffered() {
|
|
return (await this.reader()).buffered();
|
|
}
|
|
|
|
/**
|
|
* Export max n bytes from current buffer
|
|
*
|
|
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
|
* if current buffer is deplated
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
async export(n) {
|
|
return (await this.reader()).export(n);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read exactly one bytes from the reader
|
|
*
|
|
* @param {Reader} reader the source reader
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
export async function readOne(reader) {
|
|
for (;;) {
|
|
let d = await reader.export(1);
|
|
|
|
if (d.length <= 0) {
|
|
continue;
|
|
}
|
|
|
|
return d;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read exactly n bytes from the reader
|
|
*
|
|
* @param {Reader} reader the source reader
|
|
* @param {number} n length to read
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
export async function readN(reader, n) {
|
|
let readed = 0,
|
|
result = new Uint8Array(n);
|
|
|
|
while (readed < n) {
|
|
let exported = await reader.export(n - readed);
|
|
|
|
result.set(exported, readed);
|
|
readed += exported.length;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export class Limited {
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {Reader} reader the source reader
|
|
* @param {number} maxN max bytes to read
|
|
*
|
|
* @returns {boolean} true when the reader is completed, false otherwise
|
|
*
|
|
*/
|
|
constructor(reader, maxN) {
|
|
this.reader = reader;
|
|
this.remain = maxN;
|
|
}
|
|
|
|
/**
|
|
* Indicate whether or not the current reader is completed
|
|
*
|
|
* @returns {boolean} true when the reader is completed, false otherwise
|
|
*
|
|
*/
|
|
completed() {
|
|
return this.remain <= 0;
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current available (unused) read
|
|
* buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
* @param {number} maxLen Max search length
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*
|
|
*/
|
|
searchBuffer(byteData, maxLen) {
|
|
return this.reader.searchBuffer(
|
|
byteData,
|
|
maxLen > this.remain ? this.remain : maxLen
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the index of given byte inside current read buffer
|
|
*
|
|
* @param {number} byteData Target data
|
|
*
|
|
* @returns {number} Return number >= 0 when found, -1 when not
|
|
*/
|
|
indexOf(byteData) {
|
|
return this.reader.searchBuffer(byteData, this.remain);
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes still available to be read
|
|
*
|
|
* @returns {number} Remaining size
|
|
*
|
|
*/
|
|
remains() {
|
|
return this.remain;
|
|
}
|
|
|
|
/**
|
|
* Return how many bytes still available in the buffer (How many bytes of
|
|
* buffer is left for read before reloading from data source)
|
|
*
|
|
* @returns {number} Remaining size
|
|
*
|
|
*/
|
|
buffered() {
|
|
let buf = this.reader.buffered();
|
|
|
|
return buf > this.remain ? this.remain : buf;
|
|
}
|
|
|
|
/**
|
|
* Export max n bytes from current buffer
|
|
*
|
|
* @param {number} n suggested max length
|
|
*
|
|
* @throws {Exception} when reading already completed
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
async export(n) {
|
|
if (this.completed()) {
|
|
throw new Exception("Reader already completed", false);
|
|
}
|
|
|
|
let toRead = n > this.remain ? this.remain : n,
|
|
exported = await this.reader.export(toRead);
|
|
|
|
this.remain -= exported.length;
|
|
|
|
return exported;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the whole Limited reader and return the result
|
|
*
|
|
* @param {Limited} limited the Limited reader
|
|
*
|
|
* @returns {Uint8Array} Exported data
|
|
*
|
|
*/
|
|
export async function readCompletely(limited) {
|
|
return await readN(limited, limited.remains());
|
|
}
|
|
|
|
/**
|
|
* Read until given byteData is reached. This function is guaranteed to spit
|
|
* out at least one byte
|
|
*
|
|
* @param {Reader} indexOfReader
|
|
* @param {number} byteData
|
|
*/
|
|
export async function readUntil(indexOfReader, byteData) {
|
|
let pos = await indexOfReader.indexOf(byteData),
|
|
buffered = await indexOfReader.buffered();
|
|
|
|
if (pos >= 0) {
|
|
return {
|
|
data: await readN(indexOfReader, pos + 1),
|
|
found: true
|
|
};
|
|
}
|
|
|
|
if (buffered <= 0) {
|
|
let d = await readOne(indexOfReader);
|
|
|
|
return {
|
|
data: d,
|
|
found: d[0] === byteData
|
|
};
|
|
}
|
|
|
|
return {
|
|
data: await readN(indexOfReader, buffered),
|
|
found: false
|
|
};
|
|
}
|