Initial commit
This commit is contained in:
570
ui/stream/reader.js
Normal file
570
ui/stream/reader.js
Normal file
@@ -0,0 +1,570 @@
|
||||
// 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user