Added a very complicated AsyncIterableReader class.
The class was already implemented in "https://deno.land/std@0.144.0/streams/conversion.ts". So, I litterally lost 1 day of work! I hate myself.
This commit is contained in:
1 parent
17a1b4bed3
commit
0d627c9051
125 files changed
+26942
-82
No files matched your search
+32
-9
@@ -1,7 +1,7 @@
|
||||
import { sha1 } from 'https://denopkg.com/chiefbiiko/sha1/mod.ts';
|
||||
import { iterateReader } from "https://deno.land/std@0.191.0/streams/iterate_reader.ts";
|
||||
import { AsyncIterableReader, concatUint8Array } from "./utils.ts";
|
||||
|
||||
import { readAll } from "https://deno.land/std@0.191.0/streams/read_all.ts";
|
||||
import { readerFromIterable } from "https://deno.land/std@0.144.0/streams/conversion.ts";
|
||||
|
||||
export interface FileService {
|
||||
|
||||
@@ -15,11 +15,15 @@ export interface FileService {
|
||||
|
||||
|
||||
getFullBodyByPath(path: string): Promise<string | null>;
|
||||
|
||||
getRawBodyByPath(path: string): Promise<Uint8Array | null>;
|
||||
|
||||
getBodyAsReader(path: string): Promise<Deno.Reader | null>;
|
||||
|
||||
getMetaDataByID(id: string): Promise<FileMetaData | null>;
|
||||
|
||||
renameById(id: string, newPath: string): Promise<FileMetaData | null>;
|
||||
|
||||
renameByPath(
|
||||
originalPath: string,
|
||||
newPath: string,
|
||||
@@ -101,8 +105,7 @@ export class DenoKVBlobService {
|
||||
return size;
|
||||
}
|
||||
|
||||
async getBlob(index: string): Promise<Uint8Array> {
|
||||
let ret = new Uint8Array();
|
||||
private async *iterBlob(index: string): AsyncIterable<Uint8Array> {
|
||||
for await (
|
||||
const itm of this.kv.list<string>({ prefix: ['blob_index', index] })
|
||||
) {
|
||||
@@ -112,9 +115,16 @@ export class DenoKVBlobService {
|
||||
`Inconsistant blob index for key "${foo.value}", this is very bad.`,
|
||||
);
|
||||
}
|
||||
ret = concatUint8Array(ret, foo.value);
|
||||
yield foo.value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async readAllBlob(index: string): Promise<Uint8Array> {
|
||||
return await readAll(this.getBlob(index));
|
||||
}
|
||||
|
||||
getBlob(index: string): Deno.Reader{
|
||||
return readerFromIterable(this.iterBlob(index));
|
||||
}
|
||||
|
||||
private async *iterRawBlobs(index: string): AsyncIterable<Uint8Array>{
|
||||
@@ -127,10 +137,9 @@ export class DenoKVBlobService {
|
||||
);
|
||||
}
|
||||
yield foo.value;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
async getNumberOfBlobs(index: string): Promise<number> {
|
||||
let ret = 0;
|
||||
for await (
|
||||
@@ -142,7 +151,8 @@ export class DenoKVBlobService {
|
||||
}
|
||||
|
||||
streamBlob(index: string): Deno.Reader{
|
||||
return new AsyncIterableReader(this.iterRawBlobs(index));
|
||||
|
||||
return readerFromIterable(this.iterRawBlobs(index));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +243,18 @@ export class DenoKVFileService implements FileService {
|
||||
}
|
||||
|
||||
async getRawBodyByPath(path: string): Promise<Uint8Array | null> {
|
||||
const idx = await this.kv.get<SerializedFilePathIndex>([
|
||||
'file_index_path',
|
||||
path,
|
||||
]);
|
||||
if (idx.value === null) {
|
||||
return null;
|
||||
}
|
||||
return this.blobService.readAllBlob(idx.value.fileId);
|
||||
}
|
||||
|
||||
|
||||
async getBodyAsReader(path: string): Promise<Deno.Reader | null> {
|
||||
const idx = await this.kv.get<SerializedFilePathIndex>([
|
||||
'file_index_path',
|
||||
path,
|
||||
@@ -241,6 +263,7 @@ export class DenoKVFileService implements FileService {
|
||||
return null;
|
||||
}
|
||||
return this.blobService.getBlob(idx.value.fileId);
|
||||
|
||||
}
|
||||
|
||||
async getMetaDataByID(id: string): Promise<FileMetaData | null> {
|
||||
|
||||
+14
-3
@@ -2,7 +2,9 @@ import { Context, Router } from "https://deno.land/x/oak/mod.ts";
|
||||
import { FileService } from "./fileservice.ts";
|
||||
import { readerFromStreamReader } from "https://deno.land/std@0.189.0/streams/mod.ts";
|
||||
import { asyncIterableToArray } from "./utils.ts";
|
||||
|
||||
import { StringReader } from "https://deno.land/std@0.178.0/io/string_reader.ts";
|
||||
import { isReader } from "https://deno.land/x/oak@v12.5.0/util.ts";
|
||||
import { createMockNext } from "https://deno.land/x/oak@v12.1.0/testing.ts";
|
||||
|
||||
|
||||
type Handler = (ctx: Context) => void;
|
||||
@@ -30,10 +32,19 @@ function createFileHandlerFactory(fileService: FileService): Handler {
|
||||
|
||||
|
||||
function getFileHandlerFactory(fileService: FileService): Handler {
|
||||
return (ctx: Context) => {
|
||||
return async (ctx: Context) => {
|
||||
const untypedCtx: any = ctx;
|
||||
const fileId = untypedCtx.params.file_id;
|
||||
// ctx.response.body = await fileService.getRawBodyByPath()
|
||||
const reader = await fileService.getBodyAsReader(fileId);
|
||||
if(reader == null){
|
||||
ctx.response.status = 404;
|
||||
return;
|
||||
}
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = reader;
|
||||
ctx.response.type = "text/plain";
|
||||
// ctx.send(reader)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -15,27 +15,30 @@ export function concatUint8Array(arr1: Uint8Array, arr2: Uint8Array): Uint8Array
|
||||
}
|
||||
|
||||
export class AsyncIterableReader implements Deno.Reader {
|
||||
private readonly source: AsyncIterable<Uint8Array>;
|
||||
private readonly source: AsyncIterator<Uint8Array>;
|
||||
private interReadBuffer = new Uint8Array();
|
||||
private isSourceEmpty = false;
|
||||
private shit = 0;
|
||||
constructor(source: AsyncIterable<Uint8Array>) {
|
||||
this.source = source;
|
||||
this.source = source[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
private async readMinimumOf(n: number): Promise<Uint8Array> {
|
||||
let ret = new Uint8Array();
|
||||
console.log('))) ' + n )
|
||||
if(this.isSourceEmpty){
|
||||
return ret;
|
||||
}
|
||||
let ite = await this.source.next();
|
||||
while(!ite.done){
|
||||
const piece = ite.value;
|
||||
|
||||
for await(const piece of this.source){
|
||||
console.log(piece.length)
|
||||
ret = concatUint8Array(ret, piece);
|
||||
if(ret.length >= n){
|
||||
console.log('*******************************')
|
||||
return ret;
|
||||
}
|
||||
ite = await this.source.next();
|
||||
|
||||
}
|
||||
console.log('---------------');
|
||||
console.log(this.source);
|
||||
this.isSourceEmpty = true;
|
||||
return ret;
|
||||
}
|
||||
@@ -55,14 +58,18 @@ export class AsyncIterableReader implements Deno.Reader {
|
||||
}
|
||||
|
||||
async read(buffer: Uint8Array): Promise<number | null> {
|
||||
this.shit += 1;
|
||||
console.log("reading \> " + this.shit);
|
||||
if(this.isSourceEmpty && this.interReadBuffer.length < 1){
|
||||
console.log("done mother fucker");
|
||||
return null;
|
||||
}
|
||||
const internalReadBufferTransferdSize = this.readFromInternalReadBuffer(buffer);
|
||||
if(internalReadBufferTransferdSize == buffer.length){
|
||||
return internalReadBufferTransferdSize;
|
||||
}
|
||||
|
||||
const data = await this.readMinimumOf(buffer.length - internalReadBufferTransferdSize);
|
||||
const transferedSize = this.transferData(data.subarray(internalReadBufferTransferdSize), buffer);
|
||||
const transferedSize = this.transferData(data, buffer.subarray(internalReadBufferTransferdSize));
|
||||
this.interReadBuffer = data.subarray(transferedSize);
|
||||
return internalReadBufferTransferdSize + transferedSize;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Deno.test("DenoKVBlobService happy path", async (t) => {
|
||||
|
||||
await t.step("read write small text", async () => {
|
||||
const size = await bs.persistBlob("0002", toUint8Array("hello world"));
|
||||
const body = await bs.getBlob("0002");
|
||||
const body = await bs.readAllBlob("0002");
|
||||
assertEquals(body, toUint8Array("hello world"));
|
||||
assertEquals(await bs.getNumberOfBlobs("0002"), 1);
|
||||
assertEquals(size, "hello world".length);
|
||||
@@ -32,7 +32,7 @@ Deno.test("DenoKVBlobService happy path", async (t) => {
|
||||
await t.step("read write large text", async () => {
|
||||
const largeText = toUint8Array(generateLargeText(33, 100));
|
||||
const size = await bs.persistBlob("0001", largeText);
|
||||
const body = await bs.getBlob("0001");
|
||||
const body = await bs.readAllBlob("0001");
|
||||
assertEquals(body, largeText);
|
||||
assertEquals(await bs.getNumberOfBlobs("0001"), 21);
|
||||
assertEquals(size, largeText.length);
|
||||
|
||||
+6
-1
@@ -1,12 +1,17 @@
|
||||
|
||||
|
||||
export function* randomNumbers(seed: number, max: number) : Generator<number> {
|
||||
export function* randomNumbers(seed: number, max: number, maxResult: number|undefined = undefined) : Generator<number> {
|
||||
const a = 48271;
|
||||
const m = 2147483647;
|
||||
max = Math.floor(max);
|
||||
let i = 0;
|
||||
while (true) {
|
||||
seed = (a * seed) % m;
|
||||
yield seed % max;
|
||||
i ++;
|
||||
if(maxResult !== undefined && i >= maxResult){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-57
@@ -12,72 +12,33 @@ Deno.test("AsyncIterableReader", async (t) => {
|
||||
yield Promise.resolve(new Uint8Array([8, 9]));
|
||||
}
|
||||
|
||||
async function* largeSource(size: number): AsyncIterable<Uint8Array>{
|
||||
async function* largeSource(size: number, seed = 77): AsyncIterable<Uint8Array>{
|
||||
|
||||
let val = 0
|
||||
const random = randomNumbers(77, size / 4);
|
||||
const random = randomNumbers(seed, size > 10 ? size / 4: size);
|
||||
while(val < size){
|
||||
const newVal = Math.min(val + random.next().value, size);
|
||||
// console.log(range(val, newVal));
|
||||
console.log("<<<<<<<<<<<<<<<<< " + newVal + "/" + size + " ( " + (newVal - val) )
|
||||
yield Promise.resolve(new Uint8Array(range(val, newVal)));
|
||||
|
||||
const randomValue = random.next().value;
|
||||
const newVal = Math.min(val + randomValue, size);
|
||||
const ret = new Uint8Array(range(val, newVal));
|
||||
yield Promise.resolve(ret);
|
||||
val = newVal;
|
||||
|
||||
|
||||
}
|
||||
yield Promise.resolve(new Uint8Array([9]));
|
||||
console.log(">>>>>>>>>>>>>>>>>>>>>>> ");
|
||||
console.log(">>>>>>>>>>>>>>>>>>>>>>> ");
|
||||
console.log(">>>>>>>>>>>>>>>>>>>>>>> ");
|
||||
console.log(">>>>>>>>>>>>>>>>>>>>>>> ");
|
||||
}
|
||||
|
||||
// await t.step('read all mini source', async () => {
|
||||
// const reader = new AsyncIterableReader(miniSource());
|
||||
// const expt = new Uint8Array([1, 2 ,3 ,4 ,5, 6,7,8,9])
|
||||
// assertEquals(await readAll(reader), expt);
|
||||
// });
|
||||
await t.step('read all mini source', async () => {
|
||||
const reader = new AsyncIterableReader(miniSource());
|
||||
const expt = new Uint8Array([1, 2 ,3 ,4 ,5, 6,7,8,9])
|
||||
assertEquals(await readAll(reader), expt);
|
||||
});
|
||||
|
||||
await t.step('largeSource', async () =>{
|
||||
const source = largeSource(32920);
|
||||
for await(const _piece of source){
|
||||
//
|
||||
await t.step('read all large source', async () => {
|
||||
// let's run the test 1000 times to make sure implementation is ok.
|
||||
for(const i of randomNumbers(666, 100 * 1024, 1000)){
|
||||
const reader = new AsyncIterableReader(largeSource(i));
|
||||
const ret = await readAll(reader);
|
||||
assertEquals(ret.length, i);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await t.step('read all large source', async () => {
|
||||
// for(let i = 101; i < (32 * 1024 * 50 + 101); i += 1){
|
||||
let n = 32 * 1024 * 50 + 101 // 32920; //32 * 1024 * 2;
|
||||
n = 32919
|
||||
const reader = new AsyncIterableReader(largeSource(n));
|
||||
const ret = await readAll(reader);
|
||||
assertEquals(ret.length, n);
|
||||
// }
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
// <<<<<<<<<<<<<<<<< 5137/32920
|
||||
// <<<<<<<<<<<<<<<<< 11593/32920
|
||||
// <<<<<<<<<<<<<<<<< 12563/32920
|
||||
// <<<<<<<<<<<<<<<<< 15976/32920
|
||||
// <<<<<<<<<<<<<<<<< 19155/32920
|
||||
// <<<<<<<<<<<<<<<<< 20038/32920
|
||||
// <<<<<<<<<<<<<<<<< 26452/32920
|
||||
// <<<<<<<<<<<<<<<<< 31797/32920
|
||||
// <<<<<<<<<<<<<<<<< 31994/32920
|
||||
// <<<<<<<<<<<<<<<<< 32912/32920
|
||||
// <<<<<<<<<<<<<<<<< 32920/32920
|
||||
|
||||
|
||||
// <<<<<<<<<<<<<<<<< 5137/32920
|
||||
// <<<<<<<<<<<<<<<<< 11593/32920
|
||||
// <<<<<<<<<<<<<<<<< 12563/32920
|
||||
// <<<<<<<<<<<<<<<<< 15976/32920
|
||||
// <<<<<<<<<<<<<<<<< 19155/32920
|
||||
// <<<<<<<<<<<<<<<<< 20038/32920
|
||||
// <<<<<<<<<<<<<<<<< 26452/32920
|
||||
// <<<<<<<<<<<<<<<<< 31797/32920
|
||||
// <<<<<<<<<<<<<<<<< 31994/32920
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
export class DenoStdInternalError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "DenoStdInternalError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Make an assertion, if not `true`, then throw. */
|
||||
export function assert(expr: unknown, msg = ""): asserts expr {
|
||||
if (!expr) {
|
||||
throw new DenoStdInternalError(msg);
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* An abstraction of multiple Uint8Arrays
|
||||
*/
|
||||
export class BytesList {
|
||||
#len = 0;
|
||||
#chunks: {
|
||||
value: Uint8Array;
|
||||
start: number; // start offset from head of chunk
|
||||
end: number; // end offset from head of chunk
|
||||
offset: number; // offset of head in all bytes
|
||||
}[] = [];
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Total size of bytes
|
||||
*/
|
||||
size() {
|
||||
return this.#len;
|
||||
}
|
||||
/**
|
||||
* Push bytes with given offset infos
|
||||
*/
|
||||
add(value: Uint8Array, start = 0, end = value.byteLength) {
|
||||
if (value.byteLength === 0 || end - start === 0) {
|
||||
return;
|
||||
}
|
||||
checkRange(start, end, value.byteLength);
|
||||
this.#chunks.push({
|
||||
value,
|
||||
end,
|
||||
start,
|
||||
offset: this.#len,
|
||||
});
|
||||
this.#len += end - start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop head `n` bytes.
|
||||
*/
|
||||
shift(n: number) {
|
||||
if (n === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.#len <= n) {
|
||||
this.#chunks = [];
|
||||
this.#len = 0;
|
||||
return;
|
||||
}
|
||||
const idx = this.getChunkIndex(n);
|
||||
this.#chunks.splice(0, idx);
|
||||
const [chunk] = this.#chunks;
|
||||
if (chunk) {
|
||||
const diff = n - chunk.offset;
|
||||
chunk.start += diff;
|
||||
}
|
||||
let offset = 0;
|
||||
for (const chunk of this.#chunks) {
|
||||
chunk.offset = offset;
|
||||
offset += chunk.end - chunk.start;
|
||||
}
|
||||
this.#len = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find chunk index in which `pos` locates by binary-search
|
||||
* returns -1 if out of range
|
||||
*/
|
||||
getChunkIndex(pos: number): number {
|
||||
let max = this.#chunks.length;
|
||||
let min = 0;
|
||||
while (true) {
|
||||
const i = min + Math.floor((max - min) / 2);
|
||||
if (i < 0 || this.#chunks.length <= i) {
|
||||
return -1;
|
||||
}
|
||||
const { offset, start, end } = this.#chunks[i];
|
||||
const len = end - start;
|
||||
if (offset <= pos && pos < offset + len) {
|
||||
return i;
|
||||
} else if (offset + len <= pos) {
|
||||
min = i + 1;
|
||||
} else {
|
||||
max = i - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexed byte from chunks
|
||||
*/
|
||||
get(i: number): number {
|
||||
if (i < 0 || this.#len <= i) {
|
||||
throw new Error("out of range");
|
||||
}
|
||||
const idx = this.getChunkIndex(i);
|
||||
const { value, offset, start } = this.#chunks[idx];
|
||||
return value[start + i - offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator of bytes from given position
|
||||
*/
|
||||
*iterator(start = 0): IterableIterator<number> {
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
if (startIdx < 0) return;
|
||||
const first = this.#chunks[startIdx];
|
||||
let firstOffset = start - first.offset;
|
||||
for (let i = startIdx; i < this.#chunks.length; i++) {
|
||||
const chunk = this.#chunks[i];
|
||||
for (let j = chunk.start + firstOffset; j < chunk.end; j++) {
|
||||
yield chunk.value[j];
|
||||
}
|
||||
firstOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns subset of bytes copied
|
||||
*/
|
||||
slice(start: number, end: number = this.#len): Uint8Array {
|
||||
if (end === start) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
checkRange(start, end, this.#len);
|
||||
const result = new Uint8Array(end - start);
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
const endIdx = this.getChunkIndex(end - 1);
|
||||
let written = 0;
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
const chunk = this.#chunks[i];
|
||||
const len = chunk.end - chunk.start;
|
||||
result.set(chunk.value.subarray(chunk.start, chunk.end), written);
|
||||
written += len;
|
||||
}
|
||||
const last = this.#chunks[endIdx];
|
||||
const rest = end - start - written;
|
||||
result.set(last.value.subarray(last.start, last.start + rest), written);
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Concatenate chunks into single Uint8Array copied.
|
||||
*/
|
||||
concat(): Uint8Array {
|
||||
const result = new Uint8Array(this.#len);
|
||||
let sum = 0;
|
||||
for (const { value, start, end } of this.#chunks) {
|
||||
result.set(value.subarray(start, end), sum);
|
||||
sum += end - start;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function checkRange(start: number, end: number, len: number) {
|
||||
if (start < 0 || len < start || end < 0 || len < end || end < start) {
|
||||
throw new Error("invalid range");
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Check whether binary arrays are equal to each other using 8-bit comparisons.
|
||||
* @private
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
export function equalsNaive(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Check whether binary arrays are equal to each other using 32-bit comparisons.
|
||||
* @private
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
export function equals32Bit(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
const len = a.length;
|
||||
const compressable = Math.floor(len / 4);
|
||||
const compressedA = new Uint32Array(a.buffer, 0, compressable);
|
||||
const compressedB = new Uint32Array(b.buffer, 0, compressable);
|
||||
for (let i = compressable * 4; i < len; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
for (let i = 0; i < compressedA.length; i++) {
|
||||
if (compressedA[i] !== compressedB[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Check whether binary arrays are equal to each other.
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
export function equals(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length < 1000) return equalsNaive(a, b);
|
||||
return equals32Bit(a, b);
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Provides helper functions to manipulate `Uint8Array` byte slices that are not
|
||||
* included on the `Uint8Array` prototype.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Returns the index of the first occurrence of the needle array in the source
|
||||
* array, or -1 if it is not present.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the start of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.lenth * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { indexOfNeedle } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(indexOfNeedle(source, needle)); // 1
|
||||
* console.log(indexOfNeedle(source, needle, 2)); // 3
|
||||
* ```
|
||||
*/
|
||||
export function indexOfNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = 0,
|
||||
): number {
|
||||
if (start >= source.length) {
|
||||
return -1;
|
||||
}
|
||||
if (start < 0) {
|
||||
start = Math.max(0, source.length + start);
|
||||
}
|
||||
const s = needle[0];
|
||||
for (let i = start; i < source.length; i++) {
|
||||
if (source[i] !== s) continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j++;
|
||||
if (source[j] !== needle[j - pin]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Returns the index of the last occurrence of the needle array in the source
|
||||
* array, or -1 if it is not present.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the end of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.lenth * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { lastIndexOfNeedle } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(lastIndexOfNeedle(source, needle)); // 5
|
||||
* console.log(lastIndexOfNeedle(source, needle, 4)); // 3
|
||||
* ```
|
||||
*/
|
||||
export function lastIndexOfNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = source.length - 1,
|
||||
): number {
|
||||
if (start < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (start >= source.length) {
|
||||
start = source.length - 1;
|
||||
}
|
||||
const e = needle[needle.length - 1];
|
||||
for (let i = start; i >= 0; i--) {
|
||||
if (source[i] !== e) continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j--;
|
||||
if (source[j] !== needle[needle.length - 1 - (pin - j)]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin - needle.length + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Returns true if the prefix array appears at the start of the source array,
|
||||
* false otherwise.
|
||||
*
|
||||
* The complexity of this function is O(prefix.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { startsWith } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const prefix = new Uint8Array([0, 1, 2]);
|
||||
* console.log(startsWith(source, prefix)); // true
|
||||
* ```
|
||||
*/
|
||||
export function startsWith(source: Uint8Array, prefix: Uint8Array): boolean {
|
||||
for (let i = 0, max = prefix.length; i < max; i++) {
|
||||
if (source[i] !== prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns true if the suffix array appears at the end of the source array,
|
||||
* false otherwise.
|
||||
*
|
||||
* The complexity of this function is O(suffix.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { endsWith } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const suffix = new Uint8Array([1, 2, 3]);
|
||||
* console.log(endsWith(source, suffix)); // true
|
||||
* ```
|
||||
*/
|
||||
export function endsWith(source: Uint8Array, suffix: Uint8Array): boolean {
|
||||
for (
|
||||
let srci = source.length - 1, sfxi = suffix.length - 1;
|
||||
sfxi >= 0;
|
||||
srci--, sfxi--
|
||||
) {
|
||||
if (source[srci] !== suffix[sfxi]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns a new Uint8Array composed of `count` repetitions of the `source`
|
||||
* array.
|
||||
*
|
||||
* If `count` is negative, a `RangeError` is thrown.
|
||||
*
|
||||
* ```ts
|
||||
* import { repeat } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2]);
|
||||
* console.log(repeat(source, 3)); // [0, 1, 2, 0, 1, 2, 0, 1, 2]
|
||||
* console.log(repeat(source, 0)); // []
|
||||
* console.log(repeat(source, -1)); // RangeError
|
||||
* ```
|
||||
*/
|
||||
export function repeat(source: Uint8Array, count: number): Uint8Array {
|
||||
if (count === 0) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
|
||||
if (count < 0) {
|
||||
throw new RangeError("bytes: negative repeat count");
|
||||
} else if ((source.length * count) / count !== source.length) {
|
||||
throw new Error("bytes: repeat count causes overflow");
|
||||
}
|
||||
|
||||
const int = Math.floor(count);
|
||||
|
||||
if (int !== count) {
|
||||
throw new Error("bytes: repeat count must be an integer");
|
||||
}
|
||||
|
||||
const nb = new Uint8Array(source.length * count);
|
||||
|
||||
let bp = copy(source, nb);
|
||||
|
||||
for (; bp < nb.length; bp *= 2) {
|
||||
copy(nb.slice(0, bp), nb, bp);
|
||||
}
|
||||
|
||||
return nb;
|
||||
}
|
||||
|
||||
/** Concatenate the given arrays into a new Uint8Array.
|
||||
*
|
||||
* ```ts
|
||||
* import { concat } from "./mod.ts";
|
||||
* const a = new Uint8Array([0, 1, 2]);
|
||||
* const b = new Uint8Array([3, 4, 5]);
|
||||
* console.log(concat(a, b)); // [0, 1, 2, 3, 4, 5]
|
||||
*/
|
||||
export function concat(...buf: Uint8Array[]): Uint8Array {
|
||||
let length = 0;
|
||||
for (const b of buf) {
|
||||
length += b.length;
|
||||
}
|
||||
|
||||
const output = new Uint8Array(length);
|
||||
let index = 0;
|
||||
for (const b of buf) {
|
||||
output.set(b, index);
|
||||
index += b.length;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/** Returns true if the source array contains the needle array, false otherwise.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the beginning of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.length * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { includesNeedle } from "./mod.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(includesNeedle(source, needle)); // true
|
||||
* console.log(includesNeedle(source, needle, 6)); // false
|
||||
* ```
|
||||
*/
|
||||
export function includesNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = 0,
|
||||
): boolean {
|
||||
return indexOfNeedle(source, needle, start) !== -1;
|
||||
}
|
||||
|
||||
/** Copy bytes from the `src` array to the `dst` array. Returns the number of
|
||||
* bytes copied.
|
||||
*
|
||||
* If the `src` array is larger than what the `dst` array can hold, only the
|
||||
* amount of bytes that fit in the `dst` array are copied.
|
||||
*
|
||||
* An offset can be specified as the third argument that begins the copy at
|
||||
* that given index in the `dst` array. The offset defaults to the beginning of
|
||||
* the array.
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "./mod.ts";
|
||||
* const src = new Uint8Array([9, 8, 7]);
|
||||
* const dst = new Uint8Array([0, 1, 2, 3, 4, 5]);
|
||||
* console.log(copy(src, dst)); // 3
|
||||
* console.log(dst); // [9, 8, 7, 3, 4, 5]
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "./mod.ts";
|
||||
* const src = new Uint8Array([1, 1, 1, 1]);
|
||||
* const dst = new Uint8Array([0, 0, 0, 0]);
|
||||
* console.log(copy(src, dst, 1)); // 3
|
||||
* console.log(dst); // [0, 1, 1, 1]
|
||||
* ```
|
||||
*/
|
||||
export function copy(src: Uint8Array, dst: Uint8Array, off = 0): number {
|
||||
off = Math.max(0, Math.min(off, dst.byteLength));
|
||||
const dstBytesAvailable = dst.byteLength - off;
|
||||
if (src.byteLength > dstBytesAvailable) {
|
||||
src = src.subarray(0, dstBytesAvailable);
|
||||
}
|
||||
dst.set(src, off);
|
||||
return src.byteLength;
|
||||
}
|
||||
|
||||
export { equals } from "./equals.ts";
|
||||
+1017
File diff suppressed because it is too large.
Load diff
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
export interface Reader {
|
||||
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of
|
||||
* bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error
|
||||
* encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may
|
||||
* use all of `p` as scratch space during the call. If some data is
|
||||
* available but not `p.byteLength` bytes, `read()` conventionally resolves
|
||||
* to what is available instead of waiting for more.
|
||||
*
|
||||
* When `read()` encounters end-of-file condition, it resolves to EOF
|
||||
* (`null`).
|
||||
*
|
||||
* When `read()` encounters an error, it rejects with an error.
|
||||
*
|
||||
* Callers should always process the `n` > `0` bytes returned before
|
||||
* considering the EOF (`null`). Doing so correctly handles I/O errors that
|
||||
* happen after reading some bytes and also both of the allowed EOF
|
||||
* behaviors.
|
||||
*
|
||||
* Implementations should not retain a reference to `p`.
|
||||
*
|
||||
* Use iterateReader() from https://deno.land/std/streams/conversion.ts to turn a Reader into an
|
||||
* AsyncIterator.
|
||||
*/
|
||||
read(p: Uint8Array): Promise<number | null>;
|
||||
}
|
||||
|
||||
export interface ReaderSync {
|
||||
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number
|
||||
* of bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error
|
||||
* encountered. Even if `read()` returns `n` < `p.byteLength`, it may use
|
||||
* all of `p` as scratch space during the call. If some data is available
|
||||
* but not `p.byteLength` bytes, `read()` conventionally returns what is
|
||||
* available instead of waiting for more.
|
||||
*
|
||||
* When `readSync()` encounters end-of-file condition, it returns EOF
|
||||
* (`null`).
|
||||
*
|
||||
* When `readSync()` encounters an error, it throws with an error.
|
||||
*
|
||||
* Callers should always process the `n` > `0` bytes returned before
|
||||
* considering the EOF (`null`). Doing so correctly handles I/O errors that happen
|
||||
* after reading some bytes and also both of the allowed EOF behaviors.
|
||||
*
|
||||
* Implementations should not retain a reference to `p`.
|
||||
*
|
||||
* Use iterateReaderSync() from https://deno.land/std/streams/conversion.ts to turn a ReaderSync
|
||||
* into an Iterator.
|
||||
*/
|
||||
readSync(p: Uint8Array): number | null;
|
||||
}
|
||||
|
||||
export interface Writer {
|
||||
/** Writes `p.byteLength` bytes from `p` to the underlying data stream. It
|
||||
* resolves to the number of bytes written from `p` (`0` <= `n` <=
|
||||
* `p.byteLength`) or reject with the error encountered that caused the
|
||||
* write to stop early. `write()` must reject with a non-null error if
|
||||
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
|
||||
* slice data, even temporarily.
|
||||
*
|
||||
* Implementations should not retain a reference to `p`.
|
||||
*/
|
||||
write(p: Uint8Array): Promise<number>;
|
||||
}
|
||||
|
||||
export interface WriterSync {
|
||||
/** Writes `p.byteLength` bytes from `p` to the underlying data
|
||||
* stream. It returns the number of bytes written from `p` (`0` <= `n`
|
||||
* <= `p.byteLength`) and any error encountered that caused the write to
|
||||
* stop early. `writeSync()` must throw a non-null error if it returns `n` <
|
||||
* `p.byteLength`. `writeSync()` must not modify the slice data, even
|
||||
* temporarily.
|
||||
*
|
||||
* Implementations should not retain a reference to `p`.
|
||||
*/
|
||||
writeSync(p: Uint8Array): number;
|
||||
}
|
||||
|
||||
export interface Closer {
|
||||
close(): void;
|
||||
}
|
||||
+600
@@ -0,0 +1,600 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { Buffer } from "../io/buffer.ts";
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 16_640;
|
||||
const DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
function isCloser(value: unknown): value is Deno.Closer {
|
||||
return typeof value === "object" && value != null && "close" in value &&
|
||||
// deno-lint-ignore no-explicit-any
|
||||
typeof (value as Record<string, any>)["close"] === "function";
|
||||
}
|
||||
|
||||
/** Create a `Deno.Reader` from an iterable of `Uint8Array`s.
|
||||
*
|
||||
* ```ts
|
||||
* import { readerFromIterable, copy } from "./conversion.ts";
|
||||
*
|
||||
* const file = await Deno.open("metrics.txt", { write: true });
|
||||
* const reader = readerFromIterable((async function* () {
|
||||
* while (true) {
|
||||
* await new Promise((r) => setTimeout(r, 1000));
|
||||
* const message = `data: ${JSON.stringify(Deno.metrics())}\n\n`;
|
||||
* yield new TextEncoder().encode(message);
|
||||
* }
|
||||
* })());
|
||||
* await copy(reader, file);
|
||||
* ```
|
||||
*/
|
||||
export function readerFromIterable(
|
||||
iterable: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
||||
): Deno.Reader {
|
||||
const iterator: Iterator<Uint8Array> | AsyncIterator<Uint8Array> =
|
||||
(iterable as AsyncIterable<Uint8Array>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<Uint8Array>)[Symbol.iterator]?.();
|
||||
const buffer = new Buffer();
|
||||
return {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
if (buffer.length == 0) {
|
||||
const result = await iterator.next();
|
||||
if (result.done) {
|
||||
return null;
|
||||
} else {
|
||||
if (result.value.byteLength <= p.byteLength) {
|
||||
p.set(result.value);
|
||||
return result.value.byteLength;
|
||||
}
|
||||
p.set(result.value.subarray(0, p.byteLength));
|
||||
await writeAll(buffer, result.value.subarray(p.byteLength));
|
||||
return p.byteLength;
|
||||
}
|
||||
} else {
|
||||
const n = await buffer.read(p);
|
||||
if (n == null) {
|
||||
return this.read(p);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a `Writer` from a `WritableStreamDefaultWriter`. */
|
||||
export function writerFromStreamWriter(
|
||||
streamWriter: WritableStreamDefaultWriter<Uint8Array>,
|
||||
): Deno.Writer {
|
||||
return {
|
||||
async write(p: Uint8Array): Promise<number> {
|
||||
await streamWriter.ready;
|
||||
await streamWriter.write(p);
|
||||
return p.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a `Reader` from a `ReadableStreamDefaultReader`. */
|
||||
export function readerFromStreamReader(
|
||||
streamReader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
): Deno.Reader {
|
||||
const buffer = new Buffer();
|
||||
|
||||
return {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
if (buffer.empty()) {
|
||||
const res = await streamReader.read();
|
||||
if (res.done) {
|
||||
return null; // EOF
|
||||
}
|
||||
|
||||
await writeAll(buffer, res.value);
|
||||
}
|
||||
|
||||
return buffer.read(p);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface WritableStreamFromWriterOptions {
|
||||
/**
|
||||
* If the `writer` is also a `Deno.Closer`, automatically close the `writer`
|
||||
* when the stream is closed, aborted, or a write error occurs.
|
||||
*
|
||||
* Defaults to `true`. */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
|
||||
/** Create a `WritableStream` from a `Writer`. */
|
||||
export function writableStreamFromWriter(
|
||||
writer: Deno.Writer,
|
||||
options: WritableStreamFromWriterOptions = {},
|
||||
): WritableStream<Uint8Array> {
|
||||
const { autoClose = true } = options;
|
||||
|
||||
return new WritableStream({
|
||||
async write(chunk, controller) {
|
||||
try {
|
||||
await writeAll(writer, chunk);
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
},
|
||||
abort() {
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a `ReadableStream` from any kind of iterable.
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable } from "./conversion.ts";
|
||||
*
|
||||
* const r1 = readableStreamFromIterable(["foo, bar, baz"]);
|
||||
* const r2 = readableStreamFromIterable(async function* () {
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "foo";
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "bar";
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "baz";
|
||||
* }());
|
||||
* ```
|
||||
*
|
||||
* If the produced iterator (`iterable[Symbol.asyncIterator]()` or
|
||||
* `iterable[Symbol.iterator]()`) is a generator, or more specifically is found
|
||||
* to have a `.throw()` method on it, that will be called upon
|
||||
* `readableStream.cancel()`. This is the case for the second input type above:
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable } from "./conversion.ts";
|
||||
*
|
||||
* const r3 = readableStreamFromIterable(async function* () {
|
||||
* try {
|
||||
* yield "foo";
|
||||
* } catch (error) {
|
||||
* console.log(error); // Error: Cancelled by consumer.
|
||||
* }
|
||||
* }());
|
||||
* const reader = r3.getReader();
|
||||
* console.log(await reader.read()); // { value: "foo", done: false }
|
||||
* await reader.cancel(new Error("Cancelled by consumer."));
|
||||
* ```
|
||||
*/
|
||||
export function readableStreamFromIterable<T>(
|
||||
iterable: Iterable<T> | AsyncIterable<T>,
|
||||
): ReadableStream<T> {
|
||||
const iterator: Iterator<T> | AsyncIterator<T> =
|
||||
(iterable as AsyncIterable<T>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<T>)[Symbol.iterator]?.();
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const { value, done } = await iterator.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.enqueue(value);
|
||||
}
|
||||
},
|
||||
async cancel(reason) {
|
||||
if (typeof iterator.throw == "function") {
|
||||
try {
|
||||
await iterator.throw(reason);
|
||||
} catch { /* `iterator.throw()` always throws on site. We catch it. */ }
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the generator function into a TransformStream.
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable, toTransformStream } from "./conversion.ts";
|
||||
*
|
||||
* const readable = readableStreamFromIterable([0, 1, 2])
|
||||
* .pipeThrough(toTransformStream(async function* (src) {
|
||||
* for await (const chunk of src) {
|
||||
* yield chunk * 100;
|
||||
* }
|
||||
* }));
|
||||
*
|
||||
* for await (const chunk of readable) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* // output: 0, 100, 200
|
||||
* ```
|
||||
*
|
||||
* @param transformer A function to transform.
|
||||
* @param writableStrategy An object that optionally defines a queuing strategy for the stream.
|
||||
* @param readableStrategy An object that optionally defines a queuing strategy for the stream.
|
||||
*/
|
||||
export function toTransformStream<I, O>(
|
||||
transformer: (src: ReadableStream<I>) => Iterable<O> | AsyncIterable<O>,
|
||||
writableStrategy?: QueuingStrategy<I>,
|
||||
readableStrategy?: QueuingStrategy<O>,
|
||||
): TransformStream<I, O> {
|
||||
const {
|
||||
writable,
|
||||
readable,
|
||||
} = new TransformStream<I, I>(undefined, writableStrategy);
|
||||
|
||||
const iterable = transformer(readable);
|
||||
const iterator: Iterator<O> | AsyncIterator<O> =
|
||||
(iterable as AsyncIterable<O>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<O>)[Symbol.iterator]?.();
|
||||
return {
|
||||
writable,
|
||||
readable: new ReadableStream<O>({
|
||||
async pull(controller) {
|
||||
let result: IteratorResult<O>;
|
||||
try {
|
||||
result = await iterator.next();
|
||||
} catch (error) {
|
||||
// Propagate error to stream from iterator
|
||||
// If the stream status is "errored", it will be thrown, but ignore.
|
||||
await readable.cancel(error).catch(() => {});
|
||||
controller.error(error);
|
||||
return;
|
||||
}
|
||||
if (result.done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(result.value);
|
||||
},
|
||||
async cancel(reason) {
|
||||
// Propagate cancellation to readable and iterator
|
||||
if (typeof iterator.throw == "function") {
|
||||
try {
|
||||
await iterator.throw(reason);
|
||||
} catch {
|
||||
/* `iterator.throw()` always throws on site. We catch it. */
|
||||
}
|
||||
}
|
||||
await readable.cancel(reason);
|
||||
},
|
||||
}, readableStrategy),
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReadableStreamFromReaderOptions {
|
||||
/** If the `reader` is also a `Deno.Closer`, automatically close the `reader`
|
||||
* when `EOF` is encountered, or a read error occurs.
|
||||
*
|
||||
* Defaults to `true`. */
|
||||
autoClose?: boolean;
|
||||
|
||||
/** The size of chunks to allocate to read, the default is ~16KiB, which is
|
||||
* the maximum size that Deno operations can currently support. */
|
||||
chunkSize?: number;
|
||||
|
||||
/** The queuing strategy to create the `ReadableStream` with. */
|
||||
strategy?: { highWaterMark?: number | undefined; size?: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `ReadableStream<Uint8Array>` from from a `Deno.Reader`.
|
||||
*
|
||||
* When the pull algorithm is called on the stream, a chunk from the reader
|
||||
* will be read. When `null` is returned from the reader, the stream will be
|
||||
* closed along with the reader (if it is also a `Deno.Closer`).
|
||||
*
|
||||
* An example converting a `Deno.FsFile` into a readable stream:
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromReader } from "./mod.ts";
|
||||
*
|
||||
* const file = await Deno.open("./file.txt", { read: true });
|
||||
* const fileStream = readableStreamFromReader(file);
|
||||
* ```
|
||||
*/
|
||||
export function readableStreamFromReader(
|
||||
reader: Deno.Reader | (Deno.Reader & Deno.Closer),
|
||||
options: ReadableStreamFromReaderOptions = {},
|
||||
): ReadableStream<Uint8Array> {
|
||||
const {
|
||||
autoClose = true,
|
||||
chunkSize = DEFAULT_CHUNK_SIZE,
|
||||
strategy,
|
||||
} = options;
|
||||
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const chunk = new Uint8Array(chunkSize);
|
||||
try {
|
||||
const read = await reader.read(chunk);
|
||||
if (read === null) {
|
||||
if (isCloser(reader) && autoClose) {
|
||||
reader.close();
|
||||
}
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(chunk.subarray(0, read));
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
if (isCloser(reader)) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
if (isCloser(reader) && autoClose) {
|
||||
reader.close();
|
||||
}
|
||||
},
|
||||
}, strategy);
|
||||
}
|
||||
|
||||
/** Read Reader `r` until EOF (`null`) and resolve to the content as
|
||||
* Uint8Array`.
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "../io/buffer.ts";
|
||||
* import { readAll } from "./conversion.ts";
|
||||
*
|
||||
* // Example from stdin
|
||||
* const stdinContent = await readAll(Deno.stdin);
|
||||
*
|
||||
* // Example from file
|
||||
* const file = await Deno.open("my_file.txt", {read: true});
|
||||
* const myFileContent = await readAll(file);
|
||||
* Deno.close(file.rid);
|
||||
*
|
||||
* // Example from buffer
|
||||
* const myData = new Uint8Array(100);
|
||||
* // ... fill myData array with data
|
||||
* const reader = new Buffer(myData.buffer);
|
||||
* const bufferContent = await readAll(reader);
|
||||
* ```
|
||||
*/
|
||||
export async function readAll(r: Deno.Reader): Promise<Uint8Array> {
|
||||
const buf = new Buffer();
|
||||
await buf.readFrom(r);
|
||||
return buf.bytes();
|
||||
}
|
||||
|
||||
/** Synchronously reads Reader `r` until EOF (`null`) and returns the content
|
||||
* as `Uint8Array`.
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "../io/buffer.ts";
|
||||
* import { readAllSync } from "./conversion.ts";
|
||||
*
|
||||
* // Example from stdin
|
||||
* const stdinContent = readAllSync(Deno.stdin);
|
||||
*
|
||||
* // Example from file
|
||||
* const file = Deno.openSync("my_file.txt", {read: true});
|
||||
* const myFileContent = readAllSync(file);
|
||||
* Deno.close(file.rid);
|
||||
*
|
||||
* // Example from buffer
|
||||
* const myData = new Uint8Array(100);
|
||||
* // ... fill myData array with data
|
||||
* const reader = new Buffer(myData.buffer);
|
||||
* const bufferContent = readAllSync(reader);
|
||||
* ```
|
||||
*/
|
||||
export function readAllSync(r: Deno.ReaderSync): Uint8Array {
|
||||
const buf = new Buffer();
|
||||
buf.readFromSync(r);
|
||||
return buf.bytes();
|
||||
}
|
||||
|
||||
/** Write all the content of the array buffer (`arr`) to the writer (`w`).
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "../io/buffer.ts";
|
||||
* import { writeAll } from "./conversion.ts";
|
||||
|
||||
* // Example writing to stdout
|
||||
* let contentBytes = new TextEncoder().encode("Hello World");
|
||||
* await writeAll(Deno.stdout, contentBytes);
|
||||
*
|
||||
* // Example writing to file
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const file = await Deno.open('test.file', {write: true});
|
||||
* await writeAll(file, contentBytes);
|
||||
* Deno.close(file.rid);
|
||||
*
|
||||
* // Example writing to buffer
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const writer = new Buffer();
|
||||
* await writeAll(writer, contentBytes);
|
||||
* console.log(writer.bytes().length); // 11
|
||||
* ```
|
||||
*/
|
||||
export async function writeAll(w: Deno.Writer, arr: Uint8Array) {
|
||||
let nwritten = 0;
|
||||
while (nwritten < arr.length) {
|
||||
nwritten += await w.write(arr.subarray(nwritten));
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronously write all the content of the array buffer (`arr`) to the
|
||||
* writer (`w`).
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "../io/buffer.ts";
|
||||
* import { writeAllSync } from "./conversion.ts";
|
||||
*
|
||||
* // Example writing to stdout
|
||||
* let contentBytes = new TextEncoder().encode("Hello World");
|
||||
* writeAllSync(Deno.stdout, contentBytes);
|
||||
*
|
||||
* // Example writing to file
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const file = Deno.openSync('test.file', {write: true});
|
||||
* writeAllSync(file, contentBytes);
|
||||
* Deno.close(file.rid);
|
||||
*
|
||||
* // Example writing to buffer
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const writer = new Buffer();
|
||||
* writeAllSync(writer, contentBytes);
|
||||
* console.log(writer.bytes().length); // 11
|
||||
* ```
|
||||
*/
|
||||
export function writeAllSync(w: Deno.WriterSync, arr: Uint8Array): void {
|
||||
let nwritten = 0;
|
||||
while (nwritten < arr.length) {
|
||||
nwritten += w.writeSync(arr.subarray(nwritten));
|
||||
}
|
||||
}
|
||||
|
||||
/** Turns a Reader, `r`, into an async iterator.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReader } from "./conversion.ts";
|
||||
*
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* for await (const chunk of iterateReader(f)) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Second argument can be used to tune size of a buffer.
|
||||
* Default size of the buffer is 32kB.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReader } from "./conversion.ts";
|
||||
*
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* const it = iterateReader(f, {
|
||||
* bufSize: 1024 * 1024
|
||||
* });
|
||||
* for await (const chunk of it) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Iterator uses an internal buffer of fixed size for efficiency; it returns
|
||||
* a view on that buffer on each iteration. It is therefore caller's
|
||||
* responsibility to copy contents of the buffer if needed; otherwise the
|
||||
* next iteration will overwrite contents of previously returned chunk.
|
||||
*/
|
||||
export async function* iterateReader(
|
||||
r: Deno.Reader,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): AsyncIterableIterator<Uint8Array> {
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
while (true) {
|
||||
const result = await r.read(b);
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield b.subarray(0, result);
|
||||
}
|
||||
}
|
||||
|
||||
/** Turns a ReaderSync, `r`, into an iterator.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReaderSync } from "./conversion.ts";
|
||||
*
|
||||
* let f = Deno.openSync("/etc/passwd");
|
||||
* for (const chunk of iterateReaderSync(f)) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Second argument can be used to tune size of a buffer.
|
||||
* Default size of the buffer is 32kB.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReaderSync } from "./conversion.ts";
|
||||
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* const iter = iterateReaderSync(f, {
|
||||
* bufSize: 1024 * 1024
|
||||
* });
|
||||
* for (const chunk of iter) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Iterator uses an internal buffer of fixed size for efficiency; it returns
|
||||
* a view on that buffer on each iteration. It is therefore caller's
|
||||
* responsibility to copy contents of the buffer if needed; otherwise the
|
||||
* next iteration will overwrite contents of previously returned chunk.
|
||||
*/
|
||||
export function* iterateReaderSync(
|
||||
r: Deno.ReaderSync,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): IterableIterator<Uint8Array> {
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
while (true) {
|
||||
const result = r.readSync(b);
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield b.subarray(0, result);
|
||||
}
|
||||
}
|
||||
|
||||
/** Copies from `src` to `dst` until either EOF (`null`) is read from `src` or
|
||||
* an error occurs. It resolves to the number of bytes copied or rejects with
|
||||
* the first error encountered while copying.
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "./conversion.ts";
|
||||
*
|
||||
* const source = await Deno.open("my_file.txt");
|
||||
* const bytesCopied1 = await copy(source, Deno.stdout);
|
||||
* const destination = await Deno.create("my_file_2.txt");
|
||||
* const bytesCopied2 = await copy(source, destination);
|
||||
* ```
|
||||
*
|
||||
* @param src The source to copy from
|
||||
* @param dst The destination to copy to
|
||||
* @param options Can be used to tune size of the buffer. Default size is 32kB
|
||||
*/
|
||||
export async function copy(
|
||||
src: Deno.Reader,
|
||||
dst: Deno.Writer,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): Promise<number> {
|
||||
let n = 0;
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
let gotEOF = false;
|
||||
while (gotEOF === false) {
|
||||
const result = await src.read(b);
|
||||
if (result === null) {
|
||||
gotEOF = true;
|
||||
} else {
|
||||
let nwritten = 0;
|
||||
while (nwritten < result) {
|
||||
nwritten += await dst.write(b.subarray(nwritten, result));
|
||||
}
|
||||
n += nwritten;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* All internal non-test code, that is files that do not have `test` or `bench` in the name, must use the assertion functions within `_utils/asserts.ts` and not `testing/asserts.ts`. This is to create a separation of concerns between internal and testing assertions.
|
||||
*/
|
||||
|
||||
export class DenoStdInternalError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "DenoStdInternalError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Make an assertion, if not `true`, then throw. */
|
||||
export function assert(expr: unknown, msg = ""): asserts expr {
|
||||
if (!expr) {
|
||||
throw new DenoStdInternalError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Use this to assert unreachable code. */
|
||||
export function unreachable(): never {
|
||||
throw new DenoStdInternalError("unreachable");
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
export type OSType = "windows" | "linux" | "darwin" | "freebsd";
|
||||
|
||||
export const osType: OSType = (() => {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const { Deno } = globalThis as any;
|
||||
if (typeof Deno?.build?.os === "string") {
|
||||
return Deno.build.os;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const { navigator } = globalThis as any;
|
||||
if (navigator?.appVersion?.includes?.("Win")) {
|
||||
return "windows";
|
||||
}
|
||||
|
||||
return "linux";
|
||||
})();
|
||||
|
||||
export const isWindows = osType === "windows";
|
||||
export const isLinux = osType === "linux";
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
// TODO(ry) It'd be better to make Deferred a class that inherits from
|
||||
// Promise, rather than an interface. This is possible in ES2016, however
|
||||
// typescript produces broken code when targeting ES5 code.
|
||||
// See https://github.com/Microsoft/TypeScript/issues/15202
|
||||
// At the time of writing, the github issue is closed but the problem remains.
|
||||
export interface Deferred<T> extends Promise<T> {
|
||||
readonly state: "pending" | "fulfilled" | "rejected";
|
||||
resolve(value?: T | PromiseLike<T>): void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject(reason?: any): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Promise with the `reject` and `resolve` functions placed as methods
|
||||
* on the promise object itself.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { deferred } from "https://deno.land/std@$STD_VERSION/async/deferred.ts";
|
||||
*
|
||||
* const p = deferred<number>();
|
||||
* // ...
|
||||
* p.resolve(42);
|
||||
* ```
|
||||
*/
|
||||
export function deferred<T>(): Deferred<T> {
|
||||
let methods;
|
||||
let state = "pending";
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
methods = {
|
||||
async resolve(value: T | PromiseLike<T>) {
|
||||
await value;
|
||||
state = "fulfilled";
|
||||
resolve(value);
|
||||
},
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject(reason?: any) {
|
||||
state = "rejected";
|
||||
reject(reason);
|
||||
},
|
||||
};
|
||||
});
|
||||
Object.defineProperty(promise, "state", { get: () => state });
|
||||
return Object.assign(promise, methods) as Deferred<T>;
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* An abstraction of multiple Uint8Arrays
|
||||
*/
|
||||
export class BytesList {
|
||||
#len = 0;
|
||||
#chunks: {
|
||||
value: Uint8Array;
|
||||
start: number; // start offset from head of chunk
|
||||
end: number; // end offset from head of chunk
|
||||
offset: number; // offset of head in all bytes
|
||||
}[] = [];
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Total size of bytes
|
||||
*/
|
||||
size() {
|
||||
return this.#len;
|
||||
}
|
||||
/**
|
||||
* Push bytes with given offset infos
|
||||
*/
|
||||
add(value: Uint8Array, start = 0, end = value.byteLength) {
|
||||
if (value.byteLength === 0 || end - start === 0) {
|
||||
return;
|
||||
}
|
||||
checkRange(start, end, value.byteLength);
|
||||
this.#chunks.push({
|
||||
value,
|
||||
end,
|
||||
start,
|
||||
offset: this.#len,
|
||||
});
|
||||
this.#len += end - start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop head `n` bytes.
|
||||
*/
|
||||
shift(n: number) {
|
||||
if (n === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.#len <= n) {
|
||||
this.#chunks = [];
|
||||
this.#len = 0;
|
||||
return;
|
||||
}
|
||||
const idx = this.getChunkIndex(n);
|
||||
this.#chunks.splice(0, idx);
|
||||
const [chunk] = this.#chunks;
|
||||
if (chunk) {
|
||||
const diff = n - chunk.offset;
|
||||
chunk.start += diff;
|
||||
}
|
||||
let offset = 0;
|
||||
for (const chunk of this.#chunks) {
|
||||
chunk.offset = offset;
|
||||
offset += chunk.end - chunk.start;
|
||||
}
|
||||
this.#len = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find chunk index in which `pos` locates by binary-search
|
||||
* returns -1 if out of range
|
||||
*/
|
||||
getChunkIndex(pos: number): number {
|
||||
let max = this.#chunks.length;
|
||||
let min = 0;
|
||||
while (true) {
|
||||
const i = min + Math.floor((max - min) / 2);
|
||||
if (i < 0 || this.#chunks.length <= i) {
|
||||
return -1;
|
||||
}
|
||||
const { offset, start, end } = this.#chunks[i];
|
||||
const len = end - start;
|
||||
if (offset <= pos && pos < offset + len) {
|
||||
return i;
|
||||
} else if (offset + len <= pos) {
|
||||
min = i + 1;
|
||||
} else {
|
||||
max = i - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexed byte from chunks
|
||||
*/
|
||||
get(i: number): number {
|
||||
if (i < 0 || this.#len <= i) {
|
||||
throw new Error("out of range");
|
||||
}
|
||||
const idx = this.getChunkIndex(i);
|
||||
const { value, offset, start } = this.#chunks[idx];
|
||||
return value[start + i - offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator of bytes from given position
|
||||
*/
|
||||
*iterator(start = 0): IterableIterator<number> {
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
if (startIdx < 0) return;
|
||||
const first = this.#chunks[startIdx];
|
||||
let firstOffset = start - first.offset;
|
||||
for (let i = startIdx; i < this.#chunks.length; i++) {
|
||||
const chunk = this.#chunks[i];
|
||||
for (let j = chunk.start + firstOffset; j < chunk.end; j++) {
|
||||
yield chunk.value[j];
|
||||
}
|
||||
firstOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns subset of bytes copied
|
||||
*/
|
||||
slice(start: number, end: number = this.#len): Uint8Array {
|
||||
if (end === start) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
checkRange(start, end, this.#len);
|
||||
const result = new Uint8Array(end - start);
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
const endIdx = this.getChunkIndex(end - 1);
|
||||
let written = 0;
|
||||
for (let i = startIdx; i <= endIdx; i++) {
|
||||
const {
|
||||
value: chunkValue,
|
||||
start: chunkStart,
|
||||
end: chunkEnd,
|
||||
offset: chunkOffset,
|
||||
} = this.#chunks[i];
|
||||
const readStart = chunkStart + (i === startIdx ? start - chunkOffset : 0);
|
||||
const readEnd = i === endIdx ? end - chunkOffset + chunkStart : chunkEnd;
|
||||
const len = readEnd - readStart;
|
||||
result.set(chunkValue.subarray(readStart, readEnd), written);
|
||||
written += len;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Concatenate chunks into single Uint8Array copied.
|
||||
*/
|
||||
concat(): Uint8Array {
|
||||
const result = new Uint8Array(this.#len);
|
||||
let sum = 0;
|
||||
for (const { value, start, end } of this.#chunks) {
|
||||
result.set(value.subarray(start, end), sum);
|
||||
sum += end - start;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function checkRange(start: number, end: number, len: number) {
|
||||
if (start < 0 || len < start || end < 0 || len < end || end < start) {
|
||||
throw new Error("invalid range");
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Concatenate the given arrays into a new Uint8Array.
|
||||
*
|
||||
* ```ts
|
||||
* import { concat } from "https://deno.land/std@$STD_VERSION/bytes/concat.ts";
|
||||
* const a = new Uint8Array([0, 1, 2]);
|
||||
* const b = new Uint8Array([3, 4, 5]);
|
||||
* console.log(concat(a, b)); // [0, 1, 2, 3, 4, 5]
|
||||
*/
|
||||
export function concat(...buf: Uint8Array[]): Uint8Array {
|
||||
let length = 0;
|
||||
for (const b of buf) {
|
||||
length += b.length;
|
||||
}
|
||||
|
||||
const output = new Uint8Array(length);
|
||||
let index = 0;
|
||||
for (const b of buf) {
|
||||
output.set(b, index);
|
||||
index += b.length;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Copy bytes from the `src` array to the `dst` array. Returns the number of
|
||||
* bytes copied.
|
||||
*
|
||||
* If the `src` array is larger than what the `dst` array can hold, only the
|
||||
* amount of bytes that fit in the `dst` array are copied.
|
||||
*
|
||||
* An offset can be specified as the third argument that begins the copy at
|
||||
* that given index in the `dst` array. The offset defaults to the beginning of
|
||||
* the array.
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/bytes/copy.ts";
|
||||
* const src = new Uint8Array([9, 8, 7]);
|
||||
* const dst = new Uint8Array([0, 1, 2, 3, 4, 5]);
|
||||
* console.log(copy(src, dst)); // 3
|
||||
* console.log(dst); // [9, 8, 7, 3, 4, 5]
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/bytes/copy.ts";
|
||||
* const src = new Uint8Array([1, 1, 1, 1]);
|
||||
* const dst = new Uint8Array([0, 0, 0, 0]);
|
||||
* console.log(copy(src, dst, 1)); // 3
|
||||
* console.log(dst); // [0, 1, 1, 1]
|
||||
* ```
|
||||
*/
|
||||
export function copy(src: Uint8Array, dst: Uint8Array, off = 0): number {
|
||||
off = Math.max(0, Math.min(off, dst.byteLength));
|
||||
const dstBytesAvailable = dst.byteLength - off;
|
||||
if (src.byteLength > dstBytesAvailable) {
|
||||
src = src.subarray(0, dstBytesAvailable);
|
||||
}
|
||||
dst.set(src, off);
|
||||
return src.byteLength;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Returns true if the suffix array appears at the end of the source array,
|
||||
* false otherwise.
|
||||
*
|
||||
* The complexity of this function is O(suffix.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { endsWith } from "https://deno.land/std@$STD_VERSION/bytes/ends_with.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const suffix = new Uint8Array([1, 2, 3]);
|
||||
* console.log(endsWith(source, suffix)); // true
|
||||
* ```
|
||||
*/
|
||||
export function endsWith(source: Uint8Array, suffix: Uint8Array): boolean {
|
||||
for (
|
||||
let srci = source.length - 1, sfxi = suffix.length - 1;
|
||||
sfxi >= 0;
|
||||
srci--, sfxi--
|
||||
) {
|
||||
if (source[srci] !== suffix[sfxi]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Check whether binary arrays are equal to each other using 8-bit comparisons.
|
||||
* @private
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
function equalsNaive(a: Uint8Array, b: Uint8Array): boolean {
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Check whether binary arrays are equal to each other using 32-bit comparisons.
|
||||
* @private
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
function equals32Bit(a: Uint8Array, b: Uint8Array): boolean {
|
||||
const len = a.length;
|
||||
const compressable = Math.floor(len / 4);
|
||||
const compressedA = new Uint32Array(a.buffer, 0, compressable);
|
||||
const compressedB = new Uint32Array(b.buffer, 0, compressable);
|
||||
for (let i = compressable * 4; i < len; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
for (let i = 0; i < compressedA.length; i++) {
|
||||
if (compressedA[i] !== compressedB[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Check whether binary arrays are equal to each other.
|
||||
* @param a first array to check equality
|
||||
* @param b second array to check equality
|
||||
*/
|
||||
export function equals(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.length < 1000 ? equalsNaive(a, b) : equals32Bit(a, b);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { indexOfNeedle } from "./index_of_needle.ts";
|
||||
|
||||
/** Returns true if the source array contains the needle array, false otherwise.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the beginning of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.length * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { includesNeedle } from "https://deno.land/std@$STD_VERSION/bytes/includes_needle.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(includesNeedle(source, needle)); // true
|
||||
* console.log(includesNeedle(source, needle, 6)); // false
|
||||
* ```
|
||||
*/
|
||||
export function includesNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = 0,
|
||||
): boolean {
|
||||
return indexOfNeedle(source, needle, start) !== -1;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Returns the index of the first occurrence of the needle array in the source
|
||||
* array, or -1 if it is not present.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the start of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.lenth * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { indexOfNeedle } from "https://deno.land/std@$STD_VERSION/bytes/index_of_needle.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(indexOfNeedle(source, needle)); // 1
|
||||
* console.log(indexOfNeedle(source, needle, 2)); // 3
|
||||
* ```
|
||||
*/
|
||||
export function indexOfNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = 0,
|
||||
): number {
|
||||
if (start >= source.length) {
|
||||
return -1;
|
||||
}
|
||||
if (start < 0) {
|
||||
start = Math.max(0, source.length + start);
|
||||
}
|
||||
const s = needle[0];
|
||||
for (let i = start; i < source.length; i++) {
|
||||
if (source[i] !== s) continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j++;
|
||||
if (source[j] !== needle[j - pin]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Returns the index of the last occurrence of the needle array in the source
|
||||
* array, or -1 if it is not present.
|
||||
*
|
||||
* A start index can be specified as the third argument that begins the search
|
||||
* at that given index. The start index defaults to the end of the array.
|
||||
*
|
||||
* The complexity of this function is O(source.lenth * needle.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { lastIndexOfNeedle } from "https://deno.land/std@$STD_VERSION/bytes/last_index_of_needle.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const needle = new Uint8Array([1, 2]);
|
||||
* console.log(lastIndexOfNeedle(source, needle)); // 5
|
||||
* console.log(lastIndexOfNeedle(source, needle, 4)); // 3
|
||||
* ```
|
||||
*/
|
||||
export function lastIndexOfNeedle(
|
||||
source: Uint8Array,
|
||||
needle: Uint8Array,
|
||||
start = source.length - 1,
|
||||
): number {
|
||||
if (start < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (start >= source.length) {
|
||||
start = source.length - 1;
|
||||
}
|
||||
const e = needle[needle.length - 1];
|
||||
for (let i = start; i >= 0; i--) {
|
||||
if (source[i] !== e) continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j--;
|
||||
if (source[j] !== needle[needle.length - 1 - (pin - j)]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin - needle.length + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Provides helper functions to manipulate `Uint8Array` byte slices that are not
|
||||
* included on the `Uint8Array` prototype.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export * from "./bytes_list.ts";
|
||||
export * from "./concat.ts";
|
||||
export * from "./copy.ts";
|
||||
export * from "./ends_with.ts";
|
||||
export * from "./equals.ts";
|
||||
export * from "./includes_needle.ts";
|
||||
export * from "./index_of_needle.ts";
|
||||
export * from "./last_index_of_needle.ts";
|
||||
export * from "./repeat.ts";
|
||||
export * from "./starts_with.ts";
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
import { copy } from "./copy.ts";
|
||||
|
||||
/** Returns a new Uint8Array composed of `count` repetitions of the `source`
|
||||
* array.
|
||||
*
|
||||
* If `count` is negative, a `RangeError` is thrown.
|
||||
*
|
||||
* ```ts
|
||||
* import { repeat } from "https://deno.land/std@$STD_VERSION/bytes/repeat.ts";
|
||||
* const source = new Uint8Array([0, 1, 2]);
|
||||
* console.log(repeat(source, 3)); // [0, 1, 2, 0, 1, 2, 0, 1, 2]
|
||||
* console.log(repeat(source, 0)); // []
|
||||
* console.log(repeat(source, -1)); // RangeError
|
||||
* ```
|
||||
*/
|
||||
export function repeat(source: Uint8Array, count: number): Uint8Array {
|
||||
if (count === 0) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
|
||||
if (count < 0) {
|
||||
throw new RangeError("bytes: negative repeat count");
|
||||
}
|
||||
|
||||
if (!Number.isInteger(count)) {
|
||||
throw new Error("bytes: repeat count must be an integer");
|
||||
}
|
||||
|
||||
const nb = new Uint8Array(source.length * count);
|
||||
|
||||
let bp = copy(source, nb);
|
||||
|
||||
for (; bp < nb.length; bp *= 2) {
|
||||
copy(nb.slice(0, bp), nb, bp);
|
||||
}
|
||||
|
||||
return nb;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Returns true if the prefix array appears at the start of the source array,
|
||||
* false otherwise.
|
||||
*
|
||||
* The complexity of this function is O(prefix.length).
|
||||
*
|
||||
* ```ts
|
||||
* import { startsWith } from "https://deno.land/std@$STD_VERSION/bytes/starts_with.ts";
|
||||
* const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
|
||||
* const prefix = new Uint8Array([0, 1, 2]);
|
||||
* console.log(startsWith(source, prefix)); // true
|
||||
* ```
|
||||
*/
|
||||
export function startsWith(source: Uint8Array, prefix: Uint8Array): boolean {
|
||||
for (let i = 0, max = prefix.length; i < max; i++) {
|
||||
if (source[i] !== prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Provides the {@linkcode KeyStack} class which implements the
|
||||
* {@linkcode KeyRing} interface for managing rotatable keys.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { timingSafeEqual } from "./timing_safe_equal.ts";
|
||||
import * as base64url from "../encoding/base64url.ts";
|
||||
|
||||
/** Types of data that can be signed cryptographically. */
|
||||
export type Data = string | number[] | ArrayBuffer | Uint8Array;
|
||||
|
||||
/** Types of keys that can be used to sign data. */
|
||||
export type Key = string | number[] | ArrayBuffer | Uint8Array;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function importKey(key: Key): Promise<CryptoKey> {
|
||||
if (typeof key === "string") {
|
||||
key = encoder.encode(key);
|
||||
} else if (Array.isArray(key)) {
|
||||
key = new Uint8Array(key);
|
||||
}
|
||||
return crypto.subtle.importKey(
|
||||
"raw",
|
||||
key,
|
||||
{
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-256" },
|
||||
},
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
}
|
||||
|
||||
function sign(data: Data, key: CryptoKey): Promise<ArrayBuffer> {
|
||||
if (typeof data === "string") {
|
||||
data = encoder.encode(data);
|
||||
} else if (Array.isArray(data)) {
|
||||
data = Uint8Array.from(data);
|
||||
}
|
||||
return crypto.subtle.sign("HMAC", key, data);
|
||||
}
|
||||
|
||||
/** Compare two strings, Uint8Arrays, ArrayBuffers, or arrays of numbers in a
|
||||
* way that avoids timing based attacks on the comparisons on the values.
|
||||
*
|
||||
* The function will return `true` if the values match, or `false`, if they
|
||||
* do not match.
|
||||
*
|
||||
* This was inspired by https://github.com/suryagh/tsscmp which provides a
|
||||
* timing safe string comparison to avoid timing attacks as described in
|
||||
* https://codahale.com/a-lesson-in-timing-attacks/.
|
||||
*/
|
||||
async function compare(a: Data, b: Data): Promise<boolean> {
|
||||
const key = new Uint8Array(32);
|
||||
globalThis.crypto.getRandomValues(key);
|
||||
const cryptoKey = await importKey(key);
|
||||
const ah = await sign(a, cryptoKey);
|
||||
const bh = await sign(b, cryptoKey);
|
||||
return timingSafeEqual(ah, bh);
|
||||
}
|
||||
|
||||
/** A cryptographic key chain which allows signing of data to prevent tampering,
|
||||
* but also allows for easy key rotation without needing to re-sign the data.
|
||||
*
|
||||
* Data is signed as SHA256 HMAC.
|
||||
*
|
||||
* This was inspired by [keygrip](https://github.com/crypto-utils/keygrip/).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { KeyStack } from "https://deno.land/std@$STD_VERSION/crypto/keystack.ts";
|
||||
*
|
||||
* const keyStack = new KeyStack(["hello", "world"]);
|
||||
* const digest = await keyStack.sign("some data");
|
||||
*
|
||||
* const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]);
|
||||
* await rotatedStack.verify("some data", digest); // true
|
||||
* ```
|
||||
*/
|
||||
export class KeyStack {
|
||||
#cryptoKeys = new Map<Key, CryptoKey>();
|
||||
#keys: Key[];
|
||||
|
||||
async #toCryptoKey(key: Key): Promise<CryptoKey> {
|
||||
if (!this.#cryptoKeys.has(key)) {
|
||||
this.#cryptoKeys.set(key, await importKey(key));
|
||||
}
|
||||
return this.#cryptoKeys.get(key)!;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.#keys.length;
|
||||
}
|
||||
|
||||
/** A class which accepts an array of keys that are used to sign and verify
|
||||
* data and allows easy key rotation without invalidation of previously signed
|
||||
* data.
|
||||
*
|
||||
* @param keys An iterable of keys, of which the index 0 will be used to sign
|
||||
* data, but verification can happen against any key.
|
||||
*/
|
||||
constructor(keys: Iterable<Key>) {
|
||||
const values = Array.isArray(keys) ? keys : [...keys];
|
||||
if (!(values.length)) {
|
||||
throw new TypeError("keys must contain at least one value");
|
||||
}
|
||||
this.#keys = values;
|
||||
}
|
||||
|
||||
/** Take `data` and return a SHA256 HMAC digest that uses the current 0 index
|
||||
* of the `keys` passed to the constructor. This digest is in the form of a
|
||||
* URL safe base64 encoded string. */
|
||||
async sign(data: Data): Promise<string> {
|
||||
const key = await this.#toCryptoKey(this.#keys[0]);
|
||||
return base64url.encode(await sign(data, key));
|
||||
}
|
||||
|
||||
/** Given `data` and a `digest`, verify that one of the `keys` provided the
|
||||
* constructor was used to generate the `digest`. Returns `true` if one of
|
||||
* the keys was used, otherwise `false`. */
|
||||
async verify(data: Data, digest: string): Promise<boolean> {
|
||||
return (await this.indexOf(data, digest)) > -1;
|
||||
}
|
||||
|
||||
/** Given `data` and a `digest`, return the current index of the key in the
|
||||
* `keys` passed the constructor that was used to generate the digest. If no
|
||||
* key can be found, the method returns `-1`. */
|
||||
async indexOf(data: Data, digest: string): Promise<number> {
|
||||
for (let i = 0; i < this.#keys.length; i++) {
|
||||
const cryptoKey = await this.#toCryptoKey(this.#keys[i]);
|
||||
if (
|
||||
await compare(digest, base64url.encode(await sign(data, cryptoKey)))
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) {
|
||||
const { length } = this;
|
||||
return `${this.constructor.name} ${inspect({ length })}`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
const { length } = this;
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect({ length }, newOptions)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
|
||||
/** Compare to array buffers or data views in a way that timing based attacks
|
||||
* cannot gain information about the platform. */
|
||||
export function timingSafeEqual(
|
||||
a: ArrayBufferView | ArrayBufferLike | DataView,
|
||||
b: ArrayBufferView | ArrayBufferLike | DataView,
|
||||
): boolean {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
if (!(a instanceof DataView)) {
|
||||
a = ArrayBuffer.isView(a)
|
||||
? new DataView(a.buffer, a.byteOffset, a.byteLength)
|
||||
: new DataView(a);
|
||||
}
|
||||
if (!(b instanceof DataView)) {
|
||||
b = ArrayBuffer.isView(b)
|
||||
? new DataView(b.buffer, b.byteOffset, b.byteLength)
|
||||
: new DataView(b);
|
||||
}
|
||||
assert(a instanceof DataView);
|
||||
assert(b instanceof DataView);
|
||||
const length = a.byteLength;
|
||||
let out = 0;
|
||||
let i = -1;
|
||||
while (++i < length) {
|
||||
out |= a.getUint8(i) ^ b.getUint8(i);
|
||||
}
|
||||
return out === 0;
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* {@linkcode encode} and {@linkcode decode} for
|
||||
* [base64](https://en.wikipedia.org/wiki/Base64) encoding.
|
||||
*
|
||||
* This module is browser compatible.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* decode,
|
||||
* encode,
|
||||
* } from "https://deno.land/std@$STD_VERSION/encoding/base64.ts";
|
||||
*
|
||||
* const b64Repr = "Zm9vYg==";
|
||||
*
|
||||
* const binaryData = decode(b64Repr);
|
||||
* console.log(binaryData);
|
||||
* // => Uint8Array [ 102, 111, 111, 98 ]
|
||||
*
|
||||
* console.log(encode(binaryData));
|
||||
* // => Zm9vYg==
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
const base64abc = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"+",
|
||||
"/",
|
||||
];
|
||||
|
||||
/**
|
||||
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
|
||||
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
|
||||
* @param data
|
||||
*/
|
||||
export function encode(data: ArrayBuffer | string): string {
|
||||
const uint8 = typeof data === "string"
|
||||
? new TextEncoder().encode(data)
|
||||
: data instanceof Uint8Array
|
||||
? data
|
||||
: new Uint8Array(data);
|
||||
let result = "",
|
||||
i;
|
||||
const l = uint8.length;
|
||||
for (i = 2; i < l; i += 3) {
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
|
||||
result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)];
|
||||
result += base64abc[uint8[i] & 0x3f];
|
||||
}
|
||||
if (i === l + 1) {
|
||||
// 1 octet yet to write
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[(uint8[i - 2] & 0x03) << 4];
|
||||
result += "==";
|
||||
}
|
||||
if (i === l) {
|
||||
// 2 octets yet to write
|
||||
result += base64abc[uint8[i - 2] >> 2];
|
||||
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
|
||||
result += base64abc[(uint8[i - 1] & 0x0f) << 2];
|
||||
result += "=";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a given RFC4648 base64 encoded string
|
||||
* @param b64
|
||||
*/
|
||||
export function decode(b64: string): Uint8Array {
|
||||
const binString = atob(b64);
|
||||
const size = binString.length;
|
||||
const bytes = new Uint8Array(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
bytes[i] = binString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* {@linkcode encode} and {@linkcode decode} for
|
||||
* [base64 URL safe](https://en.wikipedia.org/wiki/Base64#URL_applications) encoding.
|
||||
*
|
||||
* This module is browser compatible.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* decode,
|
||||
* encode,
|
||||
* } from "https://deno.land/std@$STD_VERSION/encoding/base64url.ts";
|
||||
*
|
||||
* const binary = new TextEncoder().encode("foobar");
|
||||
* const encoded = encode(binary);
|
||||
* console.log(encoded);
|
||||
* // => "Zm9vYmFy"
|
||||
*
|
||||
* console.log(decode(encoded));
|
||||
* // => Uint8Array(6) [ 102, 111, 111, 98, 97, 114 ]
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import * as base64 from "./base64.ts";
|
||||
|
||||
/*
|
||||
* Some variants allow or require omitting the padding '=' signs:
|
||||
* https://en.wikipedia.org/wiki/Base64#The_URL_applications
|
||||
* @param base64url
|
||||
*/
|
||||
function addPaddingToBase64url(base64url: string): string {
|
||||
if (base64url.length % 4 === 2) return base64url + "==";
|
||||
if (base64url.length % 4 === 3) return base64url + "=";
|
||||
if (base64url.length % 4 === 1) {
|
||||
throw new TypeError("Illegal base64url string!");
|
||||
}
|
||||
return base64url;
|
||||
}
|
||||
|
||||
function convertBase64urlToBase64(b64url: string): string {
|
||||
if (!/^[-_A-Z0-9]*?={0,2}$/i.test(b64url)) {
|
||||
// Contains characters not part of base64url spec.
|
||||
throw new TypeError("Failed to decode base64url: invalid character");
|
||||
}
|
||||
return addPaddingToBase64url(b64url).replace(/\-/g, "+").replace(/_/g, "/");
|
||||
}
|
||||
|
||||
function convertBase64ToBase64url(b64: string): string {
|
||||
return b64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a given ArrayBuffer or string into a base64url representation
|
||||
* @param data
|
||||
*/
|
||||
export function encode(data: ArrayBuffer | string): string {
|
||||
return convertBase64ToBase64url(base64.encode(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts given base64url encoded data back to original
|
||||
* @param b64url
|
||||
*/
|
||||
export function decode(b64url: string): Uint8Array {
|
||||
return base64.decode(convertBase64urlToBase64(b64url));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/*!
|
||||
* Adapted directly from negotiator at https://github.com/jshttp/negotiator/
|
||||
* which is licensed as follows:
|
||||
*
|
||||
* (The MIT License)
|
||||
*
|
||||
* Copyright (c) 2012-2014 Federico Romero
|
||||
* Copyright (c) 2012-2014 Isaac Z. Schlueter
|
||||
* Copyright (c) 2014-2015 Douglas Christopher Wilson
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* 'Software'), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export interface Specificity {
|
||||
i: number;
|
||||
o?: number;
|
||||
q: number;
|
||||
s?: number;
|
||||
}
|
||||
|
||||
export function compareSpecs(a: Specificity, b: Specificity): number {
|
||||
return (
|
||||
b.q - a.q ||
|
||||
(b.s ?? 0) - (a.s ?? 0) ||
|
||||
(a.o ?? 0) - (b.o ?? 0) ||
|
||||
a.i - b.i ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
export function isQuality(spec: Specificity): boolean {
|
||||
return spec.q > 0;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/*!
|
||||
* Adapted directly from negotiator at https://github.com/jshttp/negotiator/
|
||||
* which is licensed as follows:
|
||||
*
|
||||
* (The MIT License)
|
||||
*
|
||||
* Copyright (c) 2012-2014 Federico Romero
|
||||
* Copyright (c) 2012-2014 Isaac Z. Schlueter
|
||||
* Copyright (c) 2014-2015 Douglas Christopher Wilson
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* 'Software'), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { compareSpecs, isQuality, Specificity } from "./common.ts";
|
||||
|
||||
interface EncodingSpecificity extends Specificity {
|
||||
encoding?: string;
|
||||
}
|
||||
|
||||
const simpleEncodingRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/;
|
||||
|
||||
function parseEncoding(
|
||||
str: string,
|
||||
i: number,
|
||||
): EncodingSpecificity | undefined {
|
||||
const match = simpleEncodingRegExp.exec(str);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const encoding = match[1];
|
||||
let q = 1;
|
||||
if (match[2]) {
|
||||
const params = match[2].split(";");
|
||||
for (const param of params) {
|
||||
const p = param.trim().split("=");
|
||||
if (p[0] === "q") {
|
||||
q = parseFloat(p[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { encoding, q, i };
|
||||
}
|
||||
|
||||
function specify(
|
||||
encoding: string,
|
||||
spec: EncodingSpecificity,
|
||||
i = -1,
|
||||
): Specificity | undefined {
|
||||
if (!spec.encoding) {
|
||||
return;
|
||||
}
|
||||
let s = 0;
|
||||
if (spec.encoding.toLocaleLowerCase() === encoding.toLocaleLowerCase()) {
|
||||
s = 1;
|
||||
} else if (spec.encoding !== "*") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
i,
|
||||
o: spec.i,
|
||||
q: spec.q,
|
||||
s,
|
||||
};
|
||||
}
|
||||
|
||||
function parseAcceptEncoding(accept: string): EncodingSpecificity[] {
|
||||
const accepts = accept.split(",");
|
||||
const parsedAccepts: EncodingSpecificity[] = [];
|
||||
let hasIdentity = false;
|
||||
let minQuality = 1;
|
||||
|
||||
for (let i = 0; i < accepts.length; i++) {
|
||||
const encoding = parseEncoding(accepts[i].trim(), i);
|
||||
|
||||
if (encoding) {
|
||||
parsedAccepts.push(encoding);
|
||||
hasIdentity = hasIdentity || !!specify("identity", encoding);
|
||||
minQuality = Math.min(minQuality, encoding.q || 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasIdentity) {
|
||||
parsedAccepts.push({
|
||||
encoding: "identity",
|
||||
q: minQuality,
|
||||
i: accepts.length - 1,
|
||||
});
|
||||
}
|
||||
|
||||
return parsedAccepts;
|
||||
}
|
||||
|
||||
function getEncodingPriority(
|
||||
encoding: string,
|
||||
accepted: Specificity[],
|
||||
index: number,
|
||||
): Specificity {
|
||||
let priority: Specificity = { o: -1, q: 0, s: 0, i: 0 };
|
||||
|
||||
for (const s of accepted) {
|
||||
const spec = specify(encoding, s, index);
|
||||
|
||||
if (
|
||||
spec &&
|
||||
(priority.s! - spec.s! || priority.q - spec.q ||
|
||||
priority.o! - spec.o!) <
|
||||
0
|
||||
) {
|
||||
priority = spec;
|
||||
}
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/** Given an `Accept-Encoding` string, parse out the encoding returning a
|
||||
* negotiated encoding based on the `provided` encodings otherwise just a
|
||||
* prioritized array of encodings. */
|
||||
export function preferredEncodings(
|
||||
accept: string,
|
||||
provided?: string[],
|
||||
): string[] {
|
||||
const accepts = parseAcceptEncoding(accept);
|
||||
|
||||
if (!provided) {
|
||||
return accepts
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map((spec) => spec.encoding!);
|
||||
}
|
||||
|
||||
const priorities = provided.map((type, index) =>
|
||||
getEncodingPriority(type, accepts, index)
|
||||
);
|
||||
|
||||
return priorities
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map((priority) => provided[priorities.indexOf(priority)]);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/*!
|
||||
* Adapted directly from negotiator at https://github.com/jshttp/negotiator/
|
||||
* which is licensed as follows:
|
||||
*
|
||||
* (The MIT License)
|
||||
*
|
||||
* Copyright (c) 2012-2014 Federico Romero
|
||||
* Copyright (c) 2012-2014 Isaac Z. Schlueter
|
||||
* Copyright (c) 2014-2015 Douglas Christopher Wilson
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* 'Software'), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { compareSpecs, isQuality, Specificity } from "./common.ts";
|
||||
|
||||
interface LanguageSpecificity extends Specificity {
|
||||
prefix: string;
|
||||
suffix?: string;
|
||||
full: string;
|
||||
}
|
||||
|
||||
const SIMPLE_LANGUAGE_REGEXP = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
|
||||
|
||||
function parseLanguage(
|
||||
str: string,
|
||||
i: number,
|
||||
): LanguageSpecificity | undefined {
|
||||
const match = SIMPLE_LANGUAGE_REGEXP.exec(str);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [, prefix, suffix] = match;
|
||||
const full = suffix ? `${prefix}-${suffix}` : prefix;
|
||||
|
||||
let q = 1;
|
||||
if (match[3]) {
|
||||
const params = match[3].split(";");
|
||||
for (const param of params) {
|
||||
const [key, value] = param.trim().split("=");
|
||||
if (key === "q") {
|
||||
q = parseFloat(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { prefix, suffix, full, q, i };
|
||||
}
|
||||
|
||||
function parseAcceptLanguage(accept: string): LanguageSpecificity[] {
|
||||
const accepts = accept.split(",");
|
||||
const result: LanguageSpecificity[] = [];
|
||||
|
||||
for (let i = 0; i < accepts.length; i++) {
|
||||
const language = parseLanguage(accepts[i].trim(), i);
|
||||
if (language) {
|
||||
result.push(language);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function specify(
|
||||
language: string,
|
||||
spec: LanguageSpecificity,
|
||||
i: number,
|
||||
): Specificity | undefined {
|
||||
const p = parseLanguage(language, i);
|
||||
if (!p) {
|
||||
return undefined;
|
||||
}
|
||||
let s = 0;
|
||||
if (spec.full.toLowerCase() === p.full.toLowerCase()) {
|
||||
s |= 4;
|
||||
} else if (spec.prefix.toLowerCase() === p.prefix.toLowerCase()) {
|
||||
s |= 2;
|
||||
} else if (spec.full.toLowerCase() === p.prefix.toLowerCase()) {
|
||||
s |= 1;
|
||||
} else if (spec.full !== "*") {
|
||||
return;
|
||||
}
|
||||
|
||||
return { i, o: spec.i, q: spec.q, s };
|
||||
}
|
||||
|
||||
function getLanguagePriority(
|
||||
language: string,
|
||||
accepted: LanguageSpecificity[],
|
||||
index: number,
|
||||
): Specificity {
|
||||
let priority: Specificity = { i: -1, o: -1, q: 0, s: 0 };
|
||||
for (const accepts of accepted) {
|
||||
const spec = specify(language, accepts, index);
|
||||
if (
|
||||
spec &&
|
||||
((priority.s ?? 0) - (spec.s ?? 0) || priority.q - spec.q ||
|
||||
(priority.o ?? 0) - (spec.o ?? 0)) < 0
|
||||
) {
|
||||
priority = spec;
|
||||
}
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
|
||||
export function preferredLanguages(
|
||||
accept = "*",
|
||||
provided?: string[],
|
||||
): string[] {
|
||||
const accepts = parseAcceptLanguage(accept);
|
||||
|
||||
if (!provided) {
|
||||
return accepts
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map((spec) => spec.full);
|
||||
}
|
||||
|
||||
const priorities = provided
|
||||
.map((type, index) => getLanguagePriority(type, accepts, index));
|
||||
|
||||
return priorities
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map((priority) => provided[priorities.indexOf(priority)]);
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/*!
|
||||
* Adapted directly from negotiator at https://github.com/jshttp/negotiator/
|
||||
* which is licensed as follows:
|
||||
*
|
||||
* (The MIT License)
|
||||
*
|
||||
* Copyright (c) 2012-2014 Federico Romero
|
||||
* Copyright (c) 2012-2014 Isaac Z. Schlueter
|
||||
* Copyright (c) 2014-2015 Douglas Christopher Wilson
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* 'Software'), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { compareSpecs, isQuality, Specificity } from "./common.ts";
|
||||
|
||||
interface MediaTypeSpecificity extends Specificity {
|
||||
type: string;
|
||||
subtype: string;
|
||||
params: { [param: string]: string | undefined };
|
||||
}
|
||||
|
||||
const simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/;
|
||||
|
||||
function quoteCount(str: string): number {
|
||||
let count = 0;
|
||||
let index = 0;
|
||||
|
||||
while ((index = str.indexOf(`"`, index)) !== -1) {
|
||||
count++;
|
||||
index++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function splitMediaTypes(accept: string): string[] {
|
||||
const accepts = accept.split(",");
|
||||
|
||||
let j = 0;
|
||||
for (let i = 1; i < accepts.length; i++) {
|
||||
if (quoteCount(accepts[j]) % 2 === 0) {
|
||||
accepts[++j] = accepts[i];
|
||||
} else {
|
||||
accepts[j] += `,${accepts[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
accepts.length = j + 1;
|
||||
|
||||
return accepts;
|
||||
}
|
||||
|
||||
function splitParameters(str: string): string[] {
|
||||
const parameters = str.split(";");
|
||||
|
||||
let j = 0;
|
||||
for (let i = 1; i < parameters.length; i++) {
|
||||
if (quoteCount(parameters[j]) % 2 === 0) {
|
||||
parameters[++j] = parameters[i];
|
||||
} else {
|
||||
parameters[j] += `;${parameters[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
parameters.length = j + 1;
|
||||
|
||||
return parameters.map((p) => p.trim());
|
||||
}
|
||||
|
||||
function splitKeyValuePair(str: string): [string, string | undefined] {
|
||||
const [key, value] = str.split("=");
|
||||
return [key.toLowerCase(), value];
|
||||
}
|
||||
|
||||
function parseMediaType(
|
||||
str: string,
|
||||
i: number,
|
||||
): MediaTypeSpecificity | undefined {
|
||||
const match = simpleMediaTypeRegExp.exec(str);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params: { [param: string]: string | undefined } = Object.create(null);
|
||||
let q = 1;
|
||||
const [, type, subtype, parameters] = match;
|
||||
|
||||
if (parameters) {
|
||||
const kvps = splitParameters(parameters).map(splitKeyValuePair);
|
||||
|
||||
for (const [key, val] of kvps) {
|
||||
const value = val && val[0] === `"` && val[val.length - 1] === `"`
|
||||
? val.slice(1, val.length - 1)
|
||||
: val;
|
||||
|
||||
if (key === "q" && value) {
|
||||
q = parseFloat(value);
|
||||
break;
|
||||
}
|
||||
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { type, subtype, params, q, i };
|
||||
}
|
||||
|
||||
function parseAccept(accept: string): MediaTypeSpecificity[] {
|
||||
const accepts = splitMediaTypes(accept);
|
||||
|
||||
const mediaTypes: MediaTypeSpecificity[] = [];
|
||||
for (let i = 0; i < accepts.length; i++) {
|
||||
const mediaType = parseMediaType(accepts[i].trim(), i);
|
||||
|
||||
if (mediaType) {
|
||||
mediaTypes.push(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
return mediaTypes;
|
||||
}
|
||||
|
||||
function getFullType(spec: MediaTypeSpecificity) {
|
||||
return `${spec.type}/${spec.subtype}`;
|
||||
}
|
||||
|
||||
function specify(
|
||||
type: string,
|
||||
spec: MediaTypeSpecificity,
|
||||
index: number,
|
||||
): Specificity | undefined {
|
||||
const p = parseMediaType(type, index);
|
||||
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
|
||||
let s = 0;
|
||||
|
||||
if (spec.type.toLowerCase() === p.type.toLowerCase()) {
|
||||
s |= 4;
|
||||
} else if (spec.type !== "*") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (spec.subtype.toLowerCase() === p.subtype.toLowerCase()) {
|
||||
s |= 2;
|
||||
} else if (spec.subtype !== "*") {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(spec.params);
|
||||
if (keys.length) {
|
||||
if (
|
||||
keys.every((key) =>
|
||||
(spec.params[key] || "").toLowerCase() ===
|
||||
(p.params[key] || "").toLowerCase()
|
||||
)
|
||||
) {
|
||||
s |= 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
i: index,
|
||||
o: spec.o,
|
||||
q: spec.q,
|
||||
s,
|
||||
};
|
||||
}
|
||||
|
||||
function getMediaTypePriority(
|
||||
type: string,
|
||||
accepted: MediaTypeSpecificity[],
|
||||
index: number,
|
||||
) {
|
||||
let priority: Specificity = { o: -1, q: 0, s: 0, i: index };
|
||||
|
||||
for (const accepts of accepted) {
|
||||
const spec = specify(type, accepts, index);
|
||||
|
||||
if (
|
||||
spec &&
|
||||
((priority.s || 0) - (spec.s || 0) ||
|
||||
(priority.q || 0) - (spec.q || 0) ||
|
||||
(priority.o || 0) - (spec.o || 0)) < 0
|
||||
) {
|
||||
priority = spec;
|
||||
}
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
export function preferredMediaTypes(
|
||||
accept?: string | null,
|
||||
provided?: string[],
|
||||
): string[] {
|
||||
const accepts = parseAccept(accept === undefined ? "*/*" : accept || "");
|
||||
|
||||
if (!provided) {
|
||||
return accepts
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map(getFullType);
|
||||
}
|
||||
|
||||
const priorities = provided.map((type, index) => {
|
||||
return getMediaTypePriority(type, accepts, index);
|
||||
});
|
||||
|
||||
return priorities
|
||||
.filter(isQuality)
|
||||
.sort(compareSpecs)
|
||||
.map((priority) => provided[priorities.indexOf(priority)]);
|
||||
}
|
||||
+806
@@ -0,0 +1,806 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Provides a iterable map interfaces for managing cookies server side.
|
||||
*
|
||||
* @example
|
||||
* To access the keys in a request and have any set keys available for creating
|
||||
* a response:
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* CookieMap,
|
||||
* mergeHeaders
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* const cookies = new CookieMap(request, { secure: true });
|
||||
* console.log(cookies.get("foo")); // logs "bar"
|
||||
* cookies.set("session", "1234567", { secure: true });
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: mergeHeaders({
|
||||
* "content-type": "text/plain",
|
||||
* }, cookies),
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* To have automatic management of cryptographically signed cookies, you can use
|
||||
* the {@linkcode SecureCookieMap} instead of {@linkcode CookieMap}. The biggest
|
||||
* difference is that the methods operate async in order to be able to support
|
||||
* async signing and validation of cookies:
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* SecureCookieMap,
|
||||
* mergeHeaders,
|
||||
* type KeyRing,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* // The keys must implement the `KeyRing` interface.
|
||||
* declare const keys: KeyRing;
|
||||
*
|
||||
* const cookies = new SecureCookieMap(request, { keys, secure: true });
|
||||
* console.log(await cookies.get("foo")); // logs "bar"
|
||||
* // the cookie will be automatically signed using the supplied key ring.
|
||||
* await cookies.set("session", "1234567");
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: mergeHeaders({
|
||||
* "content-type": "text/plain",
|
||||
* }, cookies),
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* In addition, if you have a {@linkcode Response} or {@linkcode Headers} for a
|
||||
* response at construction of the cookies object, they can be passed and any
|
||||
* set cookies will be added directly to those headers:
|
||||
*
|
||||
* ```ts
|
||||
* import { CookieMap } from "https://deno.land/std@$STD_VERSION/http/cookie_map.ts";
|
||||
*
|
||||
* const request = new Request("https://localhost/", {
|
||||
* headers: { "cookie": "foo=bar; bar=baz;"}
|
||||
* });
|
||||
*
|
||||
* const response = new Response("hello", {
|
||||
* headers: { "content-type": "text/plain" },
|
||||
* });
|
||||
*
|
||||
* const cookies = new CookieMap(request, { response });
|
||||
* console.log(cookies.get("foo")); // logs "bar"
|
||||
* cookies.set("session", "1234567");
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export interface CookieMapOptions {
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface CookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
}
|
||||
|
||||
/** An object which contains a `headers` property which has a value of an
|
||||
* instance of {@linkcode Headers}, like {@linkcode Request} and
|
||||
* {@linkcode Response}. */
|
||||
export interface Headered {
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
export interface Mergeable {
|
||||
[cookieMapHeadersInitSymbol](): [string, string][];
|
||||
}
|
||||
|
||||
export interface SecureCookieMapOptions {
|
||||
/** Keys which will be used to validate and sign cookies. The key ring should
|
||||
* implement the {@linkcode KeyRing} interface. */
|
||||
keys?: KeyRing;
|
||||
|
||||
/** The {@linkcode Response} or the headers that will be used with the
|
||||
* response. When provided, `Set-Cookie` headers will be set in the headers
|
||||
* when cookies are set or deleted in the map.
|
||||
*
|
||||
* An alternative way to extract the headers is to pass the cookie map to the
|
||||
* {@linkcode mergeHeaders} function to merge various sources of the
|
||||
* headers to be provided when creating or updating a response.
|
||||
*/
|
||||
response?: Headered | Headers;
|
||||
|
||||
/** A flag that indicates if the request and response are being handled over
|
||||
* a secure (e.g. HTTPS/TLS) connection.
|
||||
*
|
||||
* @default {false}
|
||||
*/
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapGetOptions {
|
||||
/** Overrides the flag that was set when the instance was created. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
export interface SecureCookieMapSetDeleteOptions {
|
||||
/** The domain to scope the cookie for. */
|
||||
domain?: string;
|
||||
/** When the cookie expires. */
|
||||
expires?: Date;
|
||||
/** A flag that indicates if the cookie is valid over HTTP only. */
|
||||
httpOnly?: boolean;
|
||||
/** Do not error when signing and validating cookies over an insecure
|
||||
* connection. */
|
||||
ignoreInsecure?: boolean;
|
||||
/** Overwrite an existing value. */
|
||||
overwrite?: boolean;
|
||||
/** The path the cookie is valid for. */
|
||||
path?: string;
|
||||
/** Override the flag that was set when the instance was created. */
|
||||
secure?: boolean;
|
||||
/** Set the same-site indicator for a cookie. */
|
||||
sameSite?: "strict" | "lax" | "none" | boolean;
|
||||
/** Override the default behavior of signing the cookie. */
|
||||
signed?: boolean;
|
||||
}
|
||||
|
||||
type CookieAttributes = SecureCookieMapSetDeleteOptions;
|
||||
|
||||
// deno-lint-ignore no-control-regex
|
||||
const FIELD_CONTENT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
|
||||
const KEY_REGEXP = /(?:^|;) *([^=]*)=[^;]*/g;
|
||||
const SAME_SITE_REGEXP = /^(?:lax|none|strict)$/i;
|
||||
|
||||
const matchCache: Record<string, RegExp> = {};
|
||||
function getPattern(name: string): RegExp {
|
||||
if (name in matchCache) {
|
||||
return matchCache[name];
|
||||
}
|
||||
|
||||
return matchCache[name] = new RegExp(
|
||||
`(?:^|;) *${name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")}=([^;]*)`,
|
||||
);
|
||||
}
|
||||
|
||||
function pushCookie(values: string[], cookie: Cookie) {
|
||||
if (cookie.overwrite) {
|
||||
for (let i = values.length - 1; i >= 0; i--) {
|
||||
if (values[i].indexOf(`${cookie.name}=`) === 0) {
|
||||
values.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
values.push(cookie.toHeaderValue());
|
||||
}
|
||||
|
||||
function validateCookieProperty(
|
||||
key: string,
|
||||
value: string | undefined | null,
|
||||
) {
|
||||
if (value && !FIELD_CONTENT_REGEXP.test(value)) {
|
||||
throw new TypeError(`The "${key}" of the cookie (${value}) is invalid.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** An internal abstraction to manage cookies. */
|
||||
class Cookie implements CookieAttributes {
|
||||
domain?: string;
|
||||
expires?: Date;
|
||||
httpOnly = true;
|
||||
maxAge?: number;
|
||||
name: string;
|
||||
overwrite = false;
|
||||
path = "/";
|
||||
sameSite: "strict" | "lax" | "none" | boolean = false;
|
||||
secure = false;
|
||||
signed?: boolean;
|
||||
value: string;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
value: string | null,
|
||||
attributes: CookieAttributes,
|
||||
) {
|
||||
validateCookieProperty("name", name);
|
||||
this.name = name;
|
||||
validateCookieProperty("value", value);
|
||||
this.value = value ?? "";
|
||||
Object.assign(this, attributes);
|
||||
if (!this.value) {
|
||||
this.expires = new Date(0);
|
||||
this.maxAge = undefined;
|
||||
}
|
||||
|
||||
validateCookieProperty("path", this.path);
|
||||
validateCookieProperty("domain", this.domain);
|
||||
if (
|
||||
this.sameSite && typeof this.sameSite === "string" &&
|
||||
!SAME_SITE_REGEXP.test(this.sameSite)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`The "sameSite" of the cookie ("${this.sameSite}") is invalid.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toHeaderValue(): string {
|
||||
let value = this.toString();
|
||||
if (this.maxAge) {
|
||||
this.expires = new Date(Date.now() + (this.maxAge * 1000));
|
||||
}
|
||||
if (this.path) {
|
||||
value += `; path=${this.path}`;
|
||||
}
|
||||
if (this.expires) {
|
||||
value += `; expires=${this.expires.toUTCString()}`;
|
||||
}
|
||||
if (this.domain) {
|
||||
value += `; domain=${this.domain}`;
|
||||
}
|
||||
if (this.sameSite) {
|
||||
value += `; samesite=${
|
||||
this.sameSite === true ? "strict" : this.sameSite.toLowerCase()
|
||||
}`;
|
||||
}
|
||||
if (this.secure) {
|
||||
value += "; secure";
|
||||
}
|
||||
if (this.httpOnly) {
|
||||
value += "; httponly";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.name}=${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Symbol which is used in {@link mergeHeaders} to extract a
|
||||
* `[string | string][]` from an instance to generate the final set of
|
||||
* headers. */
|
||||
export const cookieMapHeadersInitSymbol = Symbol.for(
|
||||
"Deno.std.cookieMap.headersInit",
|
||||
);
|
||||
|
||||
function isMergeable(value: unknown): value is Mergeable {
|
||||
return value != null && typeof value === "object" &&
|
||||
cookieMapHeadersInitSymbol in value;
|
||||
}
|
||||
|
||||
/** Allows merging of various sources of headers into a final set of headers
|
||||
* which can be used in a {@linkcode Response}.
|
||||
*
|
||||
* Note, that unlike when passing a `Response` or {@linkcode Headers} used in a
|
||||
* response to {@linkcode CookieMap} or {@linkcode SecureCookieMap}, merging
|
||||
* will not ensure that there are no other `Set-Cookie` headers from other
|
||||
* sources, it will simply append the various headers together. */
|
||||
export function mergeHeaders(
|
||||
...sources: (Headered | HeadersInit | Mergeable)[]
|
||||
): Headers {
|
||||
const headers = new Headers();
|
||||
for (const source of sources) {
|
||||
let entries: Iterable<[string, string]>;
|
||||
if (source instanceof Headers) {
|
||||
entries = source;
|
||||
} else if ("headers" in source && source.headers instanceof Headers) {
|
||||
entries = source.headers;
|
||||
} else if (isMergeable(source)) {
|
||||
entries = source[cookieMapHeadersInitSymbol]();
|
||||
} else if (Array.isArray(source)) {
|
||||
entries = source as [string, string][];
|
||||
} else {
|
||||
entries = Object.entries(source);
|
||||
}
|
||||
for (const [key, value] of entries) {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
const keys = Symbol("#keys");
|
||||
const requestHeaders = Symbol("#requestHeaders");
|
||||
const responseHeaders = Symbol("#responseHeaders");
|
||||
const isSecure = Symbol("#secure");
|
||||
const requestKeys = Symbol("#requestKeys");
|
||||
|
||||
/** An internal abstract class which provides common functionality for
|
||||
* {@link CookieMap} and {@link SecureCookieMap}. */
|
||||
abstract class CookieMapBase implements Mergeable {
|
||||
[keys]?: string[];
|
||||
[requestHeaders]: Headers;
|
||||
[responseHeaders]: Headers;
|
||||
[isSecure]: boolean;
|
||||
|
||||
[requestKeys](): string[] {
|
||||
if (this[keys]) {
|
||||
return this[keys];
|
||||
}
|
||||
const result = this[keys] = [] as string[];
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return result;
|
||||
}
|
||||
let matches: RegExpExecArray | null;
|
||||
while ((matches = KEY_REGEXP.exec(header))) {
|
||||
const [, key] = matches;
|
||||
result.push(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(request: Headers | Headered, options: CookieMapOptions) {
|
||||
this[requestHeaders] = "headers" in request ? request.headers : request;
|
||||
const { secure = false, response = new Headers() } = options;
|
||||
this[responseHeaders] = "headers" in response ? response.headers : response;
|
||||
this[isSecure] = secure;
|
||||
}
|
||||
|
||||
/** A method used by {@linkcode mergeHeaders} to be able to merge
|
||||
* headers from various sources when forming a {@linkcode Response}. */
|
||||
[cookieMapHeadersInitSymbol](): [string, string][] {
|
||||
const init: [string, string][] = [];
|
||||
for (const [key, value] of this[responseHeaders]) {
|
||||
if (key === "set-cookie") {
|
||||
init.push([key, value]);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return `${this.constructor.name} []`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect([], newOptions)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection.
|
||||
*
|
||||
* The methods and properties align to {@linkcode Map}. When constructing a
|
||||
* {@linkcode Request} or {@linkcode Headers} from the request need to be
|
||||
* provided, as well as optionally the {@linkcode Response} or `Headers` for the
|
||||
* response can be provided. Alternatively the {@linkcode mergeHeaders}
|
||||
* function can be used to generate a final set of headers for sending in the
|
||||
* response. */
|
||||
export class CookieMap extends CookieMapBase {
|
||||
/** Contains the number of valid cookies in the request headers. */
|
||||
get size(): number {
|
||||
return [...this].length;
|
||||
}
|
||||
|
||||
constructor(request: Headers | Headered, options: CookieMapOptions = {}) {
|
||||
super(request, options);
|
||||
}
|
||||
|
||||
/** Deletes all the cookies from the {@linkcode Request} in the response. */
|
||||
clear(options: CookieMapSetDeleteOptions = {}) {
|
||||
for (const key of this.keys()) {
|
||||
this.set(key, null, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`.
|
||||
*/
|
||||
delete(key: string, options: CookieMapSetDeleteOptions = {}): boolean {
|
||||
this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return the value of a matching key present in the {@linkcode Request}. If
|
||||
* the key is not present `undefined` is returned. */
|
||||
get(key: string): string | undefined {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return undefined;
|
||||
}
|
||||
const match = headerValue.match(getPattern(key));
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, value] = match;
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Returns `true` if the matching key is present in the {@linkcode Request},
|
||||
* otherwise `false`. */
|
||||
has(key: string): boolean {
|
||||
const headerValue = this[requestHeaders].get("cookie");
|
||||
if (!headerValue) {
|
||||
return false;
|
||||
}
|
||||
return getPattern(key).test(headerValue);
|
||||
}
|
||||
|
||||
/** Set a named cookie in the response. The optional
|
||||
* {@linkcode CookieMapSetDeleteOptions} are applied to the cookie being set.
|
||||
*/
|
||||
set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: CookieMapSetDeleteOptions = {},
|
||||
): this {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const values: string[] = [];
|
||||
for (const [key, value] of resHeaders) {
|
||||
if (key === "set-cookie") {
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(values, cookie);
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const value of values) {
|
||||
resHeaders.append("set-cookie", value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. This is an alias of the `[Symbol.iterator]` method
|
||||
* present on the class. */
|
||||
entries(): IterableIterator<[string, string]> {
|
||||
return this[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*keys(): IterableIterator<string> {
|
||||
for (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*values(): IterableIterator<string> {
|
||||
for (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the cookie keys and values that are present in the
|
||||
* {@linkcode Request}. */
|
||||
*[Symbol.iterator](): IterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Types of data that can be signed cryptographically. */
|
||||
export type Data = string | number[] | ArrayBuffer | Uint8Array;
|
||||
|
||||
/** An interface which describes the methods that {@linkcode SecureCookieMap}
|
||||
* uses to sign and verify cookies. */
|
||||
export interface KeyRing {
|
||||
/** Given a set of data and a digest, return the key index of the key used
|
||||
* to sign the data. The index is 0 based. A non-negative number indices the
|
||||
* digest is valid and a key was found. */
|
||||
indexOf(data: Data, digest: string): Promise<number> | number;
|
||||
/** Sign the data, returning a string based digest of the data. */
|
||||
sign(data: Data): Promise<string> | string;
|
||||
/** Verifies the digest matches the provided data, indicating the data was
|
||||
* signed by the keyring and has not been tampered with. */
|
||||
verify(data: Data, digest: string): Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
/** Provides an way to manage cookies in a request and response on the server
|
||||
* as a single iterable collection, as well as the ability to sign and verify
|
||||
* cookies to prevent tampering.
|
||||
*
|
||||
* The methods and properties align to {@linkcode Map}, but due to the need to
|
||||
* support asynchronous cryptographic keys, all the APIs operate async. When
|
||||
* constructing a {@linkcode Request} or {@linkcode Headers} from the request
|
||||
* need to be provided, as well as optionally the {@linkcode Response} or
|
||||
* `Headers` for the response can be provided. Alternatively the
|
||||
* {@linkcode mergeHeaders} function can be used to generate a final set
|
||||
* of headers for sending in the response.
|
||||
*
|
||||
* On construction, the optional set of keys implementing the
|
||||
* {@linkcode KeyRing} interface. While it is optional, if you don't plan to use
|
||||
* keys, you might want to consider using just the {@linkcode CookieMap}.
|
||||
*
|
||||
* @example
|
||||
*/
|
||||
export class SecureCookieMap extends CookieMapBase {
|
||||
#keyRing?: KeyRing;
|
||||
|
||||
/** Is set to a promise which resolves with the number of cookies in the
|
||||
* {@linkcode Request}. */
|
||||
get size(): Promise<number> {
|
||||
return (async () => {
|
||||
let size = 0;
|
||||
for await (const _ of this) {
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
})();
|
||||
}
|
||||
|
||||
constructor(
|
||||
request: Headers | Headered,
|
||||
options: SecureCookieMapOptions = {},
|
||||
) {
|
||||
super(request, options);
|
||||
const { keys } = options;
|
||||
this.#keyRing = keys;
|
||||
}
|
||||
|
||||
/** Sets all cookies in the {@linkcode Request} to be deleted in the
|
||||
* response. */
|
||||
async clear(options: SecureCookieMapSetDeleteOptions) {
|
||||
for await (const key of this.keys()) {
|
||||
await this.set(key, null, options);
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie to be deleted in the response.
|
||||
*
|
||||
* This is a convenience function for `set(key, null, options?)`. */
|
||||
async delete(
|
||||
key: string,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<boolean> {
|
||||
await this.set(key, null, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Get the value of a cookie from the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `undefined` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async get(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<string | undefined> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const [, value] = match;
|
||||
if (!signed) {
|
||||
return value;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return;
|
||||
}
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if the key is in the {@linkcode Request}.
|
||||
*
|
||||
* If the cookie is signed, and the signature is invalid, `false` will be
|
||||
* returned and the cookie will be set to be deleted in the response. If the
|
||||
* cookie is using an "old" key from the keyring, the cookie will be re-signed
|
||||
* with the current key and be added to the response to be updated. */
|
||||
async has(
|
||||
key: string,
|
||||
options: SecureCookieMapGetOptions = {},
|
||||
): Promise<boolean> {
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
const nameSig = `${key}.sig`;
|
||||
|
||||
const header = this[requestHeaders].get("cookie");
|
||||
if (!header) {
|
||||
return false;
|
||||
}
|
||||
const match = header.match(getPattern(key));
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
if (!signed) {
|
||||
return true;
|
||||
}
|
||||
const digest = await this.get(nameSig, { signed: false });
|
||||
if (!digest) {
|
||||
return false;
|
||||
}
|
||||
const [, value] = match;
|
||||
const data = `${key}=${value}`;
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("key ring required for signed cookies");
|
||||
}
|
||||
const index = await this.#keyRing.indexOf(data, digest);
|
||||
|
||||
if (index < 0) {
|
||||
await this.delete(nameSig, { path: "/", signed: false });
|
||||
return false;
|
||||
} else {
|
||||
if (index) {
|
||||
await this.set(nameSig, await this.#keyRing.sign(data), {
|
||||
signed: false,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Set a cookie in the response headers.
|
||||
*
|
||||
* If there was a keyring set, cookies will be automatically signed, unless
|
||||
* overridden by the passed options. Cookies can be deleted by setting the
|
||||
* value to `null`. */
|
||||
async set(
|
||||
key: string,
|
||||
value: string | null,
|
||||
options: SecureCookieMapSetDeleteOptions = {},
|
||||
): Promise<this> {
|
||||
const resHeaders = this[responseHeaders];
|
||||
const headers: string[] = [];
|
||||
for (const [key, value] of resHeaders.entries()) {
|
||||
if (key === "set-cookie") {
|
||||
headers.push(value);
|
||||
}
|
||||
}
|
||||
const secure = this[isSecure];
|
||||
const signed = options.signed ?? !!this.#keyRing;
|
||||
|
||||
if (!secure && options.secure && !options.ignoreInsecure) {
|
||||
throw new TypeError(
|
||||
"Cannot send secure cookie over unencrypted connection.",
|
||||
);
|
||||
}
|
||||
|
||||
const cookie = new Cookie(key, value, options);
|
||||
cookie.secure = options.secure ?? secure;
|
||||
pushCookie(headers, cookie);
|
||||
|
||||
if (signed) {
|
||||
if (!this.#keyRing) {
|
||||
throw new TypeError("keys required for signed cookies.");
|
||||
}
|
||||
cookie.value = await this.#keyRing.sign(cookie.toString());
|
||||
cookie.name += ".sig";
|
||||
pushCookie(headers, cookie);
|
||||
}
|
||||
|
||||
resHeaders.delete("set-cookie");
|
||||
for (const header of headers) {
|
||||
resHeaders.append("set-cookie", header);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
entries(): AsyncIterableIterator<[string, string]> {
|
||||
return this[Symbol.asyncIterator]();
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the key of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie keys are
|
||||
* returned. */
|
||||
async *keys(): AsyncIterableIterator<string> {
|
||||
for await (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the request's cookies, yielding up the value of each cookie.
|
||||
*
|
||||
* If a keyring was provided, only properly signed cookie values are
|
||||
* returned. */
|
||||
async *values(): AsyncIterableIterator<string> {
|
||||
for await (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterate over the {@linkcode Request} cookies, yielding up a tuple
|
||||
* containing the key and value of each cookie.
|
||||
*
|
||||
* If a key ring was provided, only properly signed cookie keys and values are
|
||||
* returned. */
|
||||
async *[Symbol.asyncIterator](): AsyncIterableIterator<[string, string]> {
|
||||
const keys = this[requestKeys]();
|
||||
for (const key of keys) {
|
||||
const value = await this.get(key);
|
||||
if (value) {
|
||||
yield [key, value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/** Provides functions for dealing with and matching ETags, including
|
||||
* {@linkcode calculate} to calculate an etag for a given entity,
|
||||
* {@linkcode ifMatch} for validating if an ETag matches against a `If-Match`
|
||||
* header and {@linkcode ifNoneMatch} for validating an Etag against an
|
||||
* `If-None-Match` header.
|
||||
*
|
||||
* See further information on the `ETag` header on
|
||||
* [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { encode as base64Encode } from "../encoding/base64.ts";
|
||||
|
||||
/** Just the part of `Deno.FileInfo` that is required to calculate an `ETag`,
|
||||
* so partial or user generated file information can be passed. */
|
||||
export interface FileInfo {
|
||||
mtime: Date | null;
|
||||
size: number;
|
||||
}
|
||||
|
||||
type Entity = string | Uint8Array | FileInfo;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const DEFAULT_ALGORITHM: AlgorithmIdentifier = "SHA-256";
|
||||
|
||||
export interface ETagOptions {
|
||||
/** A digest algorithm to use to calculate the etag. Defaults to
|
||||
* `"FNV32A"`. */
|
||||
algorithm?: AlgorithmIdentifier;
|
||||
|
||||
/** Override the default behavior of calculating the `ETag`, either forcing
|
||||
* a tag to be labelled weak or not. */
|
||||
weak?: boolean;
|
||||
}
|
||||
|
||||
function isFileInfo(value: unknown): value is FileInfo {
|
||||
return Boolean(
|
||||
value && typeof value === "object" && "mtime" in value && "size" in value,
|
||||
);
|
||||
}
|
||||
|
||||
async function calcEntity(
|
||||
entity: string | Uint8Array,
|
||||
{ algorithm = DEFAULT_ALGORITHM }: ETagOptions,
|
||||
) {
|
||||
// a short circuit for zero length entities
|
||||
if (entity.length === 0) {
|
||||
return `0-47DEQpj8HBSa+/TImW+5JCeuQeR`;
|
||||
}
|
||||
|
||||
if (typeof entity === "string") {
|
||||
entity = encoder.encode(entity);
|
||||
}
|
||||
|
||||
const hash = base64Encode(await crypto.subtle.digest(algorithm, entity))
|
||||
.substring(0, 27);
|
||||
|
||||
return `${entity.length.toString(16)}-${hash}`;
|
||||
}
|
||||
|
||||
async function calcFileInfo(
|
||||
fileInfo: FileInfo,
|
||||
{ algorithm = DEFAULT_ALGORITHM }: ETagOptions,
|
||||
) {
|
||||
if (fileInfo.mtime) {
|
||||
const hash = base64Encode(
|
||||
await crypto.subtle.digest(
|
||||
algorithm,
|
||||
encoder.encode(fileInfo.mtime.toJSON()),
|
||||
),
|
||||
).substring(0, 27);
|
||||
return `${fileInfo.size.toString(16)}-${hash}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Calculate an ETag for an entity. When the entity is a specific set of data
|
||||
* it will be fingerprinted as a "strong" tag, otherwise if it is just file
|
||||
* information, it will be calculated as a weak tag.
|
||||
*
|
||||
* ```ts
|
||||
* import { calculate } from "https://deno.land/std@$STD_VERSION/http/etag.ts";
|
||||
* import { assert } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"
|
||||
*
|
||||
* const body = "hello deno!";
|
||||
*
|
||||
* const etag = await calculate(body);
|
||||
* assert(etag);
|
||||
*
|
||||
* const res = new Response(body, { headers: { etag } });
|
||||
* ```
|
||||
*/
|
||||
export async function calculate(
|
||||
entity: Entity,
|
||||
options: ETagOptions = {},
|
||||
): Promise<string | undefined> {
|
||||
const weak = options.weak ?? isFileInfo(entity);
|
||||
const tag =
|
||||
await (isFileInfo(entity)
|
||||
? calcFileInfo(entity, options)
|
||||
: calcEntity(entity, options));
|
||||
|
||||
return tag ? weak ? `W/"${tag}"` : `"${tag}"` : undefined;
|
||||
}
|
||||
|
||||
/** A helper function that takes the value from the `If-Match` header and a
|
||||
* calculated etag for the target. By using strong comparison, return `true` if
|
||||
* the values match, otherwise `false`.
|
||||
*
|
||||
* See MDN's [`If-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match)
|
||||
* article for more information on how to use this function.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* calculate,
|
||||
* ifMatch,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/etag.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
* import { assert } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"
|
||||
*
|
||||
* const body = "hello deno!";
|
||||
*
|
||||
* await serve(async (req) => {
|
||||
* const ifMatchValue = req.headers.get("if-match");
|
||||
* const etag = await calculate(body);
|
||||
* assert(etag);
|
||||
* if (!ifMatchValue || ifMatch(ifMatchValue, etag)) {
|
||||
* return new Response(body, { status: 200, headers: { etag } });
|
||||
* } else {
|
||||
* return new Response(null, { status: 412, statusText: "Precondition Failed"});
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function ifMatch(
|
||||
value: string | null,
|
||||
etag: string | undefined,
|
||||
): boolean {
|
||||
// Weak tags cannot be matched and return false.
|
||||
if (!value || !etag || etag.startsWith("W/")) {
|
||||
return false;
|
||||
}
|
||||
if (value.trim() === "*") {
|
||||
return true;
|
||||
}
|
||||
const tags = value.split(/\s*,\s*/);
|
||||
return tags.includes(etag);
|
||||
}
|
||||
|
||||
/** A helper function that takes the value from the `If-None-Match` header and
|
||||
* a calculated etag for the target entity and returns `false` if the etag for
|
||||
* the entity matches the supplied value, otherwise `true`.
|
||||
*
|
||||
* See MDN's [`If-None-Match`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match)
|
||||
* article for more information on how to use this function.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* calculate,
|
||||
* ifNoneMatch,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/etag.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
* import { assert } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"
|
||||
*
|
||||
* const body = "hello deno!";
|
||||
*
|
||||
* await serve(async (req) => {
|
||||
* const ifNoneMatchValue = req.headers.get("if-none-match");
|
||||
* const etag = await calculate(body);
|
||||
* assert(etag);
|
||||
* if (!ifNoneMatch(ifNoneMatchValue, etag)) {
|
||||
* return new Response(null, { status: 304, headers: { etag } });
|
||||
* } else {
|
||||
* return new Response(body, { status: 200, headers: { etag } });
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function ifNoneMatch(
|
||||
value: string | null,
|
||||
etag: string | undefined,
|
||||
): boolean {
|
||||
if (!value || !etag) {
|
||||
return true;
|
||||
}
|
||||
if (value.trim() === "*") {
|
||||
return false;
|
||||
}
|
||||
etag = etag.startsWith("W/") ? etag.slice(2) : etag;
|
||||
const tags = value.split(/\s*,\s*/).map((tag) =>
|
||||
tag.startsWith("W/") ? tag.slice(2) : tag
|
||||
);
|
||||
return !tags.includes(etag);
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** A collection of HTTP errors and utilities.
|
||||
*
|
||||
* The export {@linkcode errors} contains an individual class that extends
|
||||
* {@linkcode HttpError} which makes handling HTTP errors in a structured way.
|
||||
*
|
||||
* The function {@linkcode createHttpError} provides a way to create instances
|
||||
* of errors in a factory pattern.
|
||||
*
|
||||
* The function {@linkcode isHttpError} is a type guard that will narrow a value
|
||||
* to an `HttpError` instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors, isHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
*
|
||||
* try {
|
||||
* throw new errors.NotFound();
|
||||
* } catch (e) {
|
||||
* if (isHttpError(e)) {
|
||||
* const response = new Response(e.message, { status: e.status });
|
||||
* } else {
|
||||
* throw e;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createHttpError } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
* import { Status } from "https://deno.land/std@$STD_VERSION/http/http_status.ts";
|
||||
*
|
||||
* try {
|
||||
* throw createHttpError(
|
||||
* Status.BadRequest,
|
||||
* "The request was bad.",
|
||||
* { expose: false }
|
||||
* );
|
||||
* } catch (e) {
|
||||
* // handle errors
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
type ErrorStatus,
|
||||
isClientErrorStatus,
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from "./http_status.ts";
|
||||
|
||||
const ERROR_STATUS_MAP = {
|
||||
"BadRequest": 400,
|
||||
"Unauthorized": 401,
|
||||
"PaymentRequired": 402,
|
||||
"Forbidden": 403,
|
||||
"NotFound": 404,
|
||||
"MethodNotAllowed": 405,
|
||||
"NotAcceptable": 406,
|
||||
"ProxyAuthRequired": 407,
|
||||
"RequestTimeout": 408,
|
||||
"Conflict": 409,
|
||||
"Gone": 410,
|
||||
"LengthRequired": 411,
|
||||
"PreconditionFailed": 412,
|
||||
"RequestEntityTooLarge": 413,
|
||||
"RequestURITooLong": 414,
|
||||
"UnsupportedMediaType": 415,
|
||||
"RequestedRangeNotSatisfiable": 416,
|
||||
"ExpectationFailed": 417,
|
||||
"Teapot": 418,
|
||||
"MisdirectedRequest": 421,
|
||||
"UnprocessableEntity": 422,
|
||||
"Locked": 423,
|
||||
"FailedDependency": 424,
|
||||
"UpgradeRequired": 426,
|
||||
"PreconditionRequired": 428,
|
||||
"TooManyRequests": 429,
|
||||
"RequestHeaderFieldsTooLarge": 431,
|
||||
"UnavailableForLegalReasons": 451,
|
||||
"InternalServerError": 500,
|
||||
"NotImplemented": 501,
|
||||
"BadGateway": 502,
|
||||
"ServiceUnavailable": 503,
|
||||
"GatewayTimeout": 504,
|
||||
"HTTPVersionNotSupported": 505,
|
||||
"VariantAlsoNegotiates": 506,
|
||||
"InsufficientStorage": 507,
|
||||
"LoopDetected": 508,
|
||||
"NotExtended": 510,
|
||||
"NetworkAuthenticationRequired": 511,
|
||||
} as const;
|
||||
|
||||
export type ErrorStatusKeys = keyof typeof ERROR_STATUS_MAP;
|
||||
|
||||
export interface HttpErrorOptions extends ErrorOptions {
|
||||
expose?: boolean;
|
||||
headers?: HeadersInit;
|
||||
}
|
||||
|
||||
/** The base class that all derivative HTTP extend, providing a `status` and an
|
||||
* `expose` property. */
|
||||
export class HttpError extends Error {
|
||||
#status: ErrorStatus = Status.InternalServerError;
|
||||
#expose: boolean;
|
||||
#headers?: Headers;
|
||||
constructor(
|
||||
message = "Http Error",
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
this.#expose = options?.expose === undefined
|
||||
? isClientErrorStatus(this.status)
|
||||
: options.expose;
|
||||
if (options?.headers) {
|
||||
this.#headers = new Headers(options.headers);
|
||||
}
|
||||
}
|
||||
/** A flag to indicate if the internals of the error, like the stack, should
|
||||
* be exposed to a client, or if they are "private" and should not be leaked.
|
||||
* By default, all client errors are `true` and all server errors are
|
||||
* `false`. */
|
||||
get expose(): boolean {
|
||||
return this.#expose;
|
||||
}
|
||||
/** The optional headers object that is set on the error. */
|
||||
get headers(): Headers | undefined {
|
||||
return this.#headers;
|
||||
}
|
||||
/** The error status that is set on the error. */
|
||||
get status(): ErrorStatus {
|
||||
return this.#status;
|
||||
}
|
||||
}
|
||||
|
||||
function createHttpErrorConstructor(status: ErrorStatus): typeof HttpError {
|
||||
const name = `${Status[status]}Error`;
|
||||
const ErrorCtor = class extends HttpError {
|
||||
constructor(
|
||||
message = STATUS_TEXT[status],
|
||||
options?: HttpErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
Object.defineProperty(this, "name", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: name,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
override get status() {
|
||||
return status;
|
||||
}
|
||||
};
|
||||
return ErrorCtor;
|
||||
}
|
||||
|
||||
/**
|
||||
* A namespace that contains each error constructor. Each error extends
|
||||
* `HTTPError` and provides `.status` and `.expose` properties, where the
|
||||
* `.status` will be an error `Status` value and `.expose` indicates if
|
||||
* information, like a stack trace, should be shared in the response.
|
||||
*
|
||||
* By default, `.expose` is set to false in server errors, and true for client
|
||||
* errors.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { errors } from "https://deno.land/std@$STD_VERSION/http/http_errors.ts";
|
||||
*
|
||||
* throw new errors.InternalServerError("Ooops!");
|
||||
* ```
|
||||
*/
|
||||
export const errors: Record<ErrorStatusKeys, typeof HttpError> = {} as Record<
|
||||
ErrorStatusKeys,
|
||||
typeof HttpError
|
||||
>;
|
||||
|
||||
for (const [key, value] of Object.entries(ERROR_STATUS_MAP)) {
|
||||
errors[key as ErrorStatusKeys] = createHttpErrorConstructor(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function which provides a way to create errors. It takes up to 3
|
||||
* arguments, the error `Status`, an message, which defaults to the status text
|
||||
* and error options, which incudes the `expose` property to set the `.expose`
|
||||
* value on the error.
|
||||
*/
|
||||
export function createHttpError(
|
||||
status: ErrorStatus = Status.InternalServerError,
|
||||
message?: string,
|
||||
options?: HttpErrorOptions,
|
||||
): HttpError {
|
||||
return new errors[Status[status] as ErrorStatusKeys](message, options);
|
||||
}
|
||||
|
||||
/** A type guard that determines if the value is an HttpError or not. */
|
||||
export function isHttpError(value: unknown): value is HttpError {
|
||||
return value instanceof HttpError;
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Contains the enum {@linkcode Status} which enumerates standard HTTP status
|
||||
* codes and provides several type guards for handling status codes with type
|
||||
* safety.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* Status,
|
||||
* STATUS_TEXT,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/http_status.ts";
|
||||
*
|
||||
* console.log(Status.NotFound); //=> 404
|
||||
* console.log(STATUS_TEXT[Status.NotFound]); //=> "Not Found"
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import { isErrorStatus } from "https://deno.land/std@$STD_VERSION/http/http_status.ts";
|
||||
*
|
||||
* const res = await fetch("https://example.com/");
|
||||
*
|
||||
* if (isErrorStatus(res.status)) {
|
||||
* // error handling here...
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Standard HTTP status codes. */
|
||||
export enum Status {
|
||||
/** RFC 7231, 6.2.1 */
|
||||
Continue = 100,
|
||||
/** RFC 7231, 6.2.2 */
|
||||
SwitchingProtocols = 101,
|
||||
/** RFC 2518, 10.1 */
|
||||
Processing = 102,
|
||||
/** RFC 8297 **/
|
||||
EarlyHints = 103,
|
||||
|
||||
/** RFC 7231, 6.3.1 */
|
||||
OK = 200,
|
||||
/** RFC 7231, 6.3.2 */
|
||||
Created = 201,
|
||||
/** RFC 7231, 6.3.3 */
|
||||
Accepted = 202,
|
||||
/** RFC 7231, 6.3.4 */
|
||||
NonAuthoritativeInfo = 203,
|
||||
/** RFC 7231, 6.3.5 */
|
||||
NoContent = 204,
|
||||
/** RFC 7231, 6.3.6 */
|
||||
ResetContent = 205,
|
||||
/** RFC 7233, 4.1 */
|
||||
PartialContent = 206,
|
||||
/** RFC 4918, 11.1 */
|
||||
MultiStatus = 207,
|
||||
/** RFC 5842, 7.1 */
|
||||
AlreadyReported = 208,
|
||||
/** RFC 3229, 10.4.1 */
|
||||
IMUsed = 226,
|
||||
|
||||
/** RFC 7231, 6.4.1 */
|
||||
MultipleChoices = 300,
|
||||
/** RFC 7231, 6.4.2 */
|
||||
MovedPermanently = 301,
|
||||
/** RFC 7231, 6.4.3 */
|
||||
Found = 302,
|
||||
/** RFC 7231, 6.4.4 */
|
||||
SeeOther = 303,
|
||||
/** RFC 7232, 4.1 */
|
||||
NotModified = 304,
|
||||
/** RFC 7231, 6.4.5 */
|
||||
UseProxy = 305,
|
||||
/** RFC 7231, 6.4.7 */
|
||||
TemporaryRedirect = 307,
|
||||
/** RFC 7538, 3 */
|
||||
PermanentRedirect = 308,
|
||||
|
||||
/** RFC 7231, 6.5.1 */
|
||||
BadRequest = 400,
|
||||
/** RFC 7235, 3.1 */
|
||||
Unauthorized = 401,
|
||||
/** RFC 7231, 6.5.2 */
|
||||
PaymentRequired = 402,
|
||||
/** RFC 7231, 6.5.3 */
|
||||
Forbidden = 403,
|
||||
/** RFC 7231, 6.5.4 */
|
||||
NotFound = 404,
|
||||
/** RFC 7231, 6.5.5 */
|
||||
MethodNotAllowed = 405,
|
||||
/** RFC 7231, 6.5.6 */
|
||||
NotAcceptable = 406,
|
||||
/** RFC 7235, 3.2 */
|
||||
ProxyAuthRequired = 407,
|
||||
/** RFC 7231, 6.5.7 */
|
||||
RequestTimeout = 408,
|
||||
/** RFC 7231, 6.5.8 */
|
||||
Conflict = 409,
|
||||
/** RFC 7231, 6.5.9 */
|
||||
Gone = 410,
|
||||
/** RFC 7231, 6.5.10 */
|
||||
LengthRequired = 411,
|
||||
/** RFC 7232, 4.2 */
|
||||
PreconditionFailed = 412,
|
||||
/** RFC 7231, 6.5.11 */
|
||||
RequestEntityTooLarge = 413,
|
||||
/** RFC 7231, 6.5.12 */
|
||||
RequestURITooLong = 414,
|
||||
/** RFC 7231, 6.5.13 */
|
||||
UnsupportedMediaType = 415,
|
||||
/** RFC 7233, 4.4 */
|
||||
RequestedRangeNotSatisfiable = 416,
|
||||
/** RFC 7231, 6.5.14 */
|
||||
ExpectationFailed = 417,
|
||||
/** RFC 7168, 2.3.3 */
|
||||
Teapot = 418,
|
||||
/** RFC 7540, 9.1.2 */
|
||||
MisdirectedRequest = 421,
|
||||
/** RFC 4918, 11.2 */
|
||||
UnprocessableEntity = 422,
|
||||
/** RFC 4918, 11.3 */
|
||||
Locked = 423,
|
||||
/** RFC 4918, 11.4 */
|
||||
FailedDependency = 424,
|
||||
/** RFC 8470, 5.2 */
|
||||
TooEarly = 425,
|
||||
/** RFC 7231, 6.5.15 */
|
||||
UpgradeRequired = 426,
|
||||
/** RFC 6585, 3 */
|
||||
PreconditionRequired = 428,
|
||||
/** RFC 6585, 4 */
|
||||
TooManyRequests = 429,
|
||||
/** RFC 6585, 5 */
|
||||
RequestHeaderFieldsTooLarge = 431,
|
||||
/** RFC 7725, 3 */
|
||||
UnavailableForLegalReasons = 451,
|
||||
|
||||
/** RFC 7231, 6.6.1 */
|
||||
InternalServerError = 500,
|
||||
/** RFC 7231, 6.6.2 */
|
||||
NotImplemented = 501,
|
||||
/** RFC 7231, 6.6.3 */
|
||||
BadGateway = 502,
|
||||
/** RFC 7231, 6.6.4 */
|
||||
ServiceUnavailable = 503,
|
||||
/** RFC 7231, 6.6.5 */
|
||||
GatewayTimeout = 504,
|
||||
/** RFC 7231, 6.6.6 */
|
||||
HTTPVersionNotSupported = 505,
|
||||
/** RFC 2295, 8.1 */
|
||||
VariantAlsoNegotiates = 506,
|
||||
/** RFC 4918, 11.5 */
|
||||
InsufficientStorage = 507,
|
||||
/** RFC 5842, 7.2 */
|
||||
LoopDetected = 508,
|
||||
/** RFC 2774, 7 */
|
||||
NotExtended = 510,
|
||||
/** RFC 6585, 6 */
|
||||
NetworkAuthenticationRequired = 511,
|
||||
}
|
||||
|
||||
/** A record of all the status codes text. */
|
||||
export const STATUS_TEXT: Readonly<Record<Status, string>> = {
|
||||
[Status.Accepted]: "Accepted",
|
||||
[Status.AlreadyReported]: "Already Reported",
|
||||
[Status.BadGateway]: "Bad Gateway",
|
||||
[Status.BadRequest]: "Bad Request",
|
||||
[Status.Conflict]: "Conflict",
|
||||
[Status.Continue]: "Continue",
|
||||
[Status.Created]: "Created",
|
||||
[Status.EarlyHints]: "Early Hints",
|
||||
[Status.ExpectationFailed]: "Expectation Failed",
|
||||
[Status.FailedDependency]: "Failed Dependency",
|
||||
[Status.Forbidden]: "Forbidden",
|
||||
[Status.Found]: "Found",
|
||||
[Status.GatewayTimeout]: "Gateway Timeout",
|
||||
[Status.Gone]: "Gone",
|
||||
[Status.HTTPVersionNotSupported]: "HTTP Version Not Supported",
|
||||
[Status.IMUsed]: "IM Used",
|
||||
[Status.InsufficientStorage]: "Insufficient Storage",
|
||||
[Status.InternalServerError]: "Internal Server Error",
|
||||
[Status.LengthRequired]: "Length Required",
|
||||
[Status.Locked]: "Locked",
|
||||
[Status.LoopDetected]: "Loop Detected",
|
||||
[Status.MethodNotAllowed]: "Method Not Allowed",
|
||||
[Status.MisdirectedRequest]: "Misdirected Request",
|
||||
[Status.MovedPermanently]: "Moved Permanently",
|
||||
[Status.MultiStatus]: "Multi Status",
|
||||
[Status.MultipleChoices]: "Multiple Choices",
|
||||
[Status.NetworkAuthenticationRequired]: "Network Authentication Required",
|
||||
[Status.NoContent]: "No Content",
|
||||
[Status.NonAuthoritativeInfo]: "Non Authoritative Info",
|
||||
[Status.NotAcceptable]: "Not Acceptable",
|
||||
[Status.NotExtended]: "Not Extended",
|
||||
[Status.NotFound]: "Not Found",
|
||||
[Status.NotImplemented]: "Not Implemented",
|
||||
[Status.NotModified]: "Not Modified",
|
||||
[Status.OK]: "OK",
|
||||
[Status.PartialContent]: "Partial Content",
|
||||
[Status.PaymentRequired]: "Payment Required",
|
||||
[Status.PermanentRedirect]: "Permanent Redirect",
|
||||
[Status.PreconditionFailed]: "Precondition Failed",
|
||||
[Status.PreconditionRequired]: "Precondition Required",
|
||||
[Status.Processing]: "Processing",
|
||||
[Status.ProxyAuthRequired]: "Proxy Auth Required",
|
||||
[Status.RequestEntityTooLarge]: "Request Entity Too Large",
|
||||
[Status.RequestHeaderFieldsTooLarge]: "Request Header Fields Too Large",
|
||||
[Status.RequestTimeout]: "Request Timeout",
|
||||
[Status.RequestURITooLong]: "Request URI Too Long",
|
||||
[Status.RequestedRangeNotSatisfiable]: "Requested Range Not Satisfiable",
|
||||
[Status.ResetContent]: "Reset Content",
|
||||
[Status.SeeOther]: "See Other",
|
||||
[Status.ServiceUnavailable]: "Service Unavailable",
|
||||
[Status.SwitchingProtocols]: "Switching Protocols",
|
||||
[Status.Teapot]: "I'm a teapot",
|
||||
[Status.TemporaryRedirect]: "Temporary Redirect",
|
||||
[Status.TooEarly]: "Too Early",
|
||||
[Status.TooManyRequests]: "Too Many Requests",
|
||||
[Status.Unauthorized]: "Unauthorized",
|
||||
[Status.UnavailableForLegalReasons]: "Unavailable For Legal Reasons",
|
||||
[Status.UnprocessableEntity]: "Unprocessable Entity",
|
||||
[Status.UnsupportedMediaType]: "Unsupported Media Type",
|
||||
[Status.UpgradeRequired]: "Upgrade Required",
|
||||
[Status.UseProxy]: "Use Proxy",
|
||||
[Status.VariantAlsoNegotiates]: "Variant Also Negotiates",
|
||||
};
|
||||
|
||||
/** An HTTP status that is a informational (1XX). */
|
||||
export type InformationalStatus =
|
||||
| Status.Continue
|
||||
| Status.SwitchingProtocols
|
||||
| Status.Processing
|
||||
| Status.EarlyHints;
|
||||
|
||||
/** An HTTP status that is a success (2XX). */
|
||||
export type SuccessfulStatus =
|
||||
| Status.OK
|
||||
| Status.Created
|
||||
| Status.Accepted
|
||||
| Status.NonAuthoritativeInfo
|
||||
| Status.NoContent
|
||||
| Status.ResetContent
|
||||
| Status.PartialContent
|
||||
| Status.MultiStatus
|
||||
| Status.AlreadyReported
|
||||
| Status.IMUsed;
|
||||
|
||||
/** An HTTP status that is a redirect (3XX). */
|
||||
export type RedirectStatus =
|
||||
| Status.MultipleChoices // 300
|
||||
| Status.MovedPermanently // 301
|
||||
| Status.Found // 302
|
||||
| Status.SeeOther // 303
|
||||
| Status.UseProxy // 305 - DEPRECATED
|
||||
| Status.TemporaryRedirect // 307
|
||||
| Status.PermanentRedirect; // 308
|
||||
|
||||
/** An HTTP status that is a client error (4XX). */
|
||||
export type ClientErrorStatus =
|
||||
| Status.BadRequest
|
||||
| Status.Unauthorized
|
||||
| Status.PaymentRequired
|
||||
| Status.Forbidden
|
||||
| Status.NotFound
|
||||
| Status.MethodNotAllowed
|
||||
| Status.NotAcceptable
|
||||
| Status.ProxyAuthRequired
|
||||
| Status.RequestTimeout
|
||||
| Status.Conflict
|
||||
| Status.Gone
|
||||
| Status.LengthRequired
|
||||
| Status.PreconditionFailed
|
||||
| Status.RequestEntityTooLarge
|
||||
| Status.RequestURITooLong
|
||||
| Status.UnsupportedMediaType
|
||||
| Status.RequestedRangeNotSatisfiable
|
||||
| Status.ExpectationFailed
|
||||
| Status.Teapot
|
||||
| Status.MisdirectedRequest
|
||||
| Status.UnprocessableEntity
|
||||
| Status.Locked
|
||||
| Status.FailedDependency
|
||||
| Status.UpgradeRequired
|
||||
| Status.PreconditionRequired
|
||||
| Status.TooManyRequests
|
||||
| Status.RequestHeaderFieldsTooLarge
|
||||
| Status.UnavailableForLegalReasons;
|
||||
|
||||
/** An HTTP status that is a server error (5XX). */
|
||||
export type ServerErrorStatus =
|
||||
| Status.InternalServerError
|
||||
| Status.NotImplemented
|
||||
| Status.BadGateway
|
||||
| Status.ServiceUnavailable
|
||||
| Status.GatewayTimeout
|
||||
| Status.HTTPVersionNotSupported
|
||||
| Status.VariantAlsoNegotiates
|
||||
| Status.InsufficientStorage
|
||||
| Status.LoopDetected
|
||||
| Status.NotExtended
|
||||
| Status.NetworkAuthenticationRequired;
|
||||
|
||||
/** An HTTP status that is an error (4XX and 5XX). */
|
||||
export type ErrorStatus = ClientErrorStatus | ServerErrorStatus;
|
||||
|
||||
/** A type guard that determines if the status code is informational. */
|
||||
export function isInformationalStatus(
|
||||
status: Status,
|
||||
): status is InformationalStatus {
|
||||
return status >= 100 && status < 200;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is successful. */
|
||||
export function isSuccessfulStatus(status: Status): status is SuccessfulStatus {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a redirection. */
|
||||
export function isRedirectStatus(status: Status): status is RedirectStatus {
|
||||
return status >= 300 && status < 400;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a client error. */
|
||||
export function isClientErrorStatus(
|
||||
status: Status,
|
||||
): status is ClientErrorStatus {
|
||||
return status >= 400 && status < 500;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is a server error. */
|
||||
export function isServerErrorStatus(
|
||||
status: Status,
|
||||
): status is ServerErrorStatus {
|
||||
return status >= 500 && status < 600;
|
||||
}
|
||||
|
||||
/** A type guard that determines if the status code is an error. */
|
||||
export function isErrorStatus(status: Status): status is ErrorStatus {
|
||||
return status >= 400 && status < 600;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Contains the constant {@linkcode HTTP_METHODS} and the type
|
||||
* {@linkcode HttpMethod} and the type guard {@linkcode isHttpMethod} for
|
||||
* working with HTTP methods with type safety.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** A constant array of common HTTP methods.
|
||||
*
|
||||
* This list is compatible with Node.js `http` module.
|
||||
*/
|
||||
export const HTTP_METHODS = [
|
||||
"ACL",
|
||||
"BIND",
|
||||
"CHECKOUT",
|
||||
"CONNECT",
|
||||
"COPY",
|
||||
"DELETE",
|
||||
"GET",
|
||||
"HEAD",
|
||||
"LINK",
|
||||
"LOCK",
|
||||
"M-SEARCH",
|
||||
"MERGE",
|
||||
"MKACTIVITY",
|
||||
"MKCALENDAR",
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"NOTIFY",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PROPFIND",
|
||||
"PROPPATCH",
|
||||
"PURGE",
|
||||
"PUT",
|
||||
"REBIND",
|
||||
"REPORT",
|
||||
"SEARCH",
|
||||
"SOURCE",
|
||||
"SUBSCRIBE",
|
||||
"TRACE",
|
||||
"UNBIND",
|
||||
"UNLINK",
|
||||
"UNLOCK",
|
||||
"UNSUBSCRIBE",
|
||||
] as const;
|
||||
|
||||
/** A type representing string literals of each of the common HTTP method. */
|
||||
export type HttpMethod = typeof HTTP_METHODS[number];
|
||||
|
||||
/** A type guard that determines if a value is a valid HTTP method. */
|
||||
export function isHttpMethod(value: unknown): value is HttpMethod {
|
||||
return HTTP_METHODS.includes(value as HttpMethod);
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Contains the functions {@linkcode accepts}, {@linkcode acceptsEncodings}, and
|
||||
* {@linkcode acceptsLanguages} to provide content negotiation capabilities.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { preferredEncodings } from "./_negotiation/encoding.ts";
|
||||
import { preferredLanguages } from "./_negotiation/language.ts";
|
||||
import { preferredMediaTypes } from "./_negotiation/media_type.ts";
|
||||
|
||||
export type Request = {
|
||||
headers: {
|
||||
get(key: string): string | null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of media types accepted by the request, in order of
|
||||
* preference. If there are no media types supplied in the request, then any
|
||||
* media type selector will be returned.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { accepts } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: {
|
||||
* "accept":
|
||||
* "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, *\/*;q=0.8",
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* console.log(accepts(req));
|
||||
* // [
|
||||
* // "text/html",
|
||||
* // "application/xhtml+xml",
|
||||
* // "image/webp",
|
||||
* // "application/xml",
|
||||
* // "*\/*",
|
||||
* // ]
|
||||
* ```
|
||||
*/
|
||||
export function accepts(request: Request): string[];
|
||||
/**
|
||||
* For a given set of media types, return the best match accepted in the
|
||||
* request. If no media type matches, then the function returns `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { accepts } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: {
|
||||
* "accept":
|
||||
* "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, *\/*;q=0.8",
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* accepts(req, "text/html", "image/webp"); // "text/html";
|
||||
* ```
|
||||
*/
|
||||
export function accepts(
|
||||
request: Request,
|
||||
...types: string[]
|
||||
): string | undefined;
|
||||
export function accepts(
|
||||
request: Request,
|
||||
...types: string[]
|
||||
): string | string[] | undefined {
|
||||
const accept = request.headers.get("accept");
|
||||
return types.length
|
||||
? accept ? preferredMediaTypes(accept, types)[0] : types[0]
|
||||
: accept
|
||||
? preferredMediaTypes(accept)
|
||||
: ["*/*"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of content encodings accepted by the request, in order of
|
||||
* preference. If there are no encoding supplied in the request, then `["*"]`
|
||||
* is returned, implying any encoding is accepted.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { acceptsEncodings } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: { "accept-encoding": "deflate, gzip;q=1.0, *;q=0.5" },
|
||||
* });
|
||||
*
|
||||
* acceptsEncodings(req); // ["deflate", "gzip", "*"]
|
||||
* ```
|
||||
*/
|
||||
export function acceptsEncodings(request: Request): string[];
|
||||
/**
|
||||
* For a given set of content encodings, return the best match accepted in the
|
||||
* request. If no content encodings match, then the function returns
|
||||
* `undefined`.
|
||||
*
|
||||
* **NOTE:** You should always supply `identity` as one of the encodings
|
||||
* to ensure that there is a match when the `Accept-Encoding` header is part
|
||||
* of the request.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { acceptsEncodings } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: { "accept-encoding": "deflate, gzip;q=1.0, *;q=0.5" },
|
||||
* });
|
||||
*
|
||||
* acceptsEncodings(req, "gzip", "identity"); // "gzip"
|
||||
* ```
|
||||
*/
|
||||
export function acceptsEncodings(
|
||||
request: Request,
|
||||
...encodings: string[]
|
||||
): string | undefined;
|
||||
export function acceptsEncodings(
|
||||
request: Request,
|
||||
...encodings: string[]
|
||||
): string | string[] | undefined {
|
||||
const acceptEncoding = request.headers.get("accept-encoding");
|
||||
return encodings.length
|
||||
? acceptEncoding
|
||||
? preferredEncodings(acceptEncoding, encodings)[0]
|
||||
: encodings[0]
|
||||
: acceptEncoding
|
||||
? preferredEncodings(acceptEncoding)
|
||||
: ["*"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of languages accepted by the request, in order of
|
||||
* preference. If there are no languages supplied in the request, then `["*"]`
|
||||
* is returned, imply any language is accepted.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { acceptsLanguages } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: {
|
||||
* "accept-language": "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5",
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* acceptsLanguages(req); // ["fr-CH", "fr", "en", "de", "*"]
|
||||
* ```
|
||||
*/
|
||||
export function acceptsLanguages(request: Request): string[];
|
||||
/**
|
||||
* For a given set of languages, return the best match accepted in the request.
|
||||
* If no languages match, then the function returns `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { acceptsLanguages } from "https://deno.land/std@$STD_VERSION/http/negotiation.ts";
|
||||
*
|
||||
* const req = new Request("https://example.com/", {
|
||||
* headers: {
|
||||
* "accept-language": "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5",
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* acceptsLanguages(req, "en-gb", "en-us", "en"); // "en"
|
||||
* ```
|
||||
*/
|
||||
export function acceptsLanguages(
|
||||
request: Request,
|
||||
...langs: string[]
|
||||
): string | undefined;
|
||||
export function acceptsLanguages(
|
||||
request: Request,
|
||||
...langs: string[]
|
||||
): string | string[] | undefined {
|
||||
const acceptLanguage = request.headers.get("accept-language");
|
||||
return langs.length
|
||||
? acceptLanguage ? preferredLanguages(acceptLanguage, langs)[0] : langs[0]
|
||||
: acceptLanguage
|
||||
? preferredLanguages(acceptLanguage)
|
||||
: ["*"];
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Provides {@linkcode ServerSentEvent} and
|
||||
* {@linkcode ServerSentEventStreamTarget} which provides an interface to send
|
||||
* server sent events to a browser using the DOM event model.
|
||||
*
|
||||
* The {@linkcode ServerSentEventStreamTarget} provides the `.asResponse()` or
|
||||
* `.asResponseInit()` to provide a body and headers to the client to establish
|
||||
* the event connection. This is accomplished by keeping a connection open to
|
||||
* the client by not closing the body, which allows events to be sent down the
|
||||
* connection and processed by the client browser.
|
||||
*
|
||||
* See more about Server-sent events on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
*
|
||||
* await serve((request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* let counter = 0;
|
||||
*
|
||||
* // Sends an event every 2 seconds, incrementing the ID
|
||||
* const id = setInterval(() => {
|
||||
* const evt = new ServerSentEvent(
|
||||
* "message",
|
||||
* { data: { hello: "world" }, id: counter++ },
|
||||
* );
|
||||
* target.dispatchEvent(evt);
|
||||
* }, 2000);
|
||||
*
|
||||
* target.addEventListener("close", () => clearInterval(id));
|
||||
* return target.asResponse();
|
||||
* }, { port: 8000 });
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const DEFAULT_KEEP_ALIVE_INTERVAL = 30_000;
|
||||
|
||||
export interface ServerSentEventInit extends EventInit {
|
||||
/** Optional arbitrary data to send to the client, data this is a string will
|
||||
* be sent unmodified, otherwise `JSON.parse()` will be used to serialize the
|
||||
* value. */
|
||||
data?: unknown;
|
||||
|
||||
/** An optional `id` which will be sent with the event and exposed in the
|
||||
* client `EventSource`. */
|
||||
id?: number;
|
||||
|
||||
/** The replacer is passed to `JSON.stringify` when converting the `data`
|
||||
* property to a JSON string. */
|
||||
replacer?:
|
||||
| (string | number)[]
|
||||
// deno-lint-ignore no-explicit-any
|
||||
| ((this: any, key: string, value: any) => any);
|
||||
|
||||
/** Space is passed to `JSON.stringify` when converting the `data` property
|
||||
* to a JSON string. */
|
||||
space?: string | number;
|
||||
}
|
||||
|
||||
export interface ServerSentEventTargetOptions {
|
||||
/** Keep client connections alive by sending a comment event to the client
|
||||
* at a specified interval. If `true`, then it polls every 30000 milliseconds
|
||||
* (30 seconds). If set to a number, then it polls that number of
|
||||
* milliseconds. The feature is disabled if set to `false`. It defaults to
|
||||
* `false`. */
|
||||
keepAlive?: boolean | number;
|
||||
}
|
||||
|
||||
class CloseEvent extends Event {
|
||||
constructor(eventInit: EventInit) {
|
||||
super("close", eventInit);
|
||||
}
|
||||
}
|
||||
|
||||
/** An event which contains information which will be sent to the remote
|
||||
* connection and be made available in an `EventSource` as an event. A server
|
||||
* creates new events and dispatches them on the target which will then be
|
||||
* sent to a client.
|
||||
*
|
||||
* See more about Server-sent events on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
*
|
||||
* await serve((request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("message", {
|
||||
* data: { hello: "world" },
|
||||
* id: 1
|
||||
* });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* }, { port: 8000 });
|
||||
* ```
|
||||
*/
|
||||
export class ServerSentEvent extends Event {
|
||||
#data: string;
|
||||
#id?: number;
|
||||
#type: string;
|
||||
|
||||
/**
|
||||
* @param type the event type that will be available on the client. The type
|
||||
* of `"message"` will be handled specifically as a message
|
||||
* server-side event.
|
||||
* @param eventInit initialization options for the event
|
||||
*/
|
||||
constructor(type: string, eventInit: ServerSentEventInit = {}) {
|
||||
super(type, eventInit);
|
||||
const { data, replacer, space } = eventInit;
|
||||
this.#type = type;
|
||||
try {
|
||||
this.#data = typeof data === "string"
|
||||
? data
|
||||
: data !== undefined
|
||||
? JSON.stringify(data, replacer as (string | number)[], space)
|
||||
: "";
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
throw new TypeError(
|
||||
`data could not be coerced into a serialized string.\n ${e.message}`,
|
||||
);
|
||||
}
|
||||
const { id } = eventInit;
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
/** The data associated with the event, which will be sent to the client and
|
||||
* be made available in the `EventSource`. */
|
||||
get data(): string {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
/** The optional ID associated with the event that will be sent to the client
|
||||
* and be made available in the `EventSource`. */
|
||||
get id(): number | undefined {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
const data = `data: ${this.#data.split("\n").join("\ndata: ")}\n`;
|
||||
return `${this.#type === "__message" ? "" : `event: ${this.#type}\n`}${
|
||||
this.#id ? `id: ${String(this.#id)}\n` : ""
|
||||
}${data}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const RESPONSE_HEADERS = [
|
||||
["Connection", "Keep-Alive"],
|
||||
["Content-Type", "text/event-stream"],
|
||||
["Cache-Control", "no-cache"],
|
||||
["Keep-Alive", `timeout=${Number.MAX_SAFE_INTEGER}`],
|
||||
] as const;
|
||||
|
||||
export interface ServerSentEventTarget extends EventTarget {
|
||||
/** Is set to `true` if events cannot be sent to the remote connection.
|
||||
* Otherwise it is set to `false`.
|
||||
*
|
||||
* *Note*: This flag is lazily set, and might not reflect a closed state until
|
||||
* another event, comment or message is attempted to be processed. */
|
||||
readonly closed: boolean;
|
||||
|
||||
/** Close the target, refusing to accept any more events. */
|
||||
close(): Promise<void>;
|
||||
|
||||
/** Send a comment to the remote connection. Comments are not exposed to the
|
||||
* client `EventSource` but are used for diagnostics and helping ensure a
|
||||
* connection is kept alive.
|
||||
*
|
||||
* ```ts
|
||||
* import { ServerSentEventStreamTarget } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
*
|
||||
* await serve((request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* target.dispatchComment("this is a comment");
|
||||
* return target.asResponse();
|
||||
* }, { port: 8000 });
|
||||
* ```
|
||||
*/
|
||||
dispatchComment(comment: string): boolean;
|
||||
|
||||
/** Dispatch a message to the client. This message will contain `data: ` only
|
||||
* and be available on the client `EventSource` on the `onmessage` or an event
|
||||
* listener of type `"message"`. */
|
||||
dispatchMessage(data: unknown): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
*
|
||||
* await serve((request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* }, { port: 8000 });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: ServerSentEvent): boolean;
|
||||
|
||||
/** Dispatch a server sent event to the client. The event `type` will be
|
||||
* sent as `event: ` to the client which will be raised as a `MessageEvent`
|
||||
* on the `EventSource` in the client.
|
||||
*
|
||||
* Any local event handlers will be dispatched to first, and if the event
|
||||
* is cancelled, it will not be sent to the client.
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* ServerSentEvent,
|
||||
* ServerSentEventStreamTarget,
|
||||
* } from "https://deno.land/std@$STD_VERSION/http/server_sent_event.ts";
|
||||
* import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts";
|
||||
*
|
||||
* await serve((request) => {
|
||||
* const target = new ServerSentEventStreamTarget();
|
||||
* const evt = new ServerSentEvent("ping", { data: "hello" });
|
||||
* target.dispatchEvent(evt);
|
||||
* return target.asResponse();
|
||||
* }, { port: 8000 });
|
||||
* ```
|
||||
*/
|
||||
dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
}
|
||||
|
||||
/** An implementation of {@linkcode ServerSentEventTarget} that provides a
|
||||
* readable stream as a body of a response to establish a connection to a
|
||||
* client. */
|
||||
export class ServerSentEventStreamTarget extends EventTarget
|
||||
implements ServerSentEventTarget {
|
||||
#bodyInit: ReadableStream<Uint8Array>;
|
||||
#closed = false;
|
||||
#controller?: ReadableStreamDefaultController<Uint8Array>;
|
||||
// we are ignoring any here, because when exporting to npm/Node.js, the timer
|
||||
// handle isn't a number.
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#keepAliveId?: any;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
#error(error: any) {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
const errorEvent = new ErrorEvent("error", { error });
|
||||
this.dispatchEvent(errorEvent);
|
||||
}
|
||||
|
||||
#push(payload: string) {
|
||||
if (!this.#controller) {
|
||||
this.#error(new Error("The controller has not been set."));
|
||||
return;
|
||||
}
|
||||
if (this.#closed) {
|
||||
return;
|
||||
}
|
||||
this.#controller.enqueue(encoder.encode(payload));
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
return this.#closed;
|
||||
}
|
||||
|
||||
constructor({ keepAlive = false }: ServerSentEventTargetOptions = {}) {
|
||||
super();
|
||||
|
||||
this.#bodyInit = new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
this.#controller = controller;
|
||||
},
|
||||
cancel: (error) => {
|
||||
// connections closing are considered "normal" for SSE events and just
|
||||
// mean the far side has closed.
|
||||
if (
|
||||
error instanceof Error && error.message.includes("connection closed")
|
||||
) {
|
||||
this.close();
|
||||
} else {
|
||||
this.#error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.addEventListener("close", () => {
|
||||
this.#closed = true;
|
||||
if (this.#keepAliveId != null) {
|
||||
clearInterval(this.#keepAliveId);
|
||||
this.#keepAliveId = undefined;
|
||||
}
|
||||
if (this.#controller) {
|
||||
try {
|
||||
this.#controller.close();
|
||||
} catch {
|
||||
// we ignore any errors here, as it is likely that the controller
|
||||
// is already closed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (keepAlive) {
|
||||
const interval = typeof keepAlive === "number"
|
||||
? keepAlive
|
||||
: DEFAULT_KEEP_ALIVE_INTERVAL;
|
||||
this.#keepAliveId = setInterval(() => {
|
||||
this.dispatchComment("keep-alive comment");
|
||||
}, interval);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a {@linkcode Response} which contains the body and headers needed
|
||||
* to initiate a SSE connection with the client. */
|
||||
asResponse(responseInit?: ResponseInit): Response {
|
||||
return new Response(...this.asResponseInit(responseInit));
|
||||
}
|
||||
|
||||
/** Returns a tuple which contains the {@linkcode BodyInit} and
|
||||
* {@linkcode ResponseInit} needed to create a response that will establish
|
||||
* a SSE connection with the client. */
|
||||
asResponseInit(responseInit: ResponseInit = {}): [BodyInit, ResponseInit] {
|
||||
responseInit.headers = new Headers(responseInit.headers);
|
||||
for (const [key, value] of RESPONSE_HEADERS) {
|
||||
responseInit.headers.set(key, value);
|
||||
}
|
||||
return [this.#bodyInit, responseInit];
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.dispatchEvent(new CloseEvent({ cancelable: false }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
dispatchComment(comment: string): boolean {
|
||||
this.#push(`: ${comment.split("\n").join("\n: ")}\n\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
dispatchMessage(data: any): boolean {
|
||||
const event = new ServerSentEvent("__message", { data });
|
||||
return this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
override dispatchEvent(event: ServerSentEvent): boolean;
|
||||
override dispatchEvent(event: CloseEvent | ErrorEvent): boolean;
|
||||
override dispatchEvent(
|
||||
event: ServerSentEvent | CloseEvent | ErrorEvent,
|
||||
): boolean {
|
||||
const dispatched = super.dispatchEvent(event);
|
||||
if (dispatched && event instanceof ServerSentEvent) {
|
||||
this.#push(String(event));
|
||||
}
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) {
|
||||
return `${this.constructor.name} ${
|
||||
inspect({ "#bodyInit": this.#bodyInit, "#closed": this.#closed })
|
||||
}`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")](
|
||||
depth: number,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
options: any,
|
||||
inspect: (value: unknown, options?: unknown) => string,
|
||||
) {
|
||||
if (depth < 0) {
|
||||
return options.stylize(`[${this.constructor.name}]`, "special");
|
||||
}
|
||||
|
||||
const newOptions = Object.assign({}, options, {
|
||||
depth: options.depth === null ? null : options.depth - 1,
|
||||
});
|
||||
return `${options.stylize(this.constructor.name, "special")} ${
|
||||
inspect(
|
||||
{ "#bodyInit": this.#bodyInit, "#closed": this.#closed },
|
||||
newOptions,
|
||||
)
|
||||
}`;
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
import { copy } from "../bytes/copy.ts";
|
||||
import type { Reader } from "../types.d.ts";
|
||||
|
||||
const DEFAULT_BUF_SIZE = 4096;
|
||||
const MIN_BUF_SIZE = 16;
|
||||
const MAX_CONSECUTIVE_EMPTY_READS = 100;
|
||||
const CR = "\r".charCodeAt(0);
|
||||
const LF = "\n".charCodeAt(0);
|
||||
|
||||
export class BufferFullError extends Error {
|
||||
override name = "BufferFullError";
|
||||
constructor(public partial: Uint8Array) {
|
||||
super("Buffer full");
|
||||
}
|
||||
}
|
||||
|
||||
export class PartialReadError extends Error {
|
||||
override name = "PartialReadError";
|
||||
partial?: Uint8Array;
|
||||
constructor() {
|
||||
super("Encountered UnexpectedEof, data only partially read");
|
||||
}
|
||||
}
|
||||
|
||||
/** Result type returned by of BufReader.readLine(). */
|
||||
export interface ReadLineResult {
|
||||
line: Uint8Array;
|
||||
more: boolean;
|
||||
}
|
||||
|
||||
export class BufReader implements Reader {
|
||||
#buf!: Uint8Array;
|
||||
#rd!: Reader; // Reader provided by caller.
|
||||
#r = 0; // buf read position.
|
||||
#w = 0; // buf write position.
|
||||
#eof = false;
|
||||
// private lastByte: number;
|
||||
// private lastCharSize: number;
|
||||
|
||||
/** return new BufReader unless r is BufReader */
|
||||
static create(r: Reader, size: number = DEFAULT_BUF_SIZE): BufReader {
|
||||
return r instanceof BufReader ? r : new BufReader(r, size);
|
||||
}
|
||||
|
||||
constructor(rd: Reader, size: number = DEFAULT_BUF_SIZE) {
|
||||
if (size < MIN_BUF_SIZE) {
|
||||
size = MIN_BUF_SIZE;
|
||||
}
|
||||
this.#reset(new Uint8Array(size), rd);
|
||||
}
|
||||
|
||||
/** Returns the size of the underlying buffer in bytes. */
|
||||
size(): number {
|
||||
return this.#buf.byteLength;
|
||||
}
|
||||
|
||||
buffered(): number {
|
||||
return this.#w - this.#r;
|
||||
}
|
||||
|
||||
// Reads a new chunk into the buffer.
|
||||
#fill = async () => {
|
||||
// Slide existing data to beginning.
|
||||
if (this.#r > 0) {
|
||||
this.#buf.copyWithin(0, this.#r, this.#w);
|
||||
this.#w -= this.#r;
|
||||
this.#r = 0;
|
||||
}
|
||||
|
||||
if (this.#w >= this.#buf.byteLength) {
|
||||
throw Error("bufio: tried to fill full buffer");
|
||||
}
|
||||
|
||||
// Read new data: try a limited number of times.
|
||||
for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) {
|
||||
const rr = await this.#rd.read(this.#buf.subarray(this.#w));
|
||||
if (rr === null) {
|
||||
this.#eof = true;
|
||||
return;
|
||||
}
|
||||
assert(rr >= 0, "negative read");
|
||||
this.#w += rr;
|
||||
if (rr > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No progress after ${MAX_CONSECUTIVE_EMPTY_READS} read() calls`,
|
||||
);
|
||||
};
|
||||
|
||||
/** Discards any buffered data, resets all state, and switches
|
||||
* the buffered reader to read from r.
|
||||
*/
|
||||
reset(r: Reader) {
|
||||
this.#reset(this.#buf, r);
|
||||
}
|
||||
|
||||
#reset = (buf: Uint8Array, rd: Reader) => {
|
||||
this.#buf = buf;
|
||||
this.#rd = rd;
|
||||
this.#eof = false;
|
||||
// this.lastByte = -1;
|
||||
// this.lastCharSize = -1;
|
||||
};
|
||||
|
||||
/** reads data into p.
|
||||
* It returns the number of bytes read into p.
|
||||
* The bytes are taken from at most one Read on the underlying Reader,
|
||||
* hence n may be less than len(p).
|
||||
* To read exactly len(p) bytes, use io.ReadFull(b, p).
|
||||
*/
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
let rr: number | null = p.byteLength;
|
||||
if (p.byteLength === 0) return rr;
|
||||
|
||||
if (this.#r === this.#w) {
|
||||
if (p.byteLength >= this.#buf.byteLength) {
|
||||
// Large read, empty buffer.
|
||||
// Read directly into p to avoid copy.
|
||||
const rr = await this.#rd.read(p);
|
||||
const nread = rr ?? 0;
|
||||
assert(nread >= 0, "negative read");
|
||||
// if (rr.nread > 0) {
|
||||
// this.lastByte = p[rr.nread - 1];
|
||||
// this.lastCharSize = -1;
|
||||
// }
|
||||
return rr;
|
||||
}
|
||||
|
||||
// One read.
|
||||
// Do not use this.fill, which will loop.
|
||||
this.#r = 0;
|
||||
this.#w = 0;
|
||||
rr = await this.#rd.read(this.#buf);
|
||||
if (rr === 0 || rr === null) return rr;
|
||||
assert(rr >= 0, "negative read");
|
||||
this.#w += rr;
|
||||
}
|
||||
|
||||
// copy as much as we can
|
||||
const copied = copy(this.#buf.subarray(this.#r, this.#w), p, 0);
|
||||
this.#r += copied;
|
||||
// this.lastByte = this.buf[this.r - 1];
|
||||
// this.lastCharSize = -1;
|
||||
return copied;
|
||||
}
|
||||
|
||||
/** reads exactly `p.length` bytes into `p`.
|
||||
*
|
||||
* If successful, `p` is returned.
|
||||
*
|
||||
* If the end of the underlying stream has been reached, and there are no more
|
||||
* bytes available in the buffer, `readFull()` returns `null` instead.
|
||||
*
|
||||
* An error is thrown if some bytes could be read, but not enough to fill `p`
|
||||
* entirely before the underlying stream reported an error or EOF. Any error
|
||||
* thrown will have a `partial` property that indicates the slice of the
|
||||
* buffer that has been successfully filled with data.
|
||||
*
|
||||
* Ported from https://golang.org/pkg/io/#ReadFull
|
||||
*/
|
||||
async readFull(p: Uint8Array): Promise<Uint8Array | null> {
|
||||
let bytesRead = 0;
|
||||
while (bytesRead < p.length) {
|
||||
try {
|
||||
const rr = await this.read(p.subarray(bytesRead));
|
||||
if (rr === null) {
|
||||
if (bytesRead === 0) {
|
||||
return null;
|
||||
} else {
|
||||
throw new PartialReadError();
|
||||
}
|
||||
}
|
||||
bytesRead += rr;
|
||||
} catch (err) {
|
||||
if (err instanceof PartialReadError) {
|
||||
err.partial = p.subarray(0, bytesRead);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Returns the next byte [0, 255] or `null`. */
|
||||
async readByte(): Promise<number | null> {
|
||||
while (this.#r === this.#w) {
|
||||
if (this.#eof) return null;
|
||||
await this.#fill(); // buffer is empty.
|
||||
}
|
||||
const c = this.#buf[this.#r];
|
||||
this.#r++;
|
||||
// this.lastByte = c;
|
||||
return c;
|
||||
}
|
||||
|
||||
/** readString() reads until the first occurrence of delim in the input,
|
||||
* returning a string containing the data up to and including the delimiter.
|
||||
* If ReadString encounters an error before finding a delimiter,
|
||||
* it returns the data read before the error and the error itself
|
||||
* (often `null`).
|
||||
* ReadString returns err != nil if and only if the returned data does not end
|
||||
* in delim.
|
||||
* For simple uses, a Scanner may be more convenient.
|
||||
*/
|
||||
async readString(delim: string): Promise<string | null> {
|
||||
if (delim.length !== 1) {
|
||||
throw new Error("Delimiter should be a single character");
|
||||
}
|
||||
const buffer = await this.readSlice(delim.charCodeAt(0));
|
||||
if (buffer === null) return null;
|
||||
return new TextDecoder().decode(buffer);
|
||||
}
|
||||
|
||||
/** `readLine()` is a low-level line-reading primitive. Most callers should
|
||||
* use `readString('\n')` instead or use a Scanner.
|
||||
*
|
||||
* `readLine()` tries to return a single line, not including the end-of-line
|
||||
* bytes. If the line was too long for the buffer then `more` is set and the
|
||||
* beginning of the line is returned. The rest of the line will be returned
|
||||
* from future calls. `more` will be false when returning the last fragment
|
||||
* of the line. The returned buffer is only valid until the next call to
|
||||
* `readLine()`.
|
||||
*
|
||||
* The text returned from ReadLine does not include the line end ("\r\n" or
|
||||
* "\n").
|
||||
*
|
||||
* When the end of the underlying stream is reached, the final bytes in the
|
||||
* stream are returned. No indication or error is given if the input ends
|
||||
* without a final line end. When there are no more trailing bytes to read,
|
||||
* `readLine()` returns `null`.
|
||||
*
|
||||
* Calling `unreadByte()` after `readLine()` will always unread the last byte
|
||||
* read (possibly a character belonging to the line end) even if that byte is
|
||||
* not part of the line returned by `readLine()`.
|
||||
*/
|
||||
async readLine(): Promise<ReadLineResult | null> {
|
||||
let line: Uint8Array | null = null;
|
||||
|
||||
try {
|
||||
line = await this.readSlice(LF);
|
||||
} catch (err) {
|
||||
let partial;
|
||||
if (err instanceof PartialReadError) {
|
||||
partial = err.partial;
|
||||
assert(
|
||||
partial instanceof Uint8Array,
|
||||
"bufio: caught error from `readSlice()` without `partial` property",
|
||||
);
|
||||
}
|
||||
|
||||
// Don't throw if `readSlice()` failed with `BufferFullError`, instead we
|
||||
// just return whatever is available and set the `more` flag.
|
||||
if (!(err instanceof BufferFullError)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
partial = err.partial;
|
||||
|
||||
// Handle the case where "\r\n" straddles the buffer.
|
||||
if (
|
||||
!this.#eof && partial &&
|
||||
partial.byteLength > 0 &&
|
||||
partial[partial.byteLength - 1] === CR
|
||||
) {
|
||||
// Put the '\r' back on buf and drop it from line.
|
||||
// Let the next call to ReadLine check for "\r\n".
|
||||
assert(this.#r > 0, "bufio: tried to rewind past start of buffer");
|
||||
this.#r--;
|
||||
partial = partial.subarray(0, partial.byteLength - 1);
|
||||
}
|
||||
|
||||
if (partial) {
|
||||
return { line: partial, more: !this.#eof };
|
||||
}
|
||||
}
|
||||
|
||||
if (line === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (line.byteLength === 0) {
|
||||
return { line, more: false };
|
||||
}
|
||||
|
||||
if (line[line.byteLength - 1] == LF) {
|
||||
let drop = 1;
|
||||
if (line.byteLength > 1 && line[line.byteLength - 2] === CR) {
|
||||
drop = 2;
|
||||
}
|
||||
line = line.subarray(0, line.byteLength - drop);
|
||||
}
|
||||
return { line, more: false };
|
||||
}
|
||||
|
||||
/** `readSlice()` reads until the first occurrence of `delim` in the input,
|
||||
* returning a slice pointing at the bytes in the buffer. The bytes stop
|
||||
* being valid at the next read.
|
||||
*
|
||||
* If `readSlice()` encounters an error before finding a delimiter, or the
|
||||
* buffer fills without finding a delimiter, it throws an error with a
|
||||
* `partial` property that contains the entire buffer.
|
||||
*
|
||||
* If `readSlice()` encounters the end of the underlying stream and there are
|
||||
* any bytes left in the buffer, the rest of the buffer is returned. In other
|
||||
* words, EOF is always treated as a delimiter. Once the buffer is empty,
|
||||
* it returns `null`.
|
||||
*
|
||||
* Because the data returned from `readSlice()` will be overwritten by the
|
||||
* next I/O operation, most clients should use `readString()` instead.
|
||||
*/
|
||||
async readSlice(delim: number): Promise<Uint8Array | null> {
|
||||
let s = 0; // search start index
|
||||
let slice: Uint8Array | undefined;
|
||||
|
||||
while (true) {
|
||||
// Search buffer.
|
||||
let i = this.#buf.subarray(this.#r + s, this.#w).indexOf(delim);
|
||||
if (i >= 0) {
|
||||
i += s;
|
||||
slice = this.#buf.subarray(this.#r, this.#r + i + 1);
|
||||
this.#r += i + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// EOF?
|
||||
if (this.#eof) {
|
||||
if (this.#r === this.#w) {
|
||||
return null;
|
||||
}
|
||||
slice = this.#buf.subarray(this.#r, this.#w);
|
||||
this.#r = this.#w;
|
||||
break;
|
||||
}
|
||||
|
||||
// Buffer full?
|
||||
if (this.buffered() >= this.#buf.byteLength) {
|
||||
this.#r = this.#w;
|
||||
// #4521 The internal buffer should not be reused across reads because it causes corruption of data.
|
||||
const oldbuf = this.#buf;
|
||||
const newbuf = this.#buf.slice(0);
|
||||
this.#buf = newbuf;
|
||||
throw new BufferFullError(oldbuf);
|
||||
}
|
||||
|
||||
s = this.#w - this.#r; // do not rescan area we scanned before
|
||||
|
||||
// Buffer is not full.
|
||||
try {
|
||||
await this.#fill();
|
||||
} catch (err) {
|
||||
if (err instanceof PartialReadError) {
|
||||
err.partial = slice;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last byte, if any.
|
||||
// const i = slice.byteLength - 1;
|
||||
// if (i >= 0) {
|
||||
// this.lastByte = slice[i];
|
||||
// this.lastCharSize = -1
|
||||
// }
|
||||
|
||||
return slice;
|
||||
}
|
||||
|
||||
/** `peek()` returns the next `n` bytes without advancing the reader. The
|
||||
* bytes stop being valid at the next read call.
|
||||
*
|
||||
* When the end of the underlying stream is reached, but there are unread
|
||||
* bytes left in the buffer, those bytes are returned. If there are no bytes
|
||||
* left in the buffer, it returns `null`.
|
||||
*
|
||||
* If an error is encountered before `n` bytes are available, `peek()` throws
|
||||
* an error with the `partial` property set to a slice of the buffer that
|
||||
* contains the bytes that were available before the error occurred.
|
||||
*/
|
||||
async peek(n: number): Promise<Uint8Array | null> {
|
||||
if (n < 0) {
|
||||
throw Error("negative count");
|
||||
}
|
||||
|
||||
let avail = this.#w - this.#r;
|
||||
while (avail < n && avail < this.#buf.byteLength && !this.#eof) {
|
||||
try {
|
||||
await this.#fill();
|
||||
} catch (err) {
|
||||
if (err instanceof PartialReadError) {
|
||||
err.partial = this.#buf.subarray(this.#r, this.#w);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
avail = this.#w - this.#r;
|
||||
}
|
||||
|
||||
if (avail === 0 && this.#eof) {
|
||||
return null;
|
||||
} else if (avail < n && this.#eof) {
|
||||
return this.#buf.subarray(this.#r, this.#r + avail);
|
||||
} else if (avail < n) {
|
||||
throw new BufferFullError(this.#buf.subarray(this.#r, this.#w));
|
||||
}
|
||||
|
||||
return this.#buf.subarray(this.#r, this.#r + n);
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { copy } from "../bytes/copy.ts";
|
||||
import type { Writer, WriterSync } from "../types.d.ts";
|
||||
|
||||
const DEFAULT_BUF_SIZE = 4096;
|
||||
|
||||
abstract class AbstractBufBase {
|
||||
buf: Uint8Array;
|
||||
usedBufferBytes = 0;
|
||||
err: Error | null = null;
|
||||
|
||||
constructor(buf: Uint8Array) {
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
/** Size returns the size of the underlying buffer in bytes. */
|
||||
size(): number {
|
||||
return this.buf.byteLength;
|
||||
}
|
||||
|
||||
/** Returns how many bytes are unused in the buffer. */
|
||||
available(): number {
|
||||
return this.buf.byteLength - this.usedBufferBytes;
|
||||
}
|
||||
|
||||
/** buffered returns the number of bytes that have been written into the
|
||||
* current buffer.
|
||||
*/
|
||||
buffered(): number {
|
||||
return this.usedBufferBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/** BufWriter implements buffering for an deno.Writer object.
|
||||
* If an error occurs writing to a Writer, no more data will be
|
||||
* accepted and all subsequent writes, and flush(), will return the error.
|
||||
* After all data has been written, the client should call the
|
||||
* flush() method to guarantee all data has been forwarded to
|
||||
* the underlying deno.Writer.
|
||||
*/
|
||||
export class BufWriter extends AbstractBufBase implements Writer {
|
||||
#writer: Writer;
|
||||
|
||||
/** return new BufWriter unless writer is BufWriter */
|
||||
static create(writer: Writer, size: number = DEFAULT_BUF_SIZE): BufWriter {
|
||||
return writer instanceof BufWriter ? writer : new BufWriter(writer, size);
|
||||
}
|
||||
|
||||
constructor(writer: Writer, size: number = DEFAULT_BUF_SIZE) {
|
||||
super(new Uint8Array(size <= 0 ? DEFAULT_BUF_SIZE : size));
|
||||
this.#writer = writer;
|
||||
}
|
||||
|
||||
/** Discards any unflushed buffered data, clears any error, and
|
||||
* resets buffer to write its output to w.
|
||||
*/
|
||||
reset(w: Writer) {
|
||||
this.err = null;
|
||||
this.usedBufferBytes = 0;
|
||||
this.#writer = w;
|
||||
}
|
||||
|
||||
/** Flush writes any buffered data to the underlying io.Writer. */
|
||||
async flush() {
|
||||
if (this.err !== null) throw this.err;
|
||||
if (this.usedBufferBytes === 0) return;
|
||||
|
||||
try {
|
||||
const p = this.buf.subarray(0, this.usedBufferBytes);
|
||||
let nwritten = 0;
|
||||
while (nwritten < p.length) {
|
||||
nwritten += await this.#writer.write(p.subarray(nwritten));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.err = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.buf = new Uint8Array(this.buf.length);
|
||||
this.usedBufferBytes = 0;
|
||||
}
|
||||
|
||||
/** Writes the contents of `data` into the buffer. If the contents won't fully
|
||||
* fit into the buffer, those bytes that are copied into the buffer will be flushed
|
||||
* to the writer and the remaining bytes are then copied into the now empty buffer.
|
||||
*
|
||||
* @return the number of bytes written to the buffer.
|
||||
*/
|
||||
async write(data: Uint8Array): Promise<number> {
|
||||
if (this.err !== null) throw this.err;
|
||||
if (data.length === 0) return 0;
|
||||
|
||||
let totalBytesWritten = 0;
|
||||
let numBytesWritten = 0;
|
||||
while (data.byteLength > this.available()) {
|
||||
if (this.buffered() === 0) {
|
||||
// Large write, empty buffer.
|
||||
// Write directly from data to avoid copy.
|
||||
try {
|
||||
numBytesWritten = await this.#writer.write(data);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.err = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
numBytesWritten = copy(data, this.buf, this.usedBufferBytes);
|
||||
this.usedBufferBytes += numBytesWritten;
|
||||
await this.flush();
|
||||
}
|
||||
totalBytesWritten += numBytesWritten;
|
||||
data = data.subarray(numBytesWritten);
|
||||
}
|
||||
|
||||
numBytesWritten = copy(data, this.buf, this.usedBufferBytes);
|
||||
this.usedBufferBytes += numBytesWritten;
|
||||
totalBytesWritten += numBytesWritten;
|
||||
return totalBytesWritten;
|
||||
}
|
||||
}
|
||||
|
||||
/** BufWriterSync implements buffering for a deno.WriterSync object.
|
||||
* If an error occurs writing to a WriterSync, no more data will be
|
||||
* accepted and all subsequent writes, and flush(), will return the error.
|
||||
* After all data has been written, the client should call the
|
||||
* flush() method to guarantee all data has been forwarded to
|
||||
* the underlying deno.WriterSync.
|
||||
*/
|
||||
export class BufWriterSync extends AbstractBufBase implements WriterSync {
|
||||
#writer: WriterSync;
|
||||
|
||||
/** return new BufWriterSync unless writer is BufWriterSync */
|
||||
static create(
|
||||
writer: WriterSync,
|
||||
size: number = DEFAULT_BUF_SIZE,
|
||||
): BufWriterSync {
|
||||
return writer instanceof BufWriterSync
|
||||
? writer
|
||||
: new BufWriterSync(writer, size);
|
||||
}
|
||||
|
||||
constructor(writer: WriterSync, size: number = DEFAULT_BUF_SIZE) {
|
||||
super(new Uint8Array(size <= 0 ? DEFAULT_BUF_SIZE : size));
|
||||
this.#writer = writer;
|
||||
}
|
||||
|
||||
/** Discards any unflushed buffered data, clears any error, and
|
||||
* resets buffer to write its output to w.
|
||||
*/
|
||||
reset(w: WriterSync) {
|
||||
this.err = null;
|
||||
this.usedBufferBytes = 0;
|
||||
this.#writer = w;
|
||||
}
|
||||
|
||||
/** Flush writes any buffered data to the underlying io.WriterSync. */
|
||||
flush() {
|
||||
if (this.err !== null) throw this.err;
|
||||
if (this.usedBufferBytes === 0) return;
|
||||
|
||||
try {
|
||||
const p = this.buf.subarray(0, this.usedBufferBytes);
|
||||
let nwritten = 0;
|
||||
while (nwritten < p.length) {
|
||||
nwritten += this.#writer.writeSync(p.subarray(nwritten));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.err = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.buf = new Uint8Array(this.buf.length);
|
||||
this.usedBufferBytes = 0;
|
||||
}
|
||||
|
||||
/** Writes the contents of `data` into the buffer. If the contents won't fully
|
||||
* fit into the buffer, those bytes that can are copied into the buffer, the
|
||||
* buffer is the flushed to the writer and the remaining bytes are copied into
|
||||
* the now empty buffer.
|
||||
*
|
||||
* @return the number of bytes written to the buffer.
|
||||
*/
|
||||
writeSync(data: Uint8Array): number {
|
||||
if (this.err !== null) throw this.err;
|
||||
if (data.length === 0) return 0;
|
||||
|
||||
let totalBytesWritten = 0;
|
||||
let numBytesWritten = 0;
|
||||
while (data.byteLength > this.available()) {
|
||||
if (this.buffered() === 0) {
|
||||
// Large write, empty buffer.
|
||||
// Write directly from data to avoid copy.
|
||||
try {
|
||||
numBytesWritten = this.#writer.writeSync(data);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
this.err = e;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
numBytesWritten = copy(data, this.buf, this.usedBufferBytes);
|
||||
this.usedBufferBytes += numBytesWritten;
|
||||
this.flush();
|
||||
}
|
||||
totalBytesWritten += numBytesWritten;
|
||||
data = data.subarray(numBytesWritten);
|
||||
}
|
||||
|
||||
numBytesWritten = copy(data, this.buf, this.usedBufferBytes);
|
||||
this.usedBufferBytes += numBytesWritten;
|
||||
totalBytesWritten += numBytesWritten;
|
||||
return totalBytesWritten;
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
import { copy } from "../bytes/copy.ts";
|
||||
import type { Reader, ReaderSync } from "../types.d.ts";
|
||||
|
||||
// MIN_READ is the minimum ArrayBuffer size passed to a read call by
|
||||
// buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
|
||||
// what is required to hold the contents of r, readFrom() will not grow the
|
||||
// underlying buffer.
|
||||
const MIN_READ = 32 * 1024;
|
||||
const MAX_SIZE = 2 ** 32 - 2;
|
||||
|
||||
/** A variable-sized buffer of bytes with `read()` and `write()` methods.
|
||||
*
|
||||
* Buffer is almost always used with some I/O like files and sockets. It allows
|
||||
* one to buffer up a download from a socket. Buffer grows and shrinks as
|
||||
* necessary.
|
||||
*
|
||||
* Buffer is NOT the same thing as Node's Buffer. Node's Buffer was created in
|
||||
* 2009 before JavaScript had the concept of ArrayBuffers. It's simply a
|
||||
* non-standard ArrayBuffer.
|
||||
*
|
||||
* ArrayBuffer is a fixed memory allocation. Buffer is implemented on top of
|
||||
* ArrayBuffer.
|
||||
*
|
||||
* Based on [Go Buffer](https://golang.org/pkg/bytes/#Buffer). */
|
||||
|
||||
export class Buffer {
|
||||
#buf: Uint8Array; // contents are the bytes buf[off : len(buf)]
|
||||
#off = 0; // read at buf[off], write at buf[buf.byteLength]
|
||||
|
||||
constructor(ab?: ArrayBufferLike | ArrayLike<number>) {
|
||||
this.#buf = ab === undefined ? new Uint8Array(0) : new Uint8Array(ab);
|
||||
}
|
||||
|
||||
/** Returns a slice holding the unread portion of the buffer.
|
||||
*
|
||||
* The slice is valid for use only until the next buffer modification (that
|
||||
* is, only until the next call to a method like `read()`, `write()`,
|
||||
* `reset()`, or `truncate()`). If `options.copy` is false the slice aliases the buffer content at
|
||||
* least until the next buffer modification, so immediate changes to the
|
||||
* slice will affect the result of future reads.
|
||||
* @param [options={ copy: true }]
|
||||
*/
|
||||
bytes(options = { copy: true }): Uint8Array {
|
||||
if (options.copy === false) return this.#buf.subarray(this.#off);
|
||||
return this.#buf.slice(this.#off);
|
||||
}
|
||||
|
||||
/** Returns whether the unread portion of the buffer is empty. */
|
||||
empty(): boolean {
|
||||
return this.#buf.byteLength <= this.#off;
|
||||
}
|
||||
|
||||
/** A read only number of bytes of the unread portion of the buffer. */
|
||||
get length(): number {
|
||||
return this.#buf.byteLength - this.#off;
|
||||
}
|
||||
|
||||
/** The read only capacity of the buffer's underlying byte slice, that is,
|
||||
* the total space allocated for the buffer's data. */
|
||||
get capacity(): number {
|
||||
return this.#buf.buffer.byteLength;
|
||||
}
|
||||
|
||||
/** Discards all but the first `n` unread bytes from the buffer but
|
||||
* continues to use the same allocated storage. It throws if `n` is
|
||||
* negative or greater than the length of the buffer. */
|
||||
truncate(n: number) {
|
||||
if (n === 0) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
if (n < 0 || n > this.length) {
|
||||
throw Error("bytes.Buffer: truncation out of range");
|
||||
}
|
||||
this.#reslice(this.#off + n);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#reslice(0);
|
||||
this.#off = 0;
|
||||
}
|
||||
|
||||
#tryGrowByReslice(n: number) {
|
||||
const l = this.#buf.byteLength;
|
||||
if (n <= this.capacity - l) {
|
||||
this.#reslice(l + n);
|
||||
return l;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#reslice(len: number) {
|
||||
assert(len <= this.#buf.buffer.byteLength);
|
||||
this.#buf = new Uint8Array(this.#buf.buffer, 0, len);
|
||||
}
|
||||
|
||||
/** Reads the next `p.length` bytes from the buffer or until the buffer is
|
||||
* drained. Returns the number of bytes read. If the buffer has no data to
|
||||
* return, the return is EOF (`null`). */
|
||||
readSync(p: Uint8Array): number | null {
|
||||
if (this.empty()) {
|
||||
// Buffer is empty, reset to recover space.
|
||||
this.reset();
|
||||
if (p.byteLength === 0) {
|
||||
// this edge case is tested in 'bufferReadEmptyAtEOF' test
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const nread = copy(this.#buf.subarray(this.#off), p);
|
||||
this.#off += nread;
|
||||
return nread;
|
||||
}
|
||||
|
||||
/** Reads the next `p.length` bytes from the buffer or until the buffer is
|
||||
* drained. Resolves to the number of bytes read. If the buffer has no
|
||||
* data to return, resolves to EOF (`null`).
|
||||
*
|
||||
* NOTE: This methods reads bytes synchronously; it's provided for
|
||||
* compatibility with `Reader` interfaces.
|
||||
*/
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
const rr = this.readSync(p);
|
||||
return Promise.resolve(rr);
|
||||
}
|
||||
|
||||
writeSync(p: Uint8Array): number {
|
||||
const m = this.#grow(p.byteLength);
|
||||
return copy(p, this.#buf, m);
|
||||
}
|
||||
|
||||
/** NOTE: This methods writes bytes synchronously; it's provided for
|
||||
* compatibility with `Writer` interface. */
|
||||
write(p: Uint8Array): Promise<number> {
|
||||
const n = this.writeSync(p);
|
||||
return Promise.resolve(n);
|
||||
}
|
||||
|
||||
#grow(n: number) {
|
||||
const m = this.length;
|
||||
// If buffer is empty, reset to recover space.
|
||||
if (m === 0 && this.#off !== 0) {
|
||||
this.reset();
|
||||
}
|
||||
// Fast: Try to grow by means of a reslice.
|
||||
const i = this.#tryGrowByReslice(n);
|
||||
if (i >= 0) {
|
||||
return i;
|
||||
}
|
||||
const c = this.capacity;
|
||||
if (n <= Math.floor(c / 2) - m) {
|
||||
// We can slide things down instead of allocating a new
|
||||
// ArrayBuffer. We only need m+n <= c to slide, but
|
||||
// we instead let capacity get twice as large so we
|
||||
// don't spend all our time copying.
|
||||
copy(this.#buf.subarray(this.#off), this.#buf);
|
||||
} else if (c + n > MAX_SIZE) {
|
||||
throw new Error("The buffer cannot be grown beyond the maximum size.");
|
||||
} else {
|
||||
// Not enough space anywhere, we need to allocate.
|
||||
const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
|
||||
copy(this.#buf.subarray(this.#off), buf);
|
||||
this.#buf = buf;
|
||||
}
|
||||
// Restore this.#off and len(this.#buf).
|
||||
this.#off = 0;
|
||||
this.#reslice(Math.min(m + n, MAX_SIZE));
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Grows the buffer's capacity, if necessary, to guarantee space for
|
||||
* another `n` bytes. After `.grow(n)`, at least `n` bytes can be written to
|
||||
* the buffer without another allocation. If `n` is negative, `.grow()` will
|
||||
* throw. If the buffer can't grow it will throw an error.
|
||||
*
|
||||
* Based on Go Lang's
|
||||
* [Buffer.Grow](https://golang.org/pkg/bytes/#Buffer.Grow). */
|
||||
grow(n: number) {
|
||||
if (n < 0) {
|
||||
throw Error("Buffer.grow: negative count");
|
||||
}
|
||||
const m = this.#grow(n);
|
||||
this.#reslice(m);
|
||||
}
|
||||
|
||||
/** Reads data from `r` until EOF (`null`) and appends it to the buffer,
|
||||
* growing the buffer as needed. It resolves to the number of bytes read.
|
||||
* If the buffer becomes too large, `.readFrom()` will reject with an error.
|
||||
*
|
||||
* Based on Go Lang's
|
||||
* [Buffer.ReadFrom](https://golang.org/pkg/bytes/#Buffer.ReadFrom). */
|
||||
async readFrom(r: Reader): Promise<number> {
|
||||
let n = 0;
|
||||
const tmp = new Uint8Array(MIN_READ);
|
||||
while (true) {
|
||||
const shouldGrow = this.capacity - this.length < MIN_READ;
|
||||
// read into tmp buffer if there's not enough room
|
||||
// otherwise read directly into the internal buffer
|
||||
const buf = shouldGrow
|
||||
? tmp
|
||||
: new Uint8Array(this.#buf.buffer, this.length);
|
||||
|
||||
const nread = await r.read(buf);
|
||||
if (nread === null) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// write will grow if needed
|
||||
if (shouldGrow) this.writeSync(buf.subarray(0, nread));
|
||||
else this.#reslice(this.length + nread);
|
||||
|
||||
n += nread;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads data from `r` until EOF (`null`) and appends it to the buffer,
|
||||
* growing the buffer as needed. It returns the number of bytes read. If the
|
||||
* buffer becomes too large, `.readFromSync()` will throw an error.
|
||||
*
|
||||
* Based on Go Lang's
|
||||
* [Buffer.ReadFrom](https://golang.org/pkg/bytes/#Buffer.ReadFrom). */
|
||||
readFromSync(r: ReaderSync): number {
|
||||
let n = 0;
|
||||
const tmp = new Uint8Array(MIN_READ);
|
||||
while (true) {
|
||||
const shouldGrow = this.capacity - this.length < MIN_READ;
|
||||
// read into tmp buffer if there's not enough room
|
||||
// otherwise read directly into the internal buffer
|
||||
const buf = shouldGrow
|
||||
? tmp
|
||||
: new Uint8Array(this.#buf.buffer, this.length);
|
||||
|
||||
const nread = r.readSync(buf);
|
||||
if (nread === null) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// write will grow if needed
|
||||
if (shouldGrow) this.writeSync(buf.subarray(0, nread));
|
||||
else this.#reslice(this.length + nread);
|
||||
|
||||
n += nread;
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
import type { Reader, Writer } from "../types.d.ts";
|
||||
|
||||
const DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
/**
|
||||
* Copy N size at the most. If read size is lesser than N, then returns nread
|
||||
* @param r Reader
|
||||
* @param dest Writer
|
||||
* @param size Read size
|
||||
*/
|
||||
export async function copyN(
|
||||
r: Reader,
|
||||
dest: Writer,
|
||||
size: number,
|
||||
): Promise<number> {
|
||||
let bytesRead = 0;
|
||||
let buf = new Uint8Array(DEFAULT_BUFFER_SIZE);
|
||||
while (bytesRead < size) {
|
||||
if (size - bytesRead < DEFAULT_BUFFER_SIZE) {
|
||||
buf = new Uint8Array(size - bytesRead);
|
||||
}
|
||||
const result = await r.read(buf);
|
||||
const nread = result ?? 0;
|
||||
bytesRead += nread;
|
||||
if (nread > 0) {
|
||||
let n = 0;
|
||||
while (n < nread) {
|
||||
n += await dest.write(buf.slice(n, nread));
|
||||
}
|
||||
assert(n === nread, "could not write");
|
||||
}
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* A `LimitedReader` reads from `reader` but limits the amount of data returned to just `limit` bytes.
|
||||
* Each call to `read` updates `limit` to reflect the new amount remaining.
|
||||
* `read` returns `null` when `limit` <= `0` or
|
||||
* when the underlying `reader` returns `null`.
|
||||
*/
|
||||
import type { Reader } from "../types.d.ts";
|
||||
|
||||
export class LimitedReader implements Reader {
|
||||
constructor(public reader: Reader, public limit: number) {}
|
||||
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
if (this.limit <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p.length > this.limit) {
|
||||
p = p.subarray(0, this.limit);
|
||||
}
|
||||
const n = await this.reader.read(p);
|
||||
if (n == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.limit -= n;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/**
|
||||
* Utilities for working with Deno's readers, writers, and web streams.
|
||||
*
|
||||
* `Reader` and `Writer` interfaces are deprecated in Deno, and so many of these
|
||||
* utilities are also deprecated. Consider using web streams instead.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export * from "./buf_reader.ts";
|
||||
export * from "./buf_writer.ts";
|
||||
export * from "./buffer.ts";
|
||||
export * from "./copy_n.ts";
|
||||
export * from "./limited_reader.ts";
|
||||
export * from "./multi_reader.ts";
|
||||
export * from "./read_delim.ts";
|
||||
export * from "./read_int.ts";
|
||||
export * from "./read_lines.ts";
|
||||
export * from "./read_long.ts";
|
||||
export * from "./read_range.ts";
|
||||
export * from "./read_short.ts";
|
||||
export * from "./read_string_delim.ts";
|
||||
export * from "./slice_long_to_bytes.ts";
|
||||
export * from "./string_reader.ts";
|
||||
export * from "./string_writer.ts";
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { Reader } from "../types.d.ts";
|
||||
|
||||
/** Reader utility for combining multiple readers */
|
||||
export class MultiReader implements Reader {
|
||||
readonly #readers: Reader[];
|
||||
#currentIndex = 0;
|
||||
|
||||
constructor(readers: Reader[]) {
|
||||
this.#readers = [...readers];
|
||||
}
|
||||
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
const r = this.#readers[this.#currentIndex];
|
||||
if (!r) return null;
|
||||
const result = await r.read(p);
|
||||
if (result === null) {
|
||||
this.#currentIndex++;
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { BytesList } from "../bytes/bytes_list.ts";
|
||||
import type { Reader } from "../types.d.ts";
|
||||
|
||||
/** Generate longest proper prefix which is also suffix array. */
|
||||
function createLPS(pat: Uint8Array): Uint8Array {
|
||||
const lps = new Uint8Array(pat.length);
|
||||
lps[0] = 0;
|
||||
let prefixEnd = 0;
|
||||
let i = 1;
|
||||
while (i < lps.length) {
|
||||
if (pat[i] == pat[prefixEnd]) {
|
||||
prefixEnd++;
|
||||
lps[i] = prefixEnd;
|
||||
i++;
|
||||
} else if (prefixEnd === 0) {
|
||||
lps[i] = 0;
|
||||
i++;
|
||||
} else {
|
||||
prefixEnd = lps[prefixEnd - 1];
|
||||
}
|
||||
}
|
||||
return lps;
|
||||
}
|
||||
|
||||
/** Read delimited bytes from a Reader. */
|
||||
export async function* readDelim(
|
||||
reader: Reader,
|
||||
delim: Uint8Array,
|
||||
): AsyncIterableIterator<Uint8Array> {
|
||||
// Avoid unicode problems
|
||||
const delimLen = delim.length;
|
||||
const delimLPS = createLPS(delim);
|
||||
const chunks = new BytesList();
|
||||
const bufSize = Math.max(1024, delimLen + 1);
|
||||
|
||||
// Modified KMP
|
||||
let inspectIndex = 0;
|
||||
let matchIndex = 0;
|
||||
while (true) {
|
||||
const inspectArr = new Uint8Array(bufSize);
|
||||
const result = await reader.read(inspectArr);
|
||||
if (result === null) {
|
||||
// Yield last chunk.
|
||||
yield chunks.concat();
|
||||
return;
|
||||
} else if (result < 0) {
|
||||
// Discard all remaining and silently fail.
|
||||
return;
|
||||
}
|
||||
chunks.add(inspectArr, 0, result);
|
||||
let localIndex = 0;
|
||||
while (inspectIndex < chunks.size()) {
|
||||
if (inspectArr[localIndex] === delim[matchIndex]) {
|
||||
inspectIndex++;
|
||||
localIndex++;
|
||||
matchIndex++;
|
||||
if (matchIndex === delimLen) {
|
||||
// Full match
|
||||
const matchEnd = inspectIndex - delimLen;
|
||||
const readyBytes = chunks.slice(0, matchEnd);
|
||||
yield readyBytes;
|
||||
// Reset match, different from KMP.
|
||||
chunks.shift(inspectIndex);
|
||||
inspectIndex = 0;
|
||||
matchIndex = 0;
|
||||
}
|
||||
} else {
|
||||
if (matchIndex === 0) {
|
||||
inspectIndex++;
|
||||
localIndex++;
|
||||
} else {
|
||||
matchIndex = delimLPS[matchIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { type BufReader } from "./buf_reader.ts";
|
||||
import { readShort } from "./read_short.ts";
|
||||
|
||||
/**
|
||||
* Read big endian 32bit integer from BufReader
|
||||
* @param buf
|
||||
*/
|
||||
export async function readInt(buf: BufReader): Promise<number | null> {
|
||||
const high = await readShort(buf);
|
||||
if (high === null) return null;
|
||||
const low = await readShort(buf);
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
return (high << 16) | low;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { type Reader } from "../types.d.ts";
|
||||
import { BufReader } from "./buf_reader.ts";
|
||||
import { concat } from "../bytes/concat.ts";
|
||||
|
||||
/**
|
||||
* Read strings line-by-line from a Reader.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { readLines } from "https://deno.land/std@$STD_VERSION/io/read_lines.ts";
|
||||
* import * as path from "https://deno.land/std@$STD_VERSION/path/mod.ts";
|
||||
*
|
||||
* const filename = path.join(Deno.cwd(), "std/io/README.md");
|
||||
* let fileReader = await Deno.open(filename);
|
||||
*
|
||||
* for await (let line of readLines(fileReader)) {
|
||||
* console.log(line);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function* readLines(
|
||||
reader: Reader,
|
||||
decoderOpts?: {
|
||||
encoding?: string;
|
||||
fatal?: boolean;
|
||||
ignoreBOM?: boolean;
|
||||
},
|
||||
): AsyncIterableIterator<string> {
|
||||
const bufReader = new BufReader(reader);
|
||||
let chunks: Uint8Array[] = [];
|
||||
const decoder = new TextDecoder(decoderOpts?.encoding, decoderOpts);
|
||||
while (true) {
|
||||
const res = await bufReader.readLine();
|
||||
if (!res) {
|
||||
if (chunks.length > 0) {
|
||||
yield decoder.decode(concat(...chunks));
|
||||
}
|
||||
break;
|
||||
}
|
||||
chunks.push(res.line);
|
||||
if (!res.more) {
|
||||
yield decoder.decode(concat(...chunks));
|
||||
chunks = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { type BufReader } from "./buf_reader.ts";
|
||||
import { readInt } from "./read_int.ts";
|
||||
|
||||
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
/**
|
||||
* Read big endian 64bit long from BufReader
|
||||
* @param buf
|
||||
*/
|
||||
export async function readLong(buf: BufReader): Promise<number | null> {
|
||||
const high = await readInt(buf);
|
||||
if (high === null) return null;
|
||||
const low = await readInt(buf);
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
const big = (BigInt(high) << 32n) | BigInt(low);
|
||||
// We probably should provide a similar API that returns BigInt values.
|
||||
if (big > MAX_SAFE_INTEGER) {
|
||||
throw new RangeError(
|
||||
"Long value too big to be represented as a JavaScript number.",
|
||||
);
|
||||
}
|
||||
return Number(big);
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { copy as copyBytes } from "../bytes/copy.ts";
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
import type { Reader, ReaderSync } from "../types.d.ts";
|
||||
|
||||
const DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
export interface ByteRange {
|
||||
/** The 0 based index of the start byte for a range. */
|
||||
start: number;
|
||||
|
||||
/** The 0 based index of the end byte for a range, which is inclusive. */
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a range of bytes from a file or other resource that is readable and
|
||||
* seekable. The range start and end are inclusive of the bytes within that
|
||||
* range.
|
||||
*
|
||||
* ```ts
|
||||
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
|
||||
* import { readRange } from "https://deno.land/std@$STD_VERSION/io/read_range.ts";
|
||||
*
|
||||
* // Read the first 10 bytes of a file
|
||||
* const file = await Deno.open("example.txt", { read: true });
|
||||
* const bytes = await readRange(file, { start: 0, end: 9 });
|
||||
* assertEquals(bytes.length, 10);
|
||||
* ```
|
||||
*/
|
||||
export async function readRange(
|
||||
r: Reader & Deno.Seeker,
|
||||
range: ByteRange,
|
||||
): Promise<Uint8Array> {
|
||||
// byte ranges are inclusive, so we have to add one to the end
|
||||
let length = range.end - range.start + 1;
|
||||
assert(length > 0, "Invalid byte range was passed.");
|
||||
await r.seek(range.start, Deno.SeekMode.Start);
|
||||
const result = new Uint8Array(length);
|
||||
let off = 0;
|
||||
while (length) {
|
||||
const p = new Uint8Array(Math.min(length, DEFAULT_BUFFER_SIZE));
|
||||
const nread = await r.read(p);
|
||||
assert(nread !== null, "Unexpected EOF reach while reading a range.");
|
||||
assert(nread > 0, "Unexpected read of 0 bytes while reading a range.");
|
||||
copyBytes(p, result, off);
|
||||
off += nread;
|
||||
length -= nread;
|
||||
assert(length >= 0, "Unexpected length remaining after reading range.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a range of bytes synchronously from a file or other resource that is
|
||||
* readable and seekable. The range start and end are inclusive of the bytes
|
||||
* within that range.
|
||||
*
|
||||
* ```ts
|
||||
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
|
||||
* import { readRangeSync } from "https://deno.land/std@$STD_VERSION/io/read_range.ts";
|
||||
*
|
||||
* // Read the first 10 bytes of a file
|
||||
* const file = Deno.openSync("example.txt", { read: true });
|
||||
* const bytes = readRangeSync(file, { start: 0, end: 9 });
|
||||
* assertEquals(bytes.length, 10);
|
||||
* ```
|
||||
*/
|
||||
export function readRangeSync(
|
||||
r: ReaderSync & Deno.SeekerSync,
|
||||
range: ByteRange,
|
||||
): Uint8Array {
|
||||
// byte ranges are inclusive, so we have to add one to the end
|
||||
let length = range.end - range.start + 1;
|
||||
assert(length > 0, "Invalid byte range was passed.");
|
||||
r.seekSync(range.start, Deno.SeekMode.Start);
|
||||
const result = new Uint8Array(length);
|
||||
let off = 0;
|
||||
while (length) {
|
||||
const p = new Uint8Array(Math.min(length, DEFAULT_BUFFER_SIZE));
|
||||
const nread = r.readSync(p);
|
||||
assert(nread !== null, "Unexpected EOF reach while reading a range.");
|
||||
assert(nread > 0, "Unexpected read of 0 bytes while reading a range.");
|
||||
copyBytes(p, result, off);
|
||||
off += nread;
|
||||
length -= nread;
|
||||
assert(length >= 0, "Unexpected length remaining after reading range.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { type BufReader } from "./buf_reader.ts";
|
||||
|
||||
/**
|
||||
* Read big endian 16bit short from BufReader
|
||||
* @param buf
|
||||
*/
|
||||
export async function readShort(buf: BufReader): Promise<number | null> {
|
||||
const high = await buf.readByte();
|
||||
if (high === null) return null;
|
||||
const low = await buf.readByte();
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
return (high << 8) | low;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { type Reader } from "../types.d.ts";
|
||||
import { readDelim } from "./read_delim.ts";
|
||||
|
||||
/**
|
||||
* Read Reader chunk by chunk, splitting based on delimiter.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { readStringDelim } from "https://deno.land/std@$STD_VERSION/io/read_string_delim.ts";
|
||||
* import * as path from "https://deno.land/std@$STD_VERSION/path/mod.ts";
|
||||
*
|
||||
* const filename = path.join(Deno.cwd(), "std/io/README.md");
|
||||
* let fileReader = await Deno.open(filename);
|
||||
*
|
||||
* for await (let line of readStringDelim(fileReader, "\n")) {
|
||||
* console.log(line);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function* readStringDelim(
|
||||
reader: Reader,
|
||||
delim: string,
|
||||
decoderOpts?: {
|
||||
encoding?: string;
|
||||
fatal?: boolean;
|
||||
ignoreBOM?: boolean;
|
||||
},
|
||||
): AsyncIterableIterator<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder(decoderOpts?.encoding, decoderOpts);
|
||||
for await (const chunk of readDelim(reader, encoder.encode(delim))) {
|
||||
yield decoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Slice number into 64bit big endian byte array
|
||||
* @param d The number to be sliced
|
||||
* @param dest The sliced array
|
||||
*/
|
||||
export function sliceLongToBytes(
|
||||
d: number,
|
||||
dest = Array.from<number>({ length: 8 }),
|
||||
): number[] {
|
||||
let big = BigInt(d);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
dest[7 - i] = Number(big & 0xffn);
|
||||
big >>= 8n;
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { Buffer } from "./buffer.ts";
|
||||
|
||||
/**
|
||||
* Reader utility for strings.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { StringReader } from "https://deno.land/std@$STD_VERSION/io/string_reader.ts";
|
||||
*
|
||||
* const data = new Uint8Array(6);
|
||||
* const r = new StringReader("abcdef");
|
||||
* const res0 = await r.read(data);
|
||||
* const res1 = await r.read(new Uint8Array(6));
|
||||
*
|
||||
* // Number of bytes read
|
||||
* console.log(res0); // 6
|
||||
* console.log(res1); // null, no byte left to read. EOL
|
||||
*
|
||||
* // text
|
||||
*
|
||||
* console.log(new TextDecoder().decode(data)); // abcdef
|
||||
* ```
|
||||
*
|
||||
* **Output:**
|
||||
*
|
||||
* ```text
|
||||
* 6
|
||||
* null
|
||||
* abcdef
|
||||
* ```
|
||||
*/
|
||||
export class StringReader extends Buffer {
|
||||
constructor(s: string) {
|
||||
super(new TextEncoder().encode(s).buffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { Writer, WriterSync } from "../types.d.ts";
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
/**
|
||||
* Writer utility for buffering string chunks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* copyN,
|
||||
* StringReader,
|
||||
* StringWriter,
|
||||
* } from "https://deno.land/std@$STD_VERSION/io/mod.ts";
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/streams/copy.ts";
|
||||
*
|
||||
* const w = new StringWriter("base");
|
||||
* const r = new StringReader("0123456789");
|
||||
* await copyN(r, w, 4); // copy 4 bytes
|
||||
*
|
||||
* // Number of bytes read
|
||||
* console.log(w.toString()); //base0123
|
||||
*
|
||||
* await copy(r, w); // copy all
|
||||
* console.log(w.toString()); // base0123456789
|
||||
* ```
|
||||
*
|
||||
* **Output:**
|
||||
*
|
||||
* ```text
|
||||
* base0123
|
||||
* base0123456789
|
||||
* ```
|
||||
*/
|
||||
export class StringWriter implements Writer, WriterSync {
|
||||
#chunks: Uint8Array[] = [];
|
||||
#byteLength = 0;
|
||||
#cache: string | undefined;
|
||||
|
||||
constructor(private base: string = "") {
|
||||
const c = new TextEncoder().encode(base);
|
||||
this.#chunks.push(c);
|
||||
this.#byteLength += c.byteLength;
|
||||
}
|
||||
|
||||
write(p: Uint8Array): Promise<number> {
|
||||
return Promise.resolve(this.writeSync(p));
|
||||
}
|
||||
|
||||
writeSync(p: Uint8Array): number {
|
||||
this.#chunks.push(new Uint8Array(p));
|
||||
this.#byteLength += p.byteLength;
|
||||
this.#cache = undefined;
|
||||
return p.byteLength;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.#cache) {
|
||||
return this.#cache;
|
||||
}
|
||||
const buf = new Uint8Array(this.#byteLength);
|
||||
let offs = 0;
|
||||
for (const chunk of this.#chunks) {
|
||||
buf.set(chunk, offs);
|
||||
offs += chunk.byteLength;
|
||||
}
|
||||
this.#cache = decoder.decode(buf);
|
||||
return this.#cache;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
import db from "./vendor/mime-db.v1.52.0.ts";
|
||||
import { type DBEntry, extensions } from "./_util.ts";
|
||||
|
||||
export type KeyOfDb = keyof typeof db;
|
||||
|
||||
/** A map of the media type for a given extension */
|
||||
export const types = new Map<string, KeyOfDb>();
|
||||
|
||||
/** Internal function to populate the maps based on the Mime DB. */
|
||||
(function populateMaps() {
|
||||
const preference = ["nginx", "apache", undefined, "iana"];
|
||||
|
||||
for (const type of Object.keys(db) as KeyOfDb[]) {
|
||||
const mime = db[type] as DBEntry;
|
||||
const exts = mime.extensions;
|
||||
|
||||
if (!exts || !exts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore work around denoland/dnt#148
|
||||
extensions.set(type, exts);
|
||||
|
||||
for (const ext of exts) {
|
||||
const current = types.get(ext);
|
||||
if (current) {
|
||||
const from = preference.indexOf((db[current] as DBEntry).source);
|
||||
const to = preference.indexOf(mime.source);
|
||||
|
||||
if (
|
||||
current !== "application/octet-stream" &&
|
||||
(from > to ||
|
||||
// @ts-ignore work around denoland/dnt#148
|
||||
(from === to && current.startsWith("application/")))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
types.set(ext, type);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
export { db };
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/** Supporting functions for media_types that do not make part of the public
|
||||
* API.
|
||||
*
|
||||
* @module
|
||||
* @private
|
||||
*/
|
||||
export interface DBEntry {
|
||||
source: string;
|
||||
compressible?: boolean;
|
||||
charset?: string;
|
||||
extensions?: string[];
|
||||
}
|
||||
|
||||
/** A map of extensions for a given media type. */
|
||||
export const extensions = new Map<string, string[]>();
|
||||
|
||||
export function consumeToken(v: string): [token: string, rest: string] {
|
||||
const notPos = indexOf(v, isNotTokenChar);
|
||||
if (notPos == -1) {
|
||||
return [v, ""];
|
||||
}
|
||||
if (notPos == 0) {
|
||||
return ["", v];
|
||||
}
|
||||
return [v.slice(0, notPos), v.slice(notPos)];
|
||||
}
|
||||
|
||||
export function consumeValue(v: string): [value: string, rest: string] {
|
||||
if (!v) {
|
||||
return ["", v];
|
||||
}
|
||||
if (v[0] !== `"`) {
|
||||
return consumeToken(v);
|
||||
}
|
||||
let value = "";
|
||||
for (let i = 1; i < v.length; i++) {
|
||||
const r = v[i];
|
||||
if (r === `"`) {
|
||||
return [value, v.slice(i + 1)];
|
||||
}
|
||||
if (r === "\\" && i + 1 < v.length && isTSpecial(v[i + 1])) {
|
||||
value += v[i + 1];
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (r === "\r" || r === "\n") {
|
||||
return ["", v];
|
||||
}
|
||||
value += v[i];
|
||||
}
|
||||
return ["", v];
|
||||
}
|
||||
|
||||
export function consumeMediaParam(
|
||||
v: string,
|
||||
): [key: string, value: string, rest: string] {
|
||||
let rest = v.trimStart();
|
||||
if (!rest.startsWith(";")) {
|
||||
return ["", "", v];
|
||||
}
|
||||
rest = rest.slice(1);
|
||||
rest = rest.trimStart();
|
||||
let param: string;
|
||||
[param, rest] = consumeToken(rest);
|
||||
param = param.toLowerCase();
|
||||
if (!param) {
|
||||
return ["", "", v];
|
||||
}
|
||||
rest = rest.slice(1);
|
||||
rest = rest.trimStart();
|
||||
const [value, rest2] = consumeValue(rest);
|
||||
if (value == "" && rest2 === rest) {
|
||||
return ["", "", v];
|
||||
}
|
||||
rest = rest2;
|
||||
return [param, value, rest];
|
||||
}
|
||||
|
||||
export function decode2331Encoding(v: string): string | undefined {
|
||||
const sv = v.split(`'`, 3);
|
||||
if (sv.length !== 3) {
|
||||
return undefined;
|
||||
}
|
||||
const charset = sv[0].toLowerCase();
|
||||
if (!charset) {
|
||||
return undefined;
|
||||
}
|
||||
if (charset != "us-ascii" && charset != "utf-8") {
|
||||
return undefined;
|
||||
}
|
||||
const encv = decodeURI(sv[2]);
|
||||
if (!encv) {
|
||||
return undefined;
|
||||
}
|
||||
return encv;
|
||||
}
|
||||
|
||||
function indexOf<T>(s: Iterable<T>, fn: (s: T) => boolean): number {
|
||||
let i = -1;
|
||||
for (const v of s) {
|
||||
i++;
|
||||
if (fn(v)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function isIterator<T>(obj: unknown): obj is Iterable<T> {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return typeof (obj as any)[Symbol.iterator] === "function";
|
||||
}
|
||||
|
||||
export function isToken(s: string): boolean {
|
||||
if (!s) {
|
||||
return false;
|
||||
}
|
||||
return indexOf(s, isNotTokenChar) < 0;
|
||||
}
|
||||
|
||||
function isNotTokenChar(r: string): boolean {
|
||||
return !isTokenChar(r);
|
||||
}
|
||||
|
||||
function isTokenChar(r: string): boolean {
|
||||
const code = r.charCodeAt(0);
|
||||
return code > 0x20 && code < 0x7f && !isTSpecial(r);
|
||||
}
|
||||
|
||||
function isTSpecial(r: string): boolean {
|
||||
return `()<>@,;:\\"/[]?=`.includes(r[0]);
|
||||
}
|
||||
|
||||
const CHAR_CODE_SPACE = " ".charCodeAt(0);
|
||||
const CHAR_CODE_TILDE = "~".charCodeAt(0);
|
||||
|
||||
export function needsEncoding(s: string): boolean {
|
||||
for (const b of s) {
|
||||
const charCode = b.charCodeAt(0);
|
||||
if (
|
||||
(charCode < CHAR_CODE_SPACE || charCode > CHAR_CODE_TILDE) && b !== "\t"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { parseMediaType } from "./parse_media_type.ts";
|
||||
import { typeByExtension } from "./type_by_extension.ts";
|
||||
import { getCharset } from "./get_charset.ts";
|
||||
import { formatMediaType } from "./format_media_type.ts";
|
||||
import type { db } from "./_db.ts";
|
||||
|
||||
type DB = typeof db;
|
||||
type ContentTypeToExtension = {
|
||||
[K in keyof DB]: DB[K] extends { "extensions": readonly string[] }
|
||||
? DB[K]["extensions"][number]
|
||||
: never;
|
||||
};
|
||||
|
||||
type KnownExtensionOrType =
|
||||
| keyof ContentTypeToExtension
|
||||
| ContentTypeToExtension[keyof ContentTypeToExtension]
|
||||
| `.${ContentTypeToExtension[keyof ContentTypeToExtension]}`;
|
||||
|
||||
/**
|
||||
* Given an extension or media type, return a full `Content-Type` or
|
||||
* `Content-Disposition` header value.
|
||||
*
|
||||
* The function will treat the `extensionOrType` as a media type when it
|
||||
* contains a `/`, otherwise it will process it as an extension, with or without
|
||||
* the leading `.`.
|
||||
*
|
||||
* Returns `undefined` if unable to resolve the media type.
|
||||
*
|
||||
* > Note: a side effect of `deno/x/media_types` was that you could pass a file
|
||||
* > name (e.g. `file.json`) and it would return the content type. This behavior
|
||||
* > is intentionally not supported here. If you want to get an extension for a
|
||||
* > file name, use `extname()` from `std/path/mod.ts` to determine the
|
||||
* > extension and pass it here.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { contentType } from "https://deno.land/std@$STD_VERSION/media_types/content_type.ts";
|
||||
*
|
||||
* contentType(".json"); // `application/json; charset=UTF-8`
|
||||
* contentType("text/html"); // `text/html; charset=UTF-8`
|
||||
* contentType("text/html; charset=UTF-8"); // `text/html; charset=UTF-8`
|
||||
* contentType("txt"); // `text/plain; charset=UTF-8`
|
||||
* contentType("foo"); // undefined
|
||||
* contentType("file.json"); // undefined
|
||||
* ```
|
||||
*/
|
||||
export function contentType<
|
||||
// Workaround to autocomplete for parameters: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
|
||||
// deno-lint-ignore ban-types
|
||||
T extends (string & {}) | KnownExtensionOrType,
|
||||
>(
|
||||
extensionOrType: T,
|
||||
): Lowercase<T> extends KnownExtensionOrType ? string : string | undefined {
|
||||
try {
|
||||
const [mediaType, params = {}] = extensionOrType.includes("/")
|
||||
? parseMediaType(extensionOrType)
|
||||
: [typeByExtension(extensionOrType), undefined];
|
||||
if (!mediaType) {
|
||||
return undefined as Lowercase<T> extends KnownExtensionOrType ? string
|
||||
: string | undefined;
|
||||
}
|
||||
if (!("charset" in params)) {
|
||||
const charset = getCharset(mediaType);
|
||||
if (charset) {
|
||||
params.charset = charset;
|
||||
}
|
||||
}
|
||||
return formatMediaType(mediaType, params);
|
||||
} catch {
|
||||
// just swallow returning undefined
|
||||
}
|
||||
return undefined as Lowercase<T> extends KnownExtensionOrType ? string
|
||||
: string | undefined;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { extensionsByType } from "./extensions_by_type.ts";
|
||||
|
||||
/**
|
||||
* For a given media type, return the most relevant extension, or `undefined`
|
||||
* if no extension can be found.
|
||||
*
|
||||
* Extensions are returned without a leading `.`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { extension } from "https://deno.land/std@$STD_VERSION/media_types/extension.ts";
|
||||
*
|
||||
* extension("text/plain"); // `txt`
|
||||
* extension("application/json"); // `json`
|
||||
* extension("text/html; charset=UTF-8"); // `html`
|
||||
* extension("application/foo"); // undefined
|
||||
* ```
|
||||
*/
|
||||
export function extension(type: string): string | undefined {
|
||||
const exts = extensionsByType(type);
|
||||
if (exts) {
|
||||
return exts[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { parseMediaType } from "./parse_media_type.ts";
|
||||
import { extensions } from "./_util.ts";
|
||||
|
||||
export { extensions };
|
||||
|
||||
/**
|
||||
* Returns the extensions known to be associated with the media type `type`.
|
||||
* The returned extensions will each begin with a leading dot, as in `.html`.
|
||||
*
|
||||
* When `type` has no associated extensions, the function returns `undefined`.
|
||||
*
|
||||
* Extensions are returned without a leading `.`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { extensionsByType } from "https://deno.land/std@$STD_VERSION/media_types/extensions_by_type.ts";
|
||||
*
|
||||
* extensionsByType("application/json"); // ["json", "map"]
|
||||
* extensionsByType("text/html; charset=UTF-8"); // ["html", "htm", "shtml"]
|
||||
* extensionsByType("application/foo"); // undefined
|
||||
* ```
|
||||
*/
|
||||
export function extensionsByType(type: string): string[] | undefined {
|
||||
try {
|
||||
const [mediaType] = parseMediaType(type);
|
||||
return extensions.get(mediaType);
|
||||
} catch {
|
||||
// just swallow errors, returning undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { isIterator, isToken, needsEncoding } from "./_util.ts";
|
||||
|
||||
/** Serializes the media type and the optional parameters as a media type
|
||||
* conforming to RFC 2045 and RFC 2616.
|
||||
*
|
||||
* The type and parameter names are written in lower-case.
|
||||
*
|
||||
* When any of the arguments results in a standard violation then the return
|
||||
* value will be an empty string (`""`).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { formatMediaType } from "https://deno.land/std@$STD_VERSION/media_types/format_media_type.ts";
|
||||
*
|
||||
* formatMediaType("text/plain", { charset: "UTF-8" }); // `text/plain; charset=UTF-8`
|
||||
* ```
|
||||
*/
|
||||
export function formatMediaType(
|
||||
type: string,
|
||||
param?: Record<string, string> | Iterable<[string, string]>,
|
||||
): string {
|
||||
let b = "";
|
||||
const [major, sub] = type.split("/");
|
||||
if (!sub) {
|
||||
if (!isToken(type)) {
|
||||
return "";
|
||||
}
|
||||
b += type.toLowerCase();
|
||||
} else {
|
||||
if (!isToken(major) || !isToken(sub)) {
|
||||
return "";
|
||||
}
|
||||
b += `${major.toLowerCase()}/${sub.toLowerCase()}`;
|
||||
}
|
||||
|
||||
if (param) {
|
||||
param = isIterator(param) ? Object.fromEntries(param) : param;
|
||||
const attrs = Object.keys(param);
|
||||
attrs.sort();
|
||||
|
||||
for (const attribute of attrs) {
|
||||
if (!isToken(attribute)) {
|
||||
return "";
|
||||
}
|
||||
const value = param[attribute];
|
||||
b += `; ${attribute.toLowerCase()}`;
|
||||
|
||||
const needEnc = needsEncoding(value);
|
||||
if (needEnc) {
|
||||
b += "*";
|
||||
}
|
||||
b += "=";
|
||||
|
||||
if (needEnc) {
|
||||
b += `utf-8''${encodeURIComponent(value)}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isToken(value)) {
|
||||
b += value;
|
||||
continue;
|
||||
}
|
||||
b += `"${value.replace(/["\\]/gi, (m) => `\\${m}`)}"`;
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { parseMediaType } from "./parse_media_type.ts";
|
||||
import { type DBEntry } from "./_util.ts";
|
||||
import { db, type KeyOfDb } from "./_db.ts";
|
||||
|
||||
/**
|
||||
* Given a media type or header value, identify the encoding charset. If the
|
||||
* charset cannot be determined, the function returns `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { getCharset } from "https://deno.land/std@$STD_VERSION/media_types/get_charset.ts";
|
||||
*
|
||||
* getCharset("text/plain"); // `UTF-8`
|
||||
* getCharset("application/foo"); // undefined
|
||||
* getCharset("application/news-checkgroups"); // `US-ASCII`
|
||||
* getCharset("application/news-checkgroups; charset=UTF-8"); // `UTF-8`
|
||||
* ```
|
||||
*/
|
||||
export function getCharset(type: string): string | undefined {
|
||||
try {
|
||||
const [mediaType, params] = parseMediaType(type);
|
||||
if (params && params["charset"]) {
|
||||
return params["charset"];
|
||||
}
|
||||
const entry = db[mediaType as KeyOfDb] as DBEntry;
|
||||
if (entry && entry.charset) {
|
||||
return entry.charset;
|
||||
}
|
||||
if (mediaType.startsWith("text/")) {
|
||||
return "UTF-8";
|
||||
}
|
||||
} catch {
|
||||
// just swallow errors, returning undefined
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Utility functions for media types (MIME types).
|
||||
*
|
||||
* This API is inspired by the GoLang [`mime`](https://pkg.go.dev/mime) package
|
||||
* and [jshttp/mime-types](https://github.com/jshttp/mime-types), and is
|
||||
* designed to integrate and improve the APIs from
|
||||
* [deno.land/x/media_types](https://deno.land/x/media_types).
|
||||
*
|
||||
* The `vendor` folder contains copy of the
|
||||
* [jshttp/mime-db](https://github.com/jshttp/mime-types) `db.json` file along
|
||||
* with its license.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export * from "./content_type.ts";
|
||||
export * from "./extension.ts";
|
||||
export * from "./extensions_by_type.ts";
|
||||
export * from "./format_media_type.ts";
|
||||
export * from "./get_charset.ts";
|
||||
export * from "./parse_media_type.ts";
|
||||
export * from "./type_by_extension.ts";
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { consumeMediaParam, decode2331Encoding } from "./_util.ts";
|
||||
|
||||
/**
|
||||
* Parses the media type and any optional parameters, per
|
||||
* [RFC 1521](https://datatracker.ietf.org/doc/html/rfc1521). Media types are
|
||||
* the values in `Content-Type` and `Content-Disposition` headers. On success
|
||||
* the function returns a tuple where the first element is the media type and
|
||||
* the second element is the optional parameters or `undefined` if there are
|
||||
* none.
|
||||
*
|
||||
* The function will throw if the parsed value is invalid.
|
||||
*
|
||||
* The returned media type will be normalized to be lower case, and returned
|
||||
* params keys will be normalized to lower case, but preserves the casing of
|
||||
* the value.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { parseMediaType } from "https://deno.land/std@$STD_VERSION/media_types/parse_media_type.ts";
|
||||
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
|
||||
*
|
||||
* assertEquals(
|
||||
* parseMediaType("application/JSON"),
|
||||
* [
|
||||
* "application/json",
|
||||
* undefined
|
||||
* ]
|
||||
* );
|
||||
*
|
||||
* assertEquals(
|
||||
* parseMediaType("text/html; charset=UTF-8"),
|
||||
* [
|
||||
* "application/json",
|
||||
* { charset: "UTF-8" },
|
||||
* ]
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function parseMediaType(
|
||||
v: string,
|
||||
): [mediaType: string, params: Record<string, string> | undefined] {
|
||||
const [base] = v.split(";");
|
||||
const mediaType = base.toLowerCase().trim();
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
// Map of base parameter name -> parameter name -> value
|
||||
// for parameters containing a '*' character.
|
||||
const continuation = new Map<string, Record<string, string>>();
|
||||
|
||||
v = v.slice(base.length);
|
||||
while (v.length) {
|
||||
v = v.trimStart();
|
||||
if (v.length === 0) {
|
||||
break;
|
||||
}
|
||||
const [key, value, rest] = consumeMediaParam(v);
|
||||
if (!key) {
|
||||
if (rest.trim() === ";") {
|
||||
// ignore trailing semicolons
|
||||
break;
|
||||
}
|
||||
throw new TypeError("Invalid media parameter.");
|
||||
}
|
||||
|
||||
let pmap = params;
|
||||
const [baseName, rest2] = key.split("*");
|
||||
if (baseName && rest2 != null) {
|
||||
if (!continuation.has(baseName)) {
|
||||
continuation.set(baseName, {});
|
||||
}
|
||||
pmap = continuation.get(baseName)!;
|
||||
}
|
||||
if (key in pmap) {
|
||||
throw new TypeError("Duplicate key parsed.");
|
||||
}
|
||||
pmap[key] = value;
|
||||
v = rest;
|
||||
}
|
||||
|
||||
// Stitch together any continuations or things with stars
|
||||
// (i.e. RFC 2231 things with stars: "foo*0" or "foo*")
|
||||
let str = "";
|
||||
for (const [key, pieceMap] of continuation) {
|
||||
const singlePartKey = `${key}*`;
|
||||
const v = pieceMap[singlePartKey];
|
||||
if (v) {
|
||||
const decv = decode2331Encoding(v);
|
||||
if (decv) {
|
||||
params[key] = decv;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
str = "";
|
||||
let valid = false;
|
||||
for (let n = 0;; n++) {
|
||||
const simplePart = `${key}*${n}`;
|
||||
let v = pieceMap[simplePart];
|
||||
if (v) {
|
||||
valid = true;
|
||||
str += v;
|
||||
continue;
|
||||
}
|
||||
const encodedPart = `${simplePart}*`;
|
||||
v = pieceMap[encodedPart];
|
||||
if (!v) {
|
||||
break;
|
||||
}
|
||||
valid = true;
|
||||
if (n === 0) {
|
||||
const decv = decode2331Encoding(v);
|
||||
if (decv) {
|
||||
str += decv;
|
||||
}
|
||||
} else {
|
||||
const decv = decodeURI(v);
|
||||
str += decv;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
params[key] = str;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(params).length
|
||||
? [mediaType, params]
|
||||
: [mediaType, undefined];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { types } from "./_db.ts";
|
||||
|
||||
/**
|
||||
* Returns the media type associated with the file extension. Values are
|
||||
* normalized to lower case and matched irrespective of a leading `.`.
|
||||
*
|
||||
* When `extension` has no associated type, the function returns `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { typeByExtension } from "https://deno.land/std@$STD_VERSION/media_types/type_by_extension.ts";
|
||||
*
|
||||
* typeByExtension("js"); // `application/json`
|
||||
* typeByExtension(".HTML"); // `text/html`
|
||||
* typeByExtension("foo"); // undefined
|
||||
* typeByExtension("file.json"); // undefined
|
||||
* ```
|
||||
*/
|
||||
export function typeByExtension(extension: string): string | undefined {
|
||||
extension = extension.startsWith(".") ? extension.slice(1) : extension;
|
||||
// @ts-ignore workaround around denoland/dnt#148
|
||||
return types.get(extension.toLowerCase());
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// Copyright the Browserify authors. MIT License.
|
||||
// Ported from https://github.com/browserify/path-browserify/
|
||||
// This module is browser compatible.
|
||||
|
||||
// Alphabet chars.
|
||||
export const CHAR_UPPERCASE_A = 65; /* A */
|
||||
export const CHAR_LOWERCASE_A = 97; /* a */
|
||||
export const CHAR_UPPERCASE_Z = 90; /* Z */
|
||||
export const CHAR_LOWERCASE_Z = 122; /* z */
|
||||
|
||||
// Non-alphabetic chars.
|
||||
export const CHAR_DOT = 46; /* . */
|
||||
export const CHAR_FORWARD_SLASH = 47; /* / */
|
||||
export const CHAR_BACKWARD_SLASH = 92; /* \ */
|
||||
export const CHAR_VERTICAL_LINE = 124; /* | */
|
||||
export const CHAR_COLON = 58; /* : */
|
||||
export const CHAR_QUESTION_MARK = 63; /* ? */
|
||||
export const CHAR_UNDERSCORE = 95; /* _ */
|
||||
export const CHAR_LINE_FEED = 10; /* \n */
|
||||
export const CHAR_CARRIAGE_RETURN = 13; /* \r */
|
||||
export const CHAR_TAB = 9; /* \t */
|
||||
export const CHAR_FORM_FEED = 12; /* \f */
|
||||
export const CHAR_EXCLAMATION_MARK = 33; /* ! */
|
||||
export const CHAR_HASH = 35; /* # */
|
||||
export const CHAR_SPACE = 32; /* */
|
||||
export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */
|
||||
export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */
|
||||
export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */
|
||||
export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */
|
||||
export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */
|
||||
export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */
|
||||
export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */
|
||||
export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */
|
||||
export const CHAR_HYPHEN_MINUS = 45; /* - */
|
||||
export const CHAR_PLUS = 43; /* + */
|
||||
export const CHAR_DOUBLE_QUOTE = 34; /* " */
|
||||
export const CHAR_SINGLE_QUOTE = 39; /* ' */
|
||||
export const CHAR_PERCENT = 37; /* % */
|
||||
export const CHAR_SEMICOLON = 59; /* ; */
|
||||
export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */
|
||||
export const CHAR_GRAVE_ACCENT = 96; /* ` */
|
||||
export const CHAR_AT = 64; /* @ */
|
||||
export const CHAR_AMPERSAND = 38; /* & */
|
||||
export const CHAR_EQUAL = 61; /* = */
|
||||
|
||||
// Digits
|
||||
export const CHAR_0 = 48; /* 0 */
|
||||
export const CHAR_9 = 57; /* 9 */
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* A parsed path object generated by path.parse() or consumed by path.format().
|
||||
*/
|
||||
export interface ParsedPath {
|
||||
/**
|
||||
* The root of the path such as '/' or 'c:\'
|
||||
*/
|
||||
root: string;
|
||||
/**
|
||||
* The full directory path such as '/home/user/dir' or 'c:\path\dir'
|
||||
*/
|
||||
dir: string;
|
||||
/**
|
||||
* The file name including extension (if any) such as 'index.html'
|
||||
*/
|
||||
base: string;
|
||||
/**
|
||||
* The file extension (if any) such as '.html'
|
||||
*/
|
||||
ext: string;
|
||||
/**
|
||||
* The file name without extension (if any) such as 'index'
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type FormatInputPathObject = Partial<ParsedPath>;
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// Copyright the Browserify authors. MIT License.
|
||||
// Ported from https://github.com/browserify/path-browserify/
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { FormatInputPathObject } from "./_interface.ts";
|
||||
import {
|
||||
CHAR_BACKWARD_SLASH,
|
||||
CHAR_DOT,
|
||||
CHAR_FORWARD_SLASH,
|
||||
CHAR_LOWERCASE_A,
|
||||
CHAR_LOWERCASE_Z,
|
||||
CHAR_UPPERCASE_A,
|
||||
CHAR_UPPERCASE_Z,
|
||||
} from "./_constants.ts";
|
||||
|
||||
export function assertPath(path: string) {
|
||||
if (typeof path !== "string") {
|
||||
throw new TypeError(
|
||||
`Path must be a string. Received ${JSON.stringify(path)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isPosixPathSeparator(code: number): boolean {
|
||||
return code === CHAR_FORWARD_SLASH;
|
||||
}
|
||||
|
||||
export function isPathSeparator(code: number): boolean {
|
||||
return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH;
|
||||
}
|
||||
|
||||
export function isWindowsDeviceRoot(code: number): boolean {
|
||||
return (
|
||||
(code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
|
||||
(code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z)
|
||||
);
|
||||
}
|
||||
|
||||
// Resolves . and .. elements in a path with directory names
|
||||
export function normalizeString(
|
||||
path: string,
|
||||
allowAboveRoot: boolean,
|
||||
separator: string,
|
||||
isPathSeparator: (code: number) => boolean,
|
||||
): string {
|
||||
let res = "";
|
||||
let lastSegmentLength = 0;
|
||||
let lastSlash = -1;
|
||||
let dots = 0;
|
||||
let code: number | undefined;
|
||||
for (let i = 0, len = path.length; i <= len; ++i) {
|
||||
if (i < len) code = path.charCodeAt(i);
|
||||
else if (isPathSeparator(code!)) break;
|
||||
else code = CHAR_FORWARD_SLASH;
|
||||
|
||||
if (isPathSeparator(code!)) {
|
||||
if (lastSlash === i - 1 || dots === 1) {
|
||||
// NOOP
|
||||
} else if (lastSlash !== i - 1 && dots === 2) {
|
||||
if (
|
||||
res.length < 2 ||
|
||||
lastSegmentLength !== 2 ||
|
||||
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
|
||||
res.charCodeAt(res.length - 2) !== CHAR_DOT
|
||||
) {
|
||||
if (res.length > 2) {
|
||||
const lastSlashIndex = res.lastIndexOf(separator);
|
||||
if (lastSlashIndex === -1) {
|
||||
res = "";
|
||||
lastSegmentLength = 0;
|
||||
} else {
|
||||
res = res.slice(0, lastSlashIndex);
|
||||
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
} else if (res.length === 2 || res.length === 1) {
|
||||
res = "";
|
||||
lastSegmentLength = 0;
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (allowAboveRoot) {
|
||||
if (res.length > 0) res += `${separator}..`;
|
||||
else res = "..";
|
||||
lastSegmentLength = 2;
|
||||
}
|
||||
} else {
|
||||
if (res.length > 0) res += separator + path.slice(lastSlash + 1, i);
|
||||
else res = path.slice(lastSlash + 1, i);
|
||||
lastSegmentLength = i - lastSlash - 1;
|
||||
}
|
||||
lastSlash = i;
|
||||
dots = 0;
|
||||
} else if (code === CHAR_DOT && dots !== -1) {
|
||||
++dots;
|
||||
} else {
|
||||
dots = -1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function _format(
|
||||
sep: string,
|
||||
pathObject: FormatInputPathObject,
|
||||
): string {
|
||||
const dir: string | undefined = pathObject.dir || pathObject.root;
|
||||
const base: string = pathObject.base ||
|
||||
(pathObject.name || "") + (pathObject.ext || "");
|
||||
if (!dir) return base;
|
||||
if (base === sep) return dir;
|
||||
if (dir === pathObject.root) return dir + base;
|
||||
return dir + sep + base;
|
||||
}
|
||||
|
||||
const WHITESPACE_ENCODINGS: Record<string, string> = {
|
||||
"\u0009": "%09",
|
||||
"\u000A": "%0A",
|
||||
"\u000B": "%0B",
|
||||
"\u000C": "%0C",
|
||||
"\u000D": "%0D",
|
||||
"\u0020": "%20",
|
||||
};
|
||||
|
||||
export function encodeWhitespace(string: string): string {
|
||||
return string.replaceAll(/[\s]/g, (c) => {
|
||||
return WHITESPACE_ENCODINGS[c] ?? c;
|
||||
});
|
||||
}
|
||||
|
||||
export function lastPathSegment(
|
||||
path: string,
|
||||
isSep: (char: number) => boolean,
|
||||
start = 0,
|
||||
): string {
|
||||
let matchedNonSeparator = false;
|
||||
let end = path.length;
|
||||
|
||||
for (let i = path.length - 1; i >= start; --i) {
|
||||
if (isSep(path.charCodeAt(i))) {
|
||||
if (matchedNonSeparator) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
} else if (!matchedNonSeparator) {
|
||||
matchedNonSeparator = true;
|
||||
end = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return path.slice(start, end);
|
||||
}
|
||||
|
||||
export function stripTrailingSeparators(
|
||||
segment: string,
|
||||
isSep: (char: number) => boolean,
|
||||
): string {
|
||||
if (segment.length <= 1) {
|
||||
return segment;
|
||||
}
|
||||
|
||||
let end = segment.length;
|
||||
|
||||
for (let i = segment.length - 1; i > 0; i--) {
|
||||
if (isSep(segment.charCodeAt(i))) {
|
||||
end = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return segment.slice(0, end);
|
||||
}
|
||||
|
||||
export function stripSuffix(name: string, suffix: string): string {
|
||||
if (suffix.length >= name.length) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const lenDiff = name.length - suffix.length;
|
||||
|
||||
for (let i = suffix.length - 1; i >= 0; --i) {
|
||||
if (name.charCodeAt(lenDiff + i) !== suffix.charCodeAt(i)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return name.slice(0, -suffix.length);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { SEP } from "./separator.ts";
|
||||
|
||||
/** Determines the common path from a set of paths, using an optional separator,
|
||||
* which defaults to the OS default separator.
|
||||
*
|
||||
* ```ts
|
||||
* import { common } from "https://deno.land/std@$STD_VERSION/path/mod.ts";
|
||||
* const p = common([
|
||||
* "./deno/std/path/mod.ts",
|
||||
* "./deno/std/fs/mod.ts",
|
||||
* ]);
|
||||
* console.log(p); // "./deno/std/"
|
||||
* ```
|
||||
*/
|
||||
export function common(paths: string[], sep = SEP): string {
|
||||
const [first = "", ...remaining] = paths;
|
||||
if (first === "" || remaining.length === 0) {
|
||||
return first.substring(0, first.lastIndexOf(sep) + 1);
|
||||
}
|
||||
const parts = first.split(sep);
|
||||
|
||||
let endOfPrefix = parts.length;
|
||||
for (const path of remaining) {
|
||||
const compare = path.split(sep);
|
||||
for (let i = 0; i < endOfPrefix; i++) {
|
||||
if (compare[i] !== parts[i]) {
|
||||
endOfPrefix = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (endOfPrefix === 0) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
const prefix = parts.slice(0, endOfPrefix).join(sep);
|
||||
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
|
||||
}
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { isWindows, osType } from "../_util/os.ts";
|
||||
import { SEP, SEP_PATTERN } from "./separator.ts";
|
||||
import * as _win32 from "./win32.ts";
|
||||
import * as _posix from "./posix.ts";
|
||||
import type { OSType } from "../_util/os.ts";
|
||||
|
||||
const path = isWindows ? _win32 : _posix;
|
||||
const { join, normalize } = path;
|
||||
|
||||
export interface GlobOptions {
|
||||
/** Extended glob syntax.
|
||||
* See https://www.linuxjournal.com/content/bash-extended-globbing.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
extended?: boolean;
|
||||
/** Globstar syntax.
|
||||
* See https://www.linuxjournal.com/content/globstar-new-bash-globbing-option.
|
||||
* If false, `**` is treated like `*`.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
globstar?: boolean;
|
||||
/** Whether globstar should be case-insensitive. */
|
||||
caseInsensitive?: boolean;
|
||||
/** Operating system. Defaults to the native OS. */
|
||||
os?: OSType;
|
||||
}
|
||||
|
||||
export type GlobToRegExpOptions = GlobOptions;
|
||||
|
||||
const regExpEscapeChars = [
|
||||
"!",
|
||||
"$",
|
||||
"(",
|
||||
")",
|
||||
"*",
|
||||
"+",
|
||||
".",
|
||||
"=",
|
||||
"?",
|
||||
"[",
|
||||
"\\",
|
||||
"^",
|
||||
"{",
|
||||
"|",
|
||||
];
|
||||
const rangeEscapeChars = ["-", "\\", "]"];
|
||||
|
||||
/** Convert a glob string to a regular expression.
|
||||
*
|
||||
* Tries to match bash glob expansion as closely as possible.
|
||||
*
|
||||
* Basic glob syntax:
|
||||
* - `*` - Matches everything without leaving the path segment.
|
||||
* - `?` - Matches any single character.
|
||||
* - `{foo,bar}` - Matches `foo` or `bar`.
|
||||
* - `[abcd]` - Matches `a`, `b`, `c` or `d`.
|
||||
* - `[a-d]` - Matches `a`, `b`, `c` or `d`.
|
||||
* - `[!abcd]` - Matches any single character besides `a`, `b`, `c` or `d`.
|
||||
* - `[[:<class>:]]` - Matches any character belonging to `<class>`.
|
||||
* - `[[:alnum:]]` - Matches any digit or letter.
|
||||
* - `[[:digit:]abc]` - Matches any digit, `a`, `b` or `c`.
|
||||
* - See https://facelessuser.github.io/wcmatch/glob/#posix-character-classes
|
||||
* for a complete list of supported character classes.
|
||||
* - `\` - Escapes the next character for an `os` other than `"windows"`.
|
||||
* - \` - Escapes the next character for `os` set to `"windows"`.
|
||||
* - `/` - Path separator.
|
||||
* - `\` - Additional path separator only for `os` set to `"windows"`.
|
||||
*
|
||||
* Extended syntax:
|
||||
* - Requires `{ extended: true }`.
|
||||
* - `?(foo|bar)` - Matches 0 or 1 instance of `{foo,bar}`.
|
||||
* - `@(foo|bar)` - Matches 1 instance of `{foo,bar}`. They behave the same.
|
||||
* - `*(foo|bar)` - Matches _n_ instances of `{foo,bar}`.
|
||||
* - `+(foo|bar)` - Matches _n > 0_ instances of `{foo,bar}`.
|
||||
* - `!(foo|bar)` - Matches anything other than `{foo,bar}`.
|
||||
* - See https://www.linuxjournal.com/content/bash-extended-globbing.
|
||||
*
|
||||
* Globstar syntax:
|
||||
* - Requires `{ globstar: true }`.
|
||||
* - `**` - Matches any number of any path segments.
|
||||
* - Must comprise its entire path segment in the provided glob.
|
||||
* - See https://www.linuxjournal.com/content/globstar-new-bash-globbing-option.
|
||||
*
|
||||
* Note the following properties:
|
||||
* - The generated `RegExp` is anchored at both start and end.
|
||||
* - Repeating and trailing separators are tolerated. Trailing separators in the
|
||||
* provided glob have no meaning and are discarded.
|
||||
* - Absolute globs will only match absolute paths, etc.
|
||||
* - Empty globs will match nothing.
|
||||
* - Any special glob syntax must be contained to one path segment. For example,
|
||||
* `?(foo|bar/baz)` is invalid. The separator will take precedence and the
|
||||
* first segment ends with an unclosed group.
|
||||
* - If a path segment ends with unclosed groups or a dangling escape prefix, a
|
||||
* parse error has occurred. Every character for that segment is taken
|
||||
* literally in this event.
|
||||
*
|
||||
* Limitations:
|
||||
* - A negative group like `!(foo|bar)` will wrongly be converted to a negative
|
||||
* look-ahead followed by a wildcard. This means that `!(foo).js` will wrongly
|
||||
* fail to match `foobar.js`, even though `foobar` is not `foo`. Effectively,
|
||||
* `!(foo|bar)` is treated like `!(@(foo|bar)*)`. This will work correctly if
|
||||
* the group occurs not nested at the end of the segment. */
|
||||
export function globToRegExp(
|
||||
glob: string,
|
||||
{
|
||||
extended = true,
|
||||
globstar: globstarOption = true,
|
||||
os = osType,
|
||||
caseInsensitive = false,
|
||||
}: GlobToRegExpOptions = {},
|
||||
): RegExp {
|
||||
if (glob == "") {
|
||||
return /(?!)/;
|
||||
}
|
||||
|
||||
const sep = os == "windows" ? "(?:\\\\|/)+" : "/+";
|
||||
const sepMaybe = os == "windows" ? "(?:\\\\|/)*" : "/*";
|
||||
const seps = os == "windows" ? ["\\", "/"] : ["/"];
|
||||
const globstar = os == "windows"
|
||||
? "(?:[^\\\\/]*(?:\\\\|/|$)+)*"
|
||||
: "(?:[^/]*(?:/|$)+)*";
|
||||
const wildcard = os == "windows" ? "[^\\\\/]*" : "[^/]*";
|
||||
const escapePrefix = os == "windows" ? "`" : "\\";
|
||||
|
||||
// Remove trailing separators.
|
||||
let newLength = glob.length;
|
||||
for (; newLength > 1 && seps.includes(glob[newLength - 1]); newLength--);
|
||||
glob = glob.slice(0, newLength);
|
||||
|
||||
let regExpString = "";
|
||||
|
||||
// Terminates correctly. Trust that `j` is incremented every iteration.
|
||||
for (let j = 0; j < glob.length;) {
|
||||
let segment = "";
|
||||
const groupStack: string[] = [];
|
||||
let inRange = false;
|
||||
let inEscape = false;
|
||||
let endsWithSep = false;
|
||||
let i = j;
|
||||
|
||||
// Terminates with `i` at the non-inclusive end of the current segment.
|
||||
for (; i < glob.length && !seps.includes(glob[i]); i++) {
|
||||
if (inEscape) {
|
||||
inEscape = false;
|
||||
const escapeChars = inRange ? rangeEscapeChars : regExpEscapeChars;
|
||||
segment += escapeChars.includes(glob[i]) ? `\\${glob[i]}` : glob[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == escapePrefix) {
|
||||
inEscape = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "[") {
|
||||
if (!inRange) {
|
||||
inRange = true;
|
||||
segment += "[";
|
||||
if (glob[i + 1] == "!") {
|
||||
i++;
|
||||
segment += "^";
|
||||
} else if (glob[i + 1] == "^") {
|
||||
i++;
|
||||
segment += "\\^";
|
||||
}
|
||||
continue;
|
||||
} else if (glob[i + 1] == ":") {
|
||||
let k = i + 1;
|
||||
let value = "";
|
||||
while (glob[k + 1] != null && glob[k + 1] != ":") {
|
||||
value += glob[k + 1];
|
||||
k++;
|
||||
}
|
||||
if (glob[k + 1] == ":" && glob[k + 2] == "]") {
|
||||
i = k + 2;
|
||||
if (value == "alnum") segment += "\\dA-Za-z";
|
||||
else if (value == "alpha") segment += "A-Za-z";
|
||||
else if (value == "ascii") segment += "\x00-\x7F";
|
||||
else if (value == "blank") segment += "\t ";
|
||||
else if (value == "cntrl") segment += "\x00-\x1F\x7F";
|
||||
else if (value == "digit") segment += "\\d";
|
||||
else if (value == "graph") segment += "\x21-\x7E";
|
||||
else if (value == "lower") segment += "a-z";
|
||||
else if (value == "print") segment += "\x20-\x7E";
|
||||
else if (value == "punct") {
|
||||
segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_‘{|}~";
|
||||
} else if (value == "space") segment += "\\s\v";
|
||||
else if (value == "upper") segment += "A-Z";
|
||||
else if (value == "word") segment += "\\w";
|
||||
else if (value == "xdigit") segment += "\\dA-Fa-f";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (glob[i] == "]" && inRange) {
|
||||
inRange = false;
|
||||
segment += "]";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
if (glob[i] == "\\") {
|
||||
segment += `\\\\`;
|
||||
} else {
|
||||
segment += glob[i];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
glob[i] == ")" && groupStack.length > 0 &&
|
||||
groupStack[groupStack.length - 1] != "BRACE"
|
||||
) {
|
||||
segment += ")";
|
||||
const type = groupStack.pop()!;
|
||||
if (type == "!") {
|
||||
segment += wildcard;
|
||||
} else if (type != "@") {
|
||||
segment += type;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
glob[i] == "|" && groupStack.length > 0 &&
|
||||
groupStack[groupStack.length - 1] != "BRACE"
|
||||
) {
|
||||
segment += "|";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "+" && extended && glob[i + 1] == "(") {
|
||||
i++;
|
||||
groupStack.push("+");
|
||||
segment += "(?:";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "@" && extended && glob[i + 1] == "(") {
|
||||
i++;
|
||||
groupStack.push("@");
|
||||
segment += "(?:";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "?") {
|
||||
if (extended && glob[i + 1] == "(") {
|
||||
i++;
|
||||
groupStack.push("?");
|
||||
segment += "(?:";
|
||||
} else {
|
||||
segment += ".";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "!" && extended && glob[i + 1] == "(") {
|
||||
i++;
|
||||
groupStack.push("!");
|
||||
segment += "(?!";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "{") {
|
||||
groupStack.push("BRACE");
|
||||
segment += "(?:";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "}" && groupStack[groupStack.length - 1] == "BRACE") {
|
||||
groupStack.pop();
|
||||
segment += ")";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "," && groupStack[groupStack.length - 1] == "BRACE") {
|
||||
segment += "|";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (glob[i] == "*") {
|
||||
if (extended && glob[i + 1] == "(") {
|
||||
i++;
|
||||
groupStack.push("*");
|
||||
segment += "(?:";
|
||||
} else {
|
||||
const prevChar = glob[i - 1];
|
||||
let numStars = 1;
|
||||
while (glob[i + 1] == "*") {
|
||||
i++;
|
||||
numStars++;
|
||||
}
|
||||
const nextChar = glob[i + 1];
|
||||
if (
|
||||
globstarOption && numStars == 2 &&
|
||||
[...seps, undefined].includes(prevChar) &&
|
||||
[...seps, undefined].includes(nextChar)
|
||||
) {
|
||||
segment += globstar;
|
||||
endsWithSep = true;
|
||||
} else {
|
||||
segment += wildcard;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
segment += regExpEscapeChars.includes(glob[i]) ? `\\${glob[i]}` : glob[i];
|
||||
}
|
||||
|
||||
// Check for unclosed groups or a dangling backslash.
|
||||
if (groupStack.length > 0 || inRange || inEscape) {
|
||||
// Parse failure. Take all characters from this segment literally.
|
||||
segment = "";
|
||||
for (const c of glob.slice(j, i)) {
|
||||
segment += regExpEscapeChars.includes(c) ? `\\${c}` : c;
|
||||
endsWithSep = false;
|
||||
}
|
||||
}
|
||||
|
||||
regExpString += segment;
|
||||
if (!endsWithSep) {
|
||||
regExpString += i < glob.length ? sep : sepMaybe;
|
||||
endsWithSep = true;
|
||||
}
|
||||
|
||||
// Terminates with `i` at the start of the next segment.
|
||||
while (seps.includes(glob[i])) i++;
|
||||
|
||||
// Check that the next value of `j` is indeed higher than the current value.
|
||||
if (!(i > j)) {
|
||||
throw new Error("Assertion failure: i > j (potential infinite loop)");
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
|
||||
regExpString = `^${regExpString}$`;
|
||||
return new RegExp(regExpString, caseInsensitive ? "i" : "");
|
||||
}
|
||||
|
||||
/** Test whether the given string is a glob */
|
||||
export function isGlob(str: string): boolean {
|
||||
const chars: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
|
||||
const regex =
|
||||
/\\(.)|(^!|\*|\?|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/;
|
||||
|
||||
if (str === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(str))) {
|
||||
if (match[2]) return true;
|
||||
let idx = match.index + match[0].length;
|
||||
|
||||
// if an open bracket/brace/paren is escaped,
|
||||
// set the index to the next closing character
|
||||
const open = match[1];
|
||||
const close = open ? chars[open] : null;
|
||||
if (open && close) {
|
||||
const n = str.indexOf(close, idx);
|
||||
if (n !== -1) {
|
||||
idx = n + 1;
|
||||
}
|
||||
}
|
||||
|
||||
str = str.slice(idx);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Like normalize(), but doesn't collapse "**\/.." when `globstar` is true. */
|
||||
export function normalizeGlob(
|
||||
glob: string,
|
||||
{ globstar = false }: GlobOptions = {},
|
||||
): string {
|
||||
if (glob.match(/\0/g)) {
|
||||
throw new Error(`Glob contains invalid characters: "${glob}"`);
|
||||
}
|
||||
if (!globstar) {
|
||||
return normalize(glob);
|
||||
}
|
||||
const s = SEP_PATTERN.source;
|
||||
const badParentPattern = new RegExp(
|
||||
`(?<=(${s}|^)\\*\\*${s})\\.\\.(?=${s}|$)`,
|
||||
"g",
|
||||
);
|
||||
return normalize(glob.replace(badParentPattern, "\0")).replace(/\0/g, "..");
|
||||
}
|
||||
|
||||
/** Like join(), but doesn't collapse "**\/.." when `globstar` is true. */
|
||||
export function joinGlobs(
|
||||
globs: string[],
|
||||
{ extended = true, globstar = false }: GlobOptions = {},
|
||||
): string {
|
||||
if (!globstar || globs.length == 0) {
|
||||
return join(...globs);
|
||||
}
|
||||
if (globs.length === 0) return ".";
|
||||
let joined: string | undefined;
|
||||
for (const glob of globs) {
|
||||
const path = glob;
|
||||
if (path.length > 0) {
|
||||
if (!joined) joined = path;
|
||||
else joined += `${SEP}${path}`;
|
||||
}
|
||||
}
|
||||
if (!joined) return ".";
|
||||
return normalizeGlob(joined, { extended, globstar });
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// Copyright the Browserify authors. MIT License.
|
||||
// Ported mostly from https://github.com/browserify/path-browserify/
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Utilities for working with OS-specific file paths.
|
||||
*
|
||||
* Codes in the examples uses POSIX path but it automatically use Windows path
|
||||
* on Windows. Use methods under `posix` or `win32` object instead to handle non
|
||||
* platform specific path like:
|
||||
* ```ts
|
||||
* import { posix, win32 } from "https://deno.land/std@$STD_VERSION/path/mod.ts";
|
||||
* const p1 = posix.fromFileUrl("file:///home/foo");
|
||||
* const p2 = win32.fromFileUrl("file:///home/foo");
|
||||
* console.log(p1); // "/home/foo"
|
||||
* console.log(p2); // "\\home\\foo"
|
||||
* ```
|
||||
*
|
||||
* This module is browser compatible.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { isWindows } from "../_util/os.ts";
|
||||
import * as _win32 from "./win32.ts";
|
||||
import * as _posix from "./posix.ts";
|
||||
|
||||
const path = isWindows ? _win32 : _posix;
|
||||
|
||||
export const win32 = _win32;
|
||||
export const posix = _posix;
|
||||
export const {
|
||||
basename,
|
||||
delimiter,
|
||||
dirname,
|
||||
extname,
|
||||
format,
|
||||
fromFileUrl,
|
||||
isAbsolute,
|
||||
join,
|
||||
normalize,
|
||||
parse,
|
||||
relative,
|
||||
resolve,
|
||||
toFileUrl,
|
||||
toNamespacedPath,
|
||||
} = path;
|
||||
|
||||
/** @deprecated (will be removed after 0.188.0) Use SEP intead. */
|
||||
export const sep = path.sep;
|
||||
|
||||
export * from "./common.ts";
|
||||
export { SEP, SEP_PATTERN } from "./separator.ts";
|
||||
export * from "./_interface.ts";
|
||||
export * from "./glob.ts";
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// Copyright the Browserify authors. MIT License.
|
||||
// Ported from https://github.com/browserify/path-browserify/
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { FormatInputPathObject, ParsedPath } from "./_interface.ts";
|
||||
import { CHAR_DOT } from "./_constants.ts";
|
||||
|
||||
import {
|
||||
_format,
|
||||
assertPath,
|
||||
encodeWhitespace,
|
||||
isPosixPathSeparator,
|
||||
lastPathSegment,
|
||||
normalizeString,
|
||||
stripSuffix,
|
||||
stripTrailingSeparators,
|
||||
} from "./_util.ts";
|
||||
|
||||
export const sep = "/";
|
||||
export const delimiter = ":";
|
||||
|
||||
// path.resolve([from ...], to)
|
||||
/**
|
||||
* Resolves `pathSegments` into an absolute path.
|
||||
* @param pathSegments an array of path segments
|
||||
*/
|
||||
export function resolve(...pathSegments: string[]): string {
|
||||
let resolvedPath = "";
|
||||
let resolvedAbsolute = false;
|
||||
|
||||
for (let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
|
||||
let path: string;
|
||||
|
||||
if (i >= 0) path = pathSegments[i];
|
||||
else {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const { Deno } = globalThis as any;
|
||||
if (typeof Deno?.cwd !== "function") {
|
||||
throw new TypeError("Resolved a relative path without a CWD.");
|
||||
}
|
||||
path = Deno.cwd();
|
||||
}
|
||||
|
||||
assertPath(path);
|
||||
|
||||
// Skip empty entries
|
||||
if (path.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resolvedPath = `${path}/${resolvedPath}`;
|
||||
resolvedAbsolute = isPosixPathSeparator(path.charCodeAt(0));
|
||||
}
|
||||
|
||||
// At this point the path should be resolved to a full absolute path, but
|
||||
// handle relative paths to be safe (might happen when process.cwd() fails)
|
||||
|
||||
// Normalize the path
|
||||
resolvedPath = normalizeString(
|
||||
resolvedPath,
|
||||
!resolvedAbsolute,
|
||||
"/",
|
||||
isPosixPathSeparator,
|
||||
);
|
||||
|
||||
if (resolvedAbsolute) {
|
||||
if (resolvedPath.length > 0) return `/${resolvedPath}`;
|
||||
else return "/";
|
||||
} else if (resolvedPath.length > 0) return resolvedPath;
|
||||
else return ".";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the `path`, resolving `'..'` and `'.'` segments.
|
||||
* Note that resolving these segments does not necessarily mean that all will be eliminated.
|
||||
* A `'..'` at the top-level will be preserved, and an empty path is canonically `'.'`.
|
||||
* @param path to be normalized
|
||||
*/
|
||||
export function normalize(path: string): string {
|
||||
assertPath(path);
|
||||
|
||||
if (path.length === 0) return ".";
|
||||
|
||||
const isAbsolute = isPosixPathSeparator(path.charCodeAt(0));
|
||||
const trailingSeparator = isPosixPathSeparator(
|
||||
path.charCodeAt(path.length - 1),
|
||||
);
|
||||
|
||||
// Normalize the path
|
||||
path = normalizeString(path, !isAbsolute, "/", isPosixPathSeparator);
|
||||
|
||||
if (path.length === 0 && !isAbsolute) path = ".";
|
||||
if (path.length > 0 && trailingSeparator) path += "/";
|
||||
|
||||
if (isAbsolute) return `/${path}`;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether provided path is absolute
|
||||
* @param path to be verified as absolute
|
||||
*/
|
||||
export function isAbsolute(path: string): boolean {
|
||||
assertPath(path);
|
||||
return path.length > 0 && isPosixPathSeparator(path.charCodeAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Join all given a sequence of `paths`,then normalizes the resulting path.
|
||||
* @param paths to be joined and normalized
|
||||
*/
|
||||
export function join(...paths: string[]): string {
|
||||
if (paths.length === 0) return ".";
|
||||
let joined: string | undefined;
|
||||
for (let i = 0, len = paths.length; i < len; ++i) {
|
||||
const path = paths[i];
|
||||
assertPath(path);
|
||||
if (path.length > 0) {
|
||||
if (!joined) joined = path;
|
||||
else joined += `/${path}`;
|
||||
}
|
||||
}
|
||||
if (!joined) return ".";
|
||||
return normalize(joined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relative path from `from` to `to` based on current working directory.
|
||||
* @param from path in current working directory
|
||||
* @param to path in current working directory
|
||||
*/
|
||||
export function relative(from: string, to: string): string {
|
||||
assertPath(from);
|
||||
assertPath(to);
|
||||
|
||||
if (from === to) return "";
|
||||
|
||||
from = resolve(from);
|
||||
to = resolve(to);
|
||||
|
||||
if (from === to) return "";
|
||||
|
||||
// Trim any leading backslashes
|
||||
let fromStart = 1;
|
||||
const fromEnd = from.length;
|
||||
for (; fromStart < fromEnd; ++fromStart) {
|
||||
if (!isPosixPathSeparator(from.charCodeAt(fromStart))) break;
|
||||
}
|
||||
const fromLen = fromEnd - fromStart;
|
||||
|
||||
// Trim any leading backslashes
|
||||
let toStart = 1;
|
||||
const toEnd = to.length;
|
||||
for (; toStart < toEnd; ++toStart) {
|
||||
if (!isPosixPathSeparator(to.charCodeAt(toStart))) break;
|
||||
}
|
||||
const toLen = toEnd - toStart;
|
||||
|
||||
// Compare paths to find the longest common path from root
|
||||
const length = fromLen < toLen ? fromLen : toLen;
|
||||
let lastCommonSep = -1;
|
||||
let i = 0;
|
||||
for (; i <= length; ++i) {
|
||||
if (i === length) {
|
||||
if (toLen > length) {
|
||||
if (isPosixPathSeparator(to.charCodeAt(toStart + i))) {
|
||||
// We get here if `from` is the exact base path for `to`.
|
||||
// For example: from='/foo/bar'; to='/foo/bar/baz'
|
||||
return to.slice(toStart + i + 1);
|
||||
} else if (i === 0) {
|
||||
// We get here if `from` is the root
|
||||
// For example: from='/'; to='/foo'
|
||||
return to.slice(toStart + i);
|
||||
}
|
||||
} else if (fromLen > length) {
|
||||
if (isPosixPathSeparator(from.charCodeAt(fromStart + i))) {
|
||||
// We get here if `to` is the exact base path for `from`.
|
||||
// For example: from='/foo/bar/baz'; to='/foo/bar'
|
||||
lastCommonSep = i;
|
||||
} else if (i === 0) {
|
||||
// We get here if `to` is the root.
|
||||
// For example: from='/foo'; to='/'
|
||||
lastCommonSep = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
const fromCode = from.charCodeAt(fromStart + i);
|
||||
const toCode = to.charCodeAt(toStart + i);
|
||||
if (fromCode !== toCode) break;
|
||||
else if (isPosixPathSeparator(fromCode)) lastCommonSep = i;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
// Generate the relative path based on the path difference between `to`
|
||||
// and `from`
|
||||
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
|
||||
if (i === fromEnd || isPosixPathSeparator(from.charCodeAt(i))) {
|
||||
if (out.length === 0) out += "..";
|
||||
else out += "/..";
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, append the rest of the destination (`to`) path that comes after
|
||||
// the common path parts
|
||||
if (out.length > 0) return out + to.slice(toStart + lastCommonSep);
|
||||
else {
|
||||
toStart += lastCommonSep;
|
||||
if (isPosixPathSeparator(to.charCodeAt(toStart))) ++toStart;
|
||||
return to.slice(toStart);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves path to a namespace path
|
||||
* @param path to resolve to namespace
|
||||
*/
|
||||
export function toNamespacedPath(path: string): string {
|
||||
// Non-op on posix systems
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the directory path of a `path`.
|
||||
* @param path - path to extract the directory from.
|
||||
*/
|
||||
export function dirname(path: string): string {
|
||||
if (path.length === 0) return ".";
|
||||
|
||||
let end = -1;
|
||||
let matchedNonSeparator = false;
|
||||
|
||||
for (let i = path.length - 1; i >= 1; --i) {
|
||||
if (isPosixPathSeparator(path.charCodeAt(i))) {
|
||||
if (matchedNonSeparator) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
matchedNonSeparator = true;
|
||||
}
|
||||
}
|
||||
|
||||
// No matches. Fallback based on provided path:
|
||||
//
|
||||
// - leading slashes paths
|
||||
// "/foo" => "/"
|
||||
// "///foo" => "/"
|
||||
// - no slash path
|
||||
// "foo" => "."
|
||||
if (end === -1) {
|
||||
return isPosixPathSeparator(path.charCodeAt(0)) ? "/" : ".";
|
||||
}
|
||||
|
||||
return stripTrailingSeparators(
|
||||
path.slice(0, end),
|
||||
isPosixPathSeparator,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last portion of a `path`.
|
||||
* Trailing directory separators are ignored, and optional suffix is removed.
|
||||
*
|
||||
* @param path - path to extract the name from.
|
||||
* @param [suffix] - suffix to remove from extracted name.
|
||||
*/
|
||||
export function basename(path: string, suffix = ""): string {
|
||||
assertPath(path);
|
||||
|
||||
if (path.length === 0) return path;
|
||||
|
||||
if (typeof suffix !== "string") {
|
||||
throw new TypeError(
|
||||
`Suffix must be a string. Received ${JSON.stringify(suffix)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lastSegment = lastPathSegment(path, isPosixPathSeparator);
|
||||
const strippedSegment = stripTrailingSeparators(
|
||||
lastSegment,
|
||||
isPosixPathSeparator,
|
||||
);
|
||||
return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the extension of the `path` with leading period.
|
||||
* @param path with extension
|
||||
* @returns extension (ex. for `file.ts` returns `.ts`)
|
||||
*/
|
||||
export function extname(path: string): string {
|
||||
assertPath(path);
|
||||
let startDot = -1;
|
||||
let startPart = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
for (let i = path.length - 1; i >= 0; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (isPosixPathSeparator(code)) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CHAR_DOT) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) startDot = i;
|
||||
else if (preDotState !== 1) preDotState = 1;
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a path from `FormatInputPathObject` object.
|
||||
* @param pathObject with path
|
||||
*/
|
||||
export function format(pathObject: FormatInputPathObject): string {
|
||||
if (pathObject === null || typeof pathObject !== "object") {
|
||||
throw new TypeError(
|
||||
`The "pathObject" argument must be of type Object. Received type ${typeof pathObject}`,
|
||||
);
|
||||
}
|
||||
return _format("/", pathObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a `ParsedPath` object of the `path`.
|
||||
* @param path to process
|
||||
*/
|
||||
export function parse(path: string): ParsedPath {
|
||||
assertPath(path);
|
||||
|
||||
const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" };
|
||||
if (path.length === 0) return ret;
|
||||
const isAbsolute = isPosixPathSeparator(path.charCodeAt(0));
|
||||
let start: number;
|
||||
if (isAbsolute) {
|
||||
ret.root = "/";
|
||||
start = 1;
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
let startDot = -1;
|
||||
let startPart = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
let i = path.length - 1;
|
||||
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
|
||||
// Get non-dir info
|
||||
for (; i >= start; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (isPosixPathSeparator(code)) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CHAR_DOT) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) startDot = i;
|
||||
else if (preDotState !== 1) preDotState = 1;
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
if (end !== -1) {
|
||||
if (startPart === 0 && isAbsolute) {
|
||||
ret.base = ret.name = path.slice(1, end);
|
||||
} else {
|
||||
ret.base = ret.name = path.slice(startPart, end);
|
||||
}
|
||||
}
|
||||
// Fallback to '/' in case there is no basename
|
||||
ret.base = ret.base || "/";
|
||||
} else {
|
||||
if (startPart === 0 && isAbsolute) {
|
||||
ret.name = path.slice(1, startDot);
|
||||
ret.base = path.slice(1, end);
|
||||
} else {
|
||||
ret.name = path.slice(startPart, startDot);
|
||||
ret.base = path.slice(startPart, end);
|
||||
}
|
||||
ret.ext = path.slice(startDot, end);
|
||||
}
|
||||
|
||||
if (startPart > 0) {
|
||||
ret.dir = stripTrailingSeparators(
|
||||
path.slice(0, startPart - 1),
|
||||
isPosixPathSeparator,
|
||||
);
|
||||
} else if (isAbsolute) ret.dir = "/";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file URL to a path string.
|
||||
*
|
||||
* ```ts
|
||||
* import { fromFileUrl } from "https://deno.land/std@$STD_VERSION/path/posix.ts";
|
||||
* fromFileUrl("file:///home/foo"); // "/home/foo"
|
||||
* ```
|
||||
* @param url of a file URL
|
||||
*/
|
||||
export function fromFileUrl(url: string | URL): string {
|
||||
url = url instanceof URL ? url : new URL(url);
|
||||
if (url.protocol != "file:") {
|
||||
throw new TypeError("Must be a file URL.");
|
||||
}
|
||||
return decodeURIComponent(
|
||||
url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a path string to a file URL.
|
||||
*
|
||||
* ```ts
|
||||
* import { toFileUrl } from "https://deno.land/std@$STD_VERSION/path/posix.ts";
|
||||
* toFileUrl("/home/foo"); // new URL("file:///home/foo")
|
||||
* ```
|
||||
* @param path to convert to file URL
|
||||
*/
|
||||
export function toFileUrl(path: string): URL {
|
||||
if (!isAbsolute(path)) {
|
||||
throw new TypeError("Must be an absolute path.");
|
||||
}
|
||||
const url = new URL("file:///");
|
||||
url.pathname = encodeWhitespace(
|
||||
path.replace(/%/g, "%25").replace(/\\/g, "%5C"),
|
||||
);
|
||||
return url;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { isWindows } from "../_util/os.ts";
|
||||
|
||||
export const SEP = isWindows ? "\\" : "/";
|
||||
export const SEP_PATTERN = isWindows ? /[\\/]+/ : /\/+/;
|
||||
+962
@@ -0,0 +1,962 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// Copyright the Browserify authors. MIT License.
|
||||
// Ported from https://github.com/browserify/path-browserify/
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { FormatInputPathObject, ParsedPath } from "./_interface.ts";
|
||||
import {
|
||||
CHAR_BACKWARD_SLASH,
|
||||
CHAR_COLON,
|
||||
CHAR_DOT,
|
||||
CHAR_QUESTION_MARK,
|
||||
} from "./_constants.ts";
|
||||
|
||||
import {
|
||||
_format,
|
||||
assertPath,
|
||||
encodeWhitespace,
|
||||
isPathSeparator,
|
||||
isPosixPathSeparator,
|
||||
isWindowsDeviceRoot,
|
||||
lastPathSegment,
|
||||
normalizeString,
|
||||
stripSuffix,
|
||||
stripTrailingSeparators,
|
||||
} from "./_util.ts";
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
|
||||
export const sep = "\\";
|
||||
export const delimiter = ";";
|
||||
|
||||
/**
|
||||
* Resolves path segments into a `path`
|
||||
* @param pathSegments to process to path
|
||||
*/
|
||||
export function resolve(...pathSegments: string[]): string {
|
||||
let resolvedDevice = "";
|
||||
let resolvedTail = "";
|
||||
let resolvedAbsolute = false;
|
||||
|
||||
for (let i = pathSegments.length - 1; i >= -1; i--) {
|
||||
let path: string;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const { Deno } = globalThis as any;
|
||||
if (i >= 0) {
|
||||
path = pathSegments[i];
|
||||
} else if (!resolvedDevice) {
|
||||
if (typeof Deno?.cwd !== "function") {
|
||||
throw new TypeError("Resolved a drive-letter-less path without a CWD.");
|
||||
}
|
||||
path = Deno.cwd();
|
||||
} else {
|
||||
if (
|
||||
typeof Deno?.env?.get !== "function" || typeof Deno?.cwd !== "function"
|
||||
) {
|
||||
throw new TypeError("Resolved a relative path without a CWD.");
|
||||
}
|
||||
path = Deno.cwd();
|
||||
|
||||
// Verify that a cwd was found and that it actually points
|
||||
// to our drive. If not, default to the drive's root.
|
||||
if (
|
||||
path === undefined ||
|
||||
path.slice(0, 3).toLowerCase() !== `${resolvedDevice.toLowerCase()}\\`
|
||||
) {
|
||||
path = `${resolvedDevice}\\`;
|
||||
}
|
||||
}
|
||||
|
||||
assertPath(path);
|
||||
|
||||
const len = path.length;
|
||||
|
||||
// Skip empty entries
|
||||
if (len === 0) continue;
|
||||
|
||||
let rootEnd = 0;
|
||||
let device = "";
|
||||
let isAbsolute = false;
|
||||
const code = path.charCodeAt(0);
|
||||
|
||||
// Try to match a root
|
||||
if (len > 1) {
|
||||
if (isPathSeparator(code)) {
|
||||
// Possible UNC root
|
||||
|
||||
// If we started with a separator, we know we at least have an
|
||||
// absolute path of some kind (UNC or otherwise)
|
||||
isAbsolute = true;
|
||||
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// Matched double path separator at beginning
|
||||
let j = 2;
|
||||
let last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
const firstPart = path.slice(last, j);
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more path separators
|
||||
for (; j < len; ++j) {
|
||||
if (!isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j === len) {
|
||||
// We matched a UNC root only
|
||||
device = `\\\\${firstPart}\\${path.slice(last)}`;
|
||||
rootEnd = j;
|
||||
} else if (j !== last) {
|
||||
// We matched a UNC root with leftovers
|
||||
|
||||
device = `\\\\${firstPart}\\${path.slice(last, j)}`;
|
||||
rootEnd = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rootEnd = 1;
|
||||
}
|
||||
} else if (isWindowsDeviceRoot(code)) {
|
||||
// Possible device root
|
||||
|
||||
if (path.charCodeAt(1) === CHAR_COLON) {
|
||||
device = path.slice(0, 2);
|
||||
rootEnd = 2;
|
||||
if (len > 2) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) {
|
||||
// Treat separator following drive name as an absolute path
|
||||
// indicator
|
||||
isAbsolute = true;
|
||||
rootEnd = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPathSeparator(code)) {
|
||||
// `path` contains just a path separator
|
||||
rootEnd = 1;
|
||||
isAbsolute = true;
|
||||
}
|
||||
|
||||
if (
|
||||
device.length > 0 &&
|
||||
resolvedDevice.length > 0 &&
|
||||
device.toLowerCase() !== resolvedDevice.toLowerCase()
|
||||
) {
|
||||
// This path points to another device so it is not applicable
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resolvedDevice.length === 0 && device.length > 0) {
|
||||
resolvedDevice = device;
|
||||
}
|
||||
if (!resolvedAbsolute) {
|
||||
resolvedTail = `${path.slice(rootEnd)}\\${resolvedTail}`;
|
||||
resolvedAbsolute = isAbsolute;
|
||||
}
|
||||
|
||||
if (resolvedAbsolute && resolvedDevice.length > 0) break;
|
||||
}
|
||||
|
||||
// At this point the path should be resolved to a full absolute path,
|
||||
// but handle relative paths to be safe (might happen when process.cwd()
|
||||
// fails)
|
||||
|
||||
// Normalize the tail path
|
||||
resolvedTail = normalizeString(
|
||||
resolvedTail,
|
||||
!resolvedAbsolute,
|
||||
"\\",
|
||||
isPathSeparator,
|
||||
);
|
||||
|
||||
return resolvedDevice + (resolvedAbsolute ? "\\" : "") + resolvedTail || ".";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a `path`
|
||||
* @param path to normalize
|
||||
*/
|
||||
export function normalize(path: string): string {
|
||||
assertPath(path);
|
||||
const len = path.length;
|
||||
if (len === 0) return ".";
|
||||
let rootEnd = 0;
|
||||
let device: string | undefined;
|
||||
let isAbsolute = false;
|
||||
const code = path.charCodeAt(0);
|
||||
|
||||
// Try to match a root
|
||||
if (len > 1) {
|
||||
if (isPathSeparator(code)) {
|
||||
// Possible UNC root
|
||||
|
||||
// If we started with a separator, we know we at least have an absolute
|
||||
// path of some kind (UNC or otherwise)
|
||||
isAbsolute = true;
|
||||
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// Matched double path separator at beginning
|
||||
let j = 2;
|
||||
let last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
const firstPart = path.slice(last, j);
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more path separators
|
||||
for (; j < len; ++j) {
|
||||
if (!isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j === len) {
|
||||
// We matched a UNC root only
|
||||
// Return the normalized version of the UNC root since there
|
||||
// is nothing left to process
|
||||
|
||||
return `\\\\${firstPart}\\${path.slice(last)}\\`;
|
||||
} else if (j !== last) {
|
||||
// We matched a UNC root with leftovers
|
||||
|
||||
device = `\\\\${firstPart}\\${path.slice(last, j)}`;
|
||||
rootEnd = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rootEnd = 1;
|
||||
}
|
||||
} else if (isWindowsDeviceRoot(code)) {
|
||||
// Possible device root
|
||||
|
||||
if (path.charCodeAt(1) === CHAR_COLON) {
|
||||
device = path.slice(0, 2);
|
||||
rootEnd = 2;
|
||||
if (len > 2) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) {
|
||||
// Treat separator following drive name as an absolute path
|
||||
// indicator
|
||||
isAbsolute = true;
|
||||
rootEnd = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPathSeparator(code)) {
|
||||
// `path` contains just a path separator, exit early to avoid unnecessary
|
||||
// work
|
||||
return "\\";
|
||||
}
|
||||
|
||||
let tail: string;
|
||||
if (rootEnd < len) {
|
||||
tail = normalizeString(
|
||||
path.slice(rootEnd),
|
||||
!isAbsolute,
|
||||
"\\",
|
||||
isPathSeparator,
|
||||
);
|
||||
} else {
|
||||
tail = "";
|
||||
}
|
||||
if (tail.length === 0 && !isAbsolute) tail = ".";
|
||||
if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) {
|
||||
tail += "\\";
|
||||
}
|
||||
if (device === undefined) {
|
||||
if (isAbsolute) {
|
||||
if (tail.length > 0) return `\\${tail}`;
|
||||
else return "\\";
|
||||
} else if (tail.length > 0) {
|
||||
return tail;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} else if (isAbsolute) {
|
||||
if (tail.length > 0) return `${device}\\${tail}`;
|
||||
else return `${device}\\`;
|
||||
} else if (tail.length > 0) {
|
||||
return device + tail;
|
||||
} else {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether path is absolute
|
||||
* @param path to verify
|
||||
*/
|
||||
export function isAbsolute(path: string): boolean {
|
||||
assertPath(path);
|
||||
const len = path.length;
|
||||
if (len === 0) return false;
|
||||
|
||||
const code = path.charCodeAt(0);
|
||||
if (isPathSeparator(code)) {
|
||||
return true;
|
||||
} else if (isWindowsDeviceRoot(code)) {
|
||||
// Possible device root
|
||||
|
||||
if (len > 2 && path.charCodeAt(1) === CHAR_COLON) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join all given a sequence of `paths`,then normalizes the resulting path.
|
||||
* @param paths to be joined and normalized
|
||||
*/
|
||||
export function join(...paths: string[]): string {
|
||||
const pathsCount = paths.length;
|
||||
if (pathsCount === 0) return ".";
|
||||
|
||||
let joined: string | undefined;
|
||||
let firstPart: string | null = null;
|
||||
for (let i = 0; i < pathsCount; ++i) {
|
||||
const path = paths[i];
|
||||
assertPath(path);
|
||||
if (path.length > 0) {
|
||||
if (joined === undefined) joined = firstPart = path;
|
||||
else joined += `\\${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (joined === undefined) return ".";
|
||||
|
||||
// Make sure that the joined path doesn't start with two slashes, because
|
||||
// normalize() will mistake it for an UNC path then.
|
||||
//
|
||||
// This step is skipped when it is very clear that the user actually
|
||||
// intended to point at an UNC path. This is assumed when the first
|
||||
// non-empty string arguments starts with exactly two slashes followed by
|
||||
// at least one more non-slash character.
|
||||
//
|
||||
// Note that for normalize() to treat a path as an UNC path it needs to
|
||||
// have at least 2 components, so we don't filter for that here.
|
||||
// This means that the user can use join to construct UNC paths from
|
||||
// a server name and a share name; for example:
|
||||
// path.join('//server', 'share') -> '\\\\server\\share\\')
|
||||
let needsReplace = true;
|
||||
let slashCount = 0;
|
||||
assert(firstPart != null);
|
||||
if (isPathSeparator(firstPart.charCodeAt(0))) {
|
||||
++slashCount;
|
||||
const firstLen = firstPart.length;
|
||||
if (firstLen > 1) {
|
||||
if (isPathSeparator(firstPart.charCodeAt(1))) {
|
||||
++slashCount;
|
||||
if (firstLen > 2) {
|
||||
if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount;
|
||||
else {
|
||||
// We matched a UNC path in the first part
|
||||
needsReplace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needsReplace) {
|
||||
// Find any more consecutive slashes we need to replace
|
||||
for (; slashCount < joined.length; ++slashCount) {
|
||||
if (!isPathSeparator(joined.charCodeAt(slashCount))) break;
|
||||
}
|
||||
|
||||
// Replace the slashes if needed
|
||||
if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`;
|
||||
}
|
||||
|
||||
return normalize(joined);
|
||||
}
|
||||
|
||||
/**
|
||||
* It will solve the relative path from `from` to `to`, for instance:
|
||||
* from = 'C:\\orandea\\test\\aaa'
|
||||
* to = 'C:\\orandea\\impl\\bbb'
|
||||
* The output of the function should be: '..\\..\\impl\\bbb'
|
||||
* @param from relative path
|
||||
* @param to relative path
|
||||
*/
|
||||
export function relative(from: string, to: string): string {
|
||||
assertPath(from);
|
||||
assertPath(to);
|
||||
|
||||
if (from === to) return "";
|
||||
|
||||
const fromOrig = resolve(from);
|
||||
const toOrig = resolve(to);
|
||||
|
||||
if (fromOrig === toOrig) return "";
|
||||
|
||||
from = fromOrig.toLowerCase();
|
||||
to = toOrig.toLowerCase();
|
||||
|
||||
if (from === to) return "";
|
||||
|
||||
// Trim any leading backslashes
|
||||
let fromStart = 0;
|
||||
let fromEnd = from.length;
|
||||
for (; fromStart < fromEnd; ++fromStart) {
|
||||
if (from.charCodeAt(fromStart) !== CHAR_BACKWARD_SLASH) break;
|
||||
}
|
||||
// Trim trailing backslashes (applicable to UNC paths only)
|
||||
for (; fromEnd - 1 > fromStart; --fromEnd) {
|
||||
if (from.charCodeAt(fromEnd - 1) !== CHAR_BACKWARD_SLASH) break;
|
||||
}
|
||||
const fromLen = fromEnd - fromStart;
|
||||
|
||||
// Trim any leading backslashes
|
||||
let toStart = 0;
|
||||
let toEnd = to.length;
|
||||
for (; toStart < toEnd; ++toStart) {
|
||||
if (to.charCodeAt(toStart) !== CHAR_BACKWARD_SLASH) break;
|
||||
}
|
||||
// Trim trailing backslashes (applicable to UNC paths only)
|
||||
for (; toEnd - 1 > toStart; --toEnd) {
|
||||
if (to.charCodeAt(toEnd - 1) !== CHAR_BACKWARD_SLASH) break;
|
||||
}
|
||||
const toLen = toEnd - toStart;
|
||||
|
||||
// Compare paths to find the longest common path from root
|
||||
const length = fromLen < toLen ? fromLen : toLen;
|
||||
let lastCommonSep = -1;
|
||||
let i = 0;
|
||||
for (; i <= length; ++i) {
|
||||
if (i === length) {
|
||||
if (toLen > length) {
|
||||
if (to.charCodeAt(toStart + i) === CHAR_BACKWARD_SLASH) {
|
||||
// We get here if `from` is the exact base path for `to`.
|
||||
// For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz'
|
||||
return toOrig.slice(toStart + i + 1);
|
||||
} else if (i === 2) {
|
||||
// We get here if `from` is the device root.
|
||||
// For example: from='C:\\'; to='C:\\foo'
|
||||
return toOrig.slice(toStart + i);
|
||||
}
|
||||
}
|
||||
if (fromLen > length) {
|
||||
if (from.charCodeAt(fromStart + i) === CHAR_BACKWARD_SLASH) {
|
||||
// We get here if `to` is the exact base path for `from`.
|
||||
// For example: from='C:\\foo\\bar'; to='C:\\foo'
|
||||
lastCommonSep = i;
|
||||
} else if (i === 2) {
|
||||
// We get here if `to` is the device root.
|
||||
// For example: from='C:\\foo\\bar'; to='C:\\'
|
||||
lastCommonSep = 3;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
const fromCode = from.charCodeAt(fromStart + i);
|
||||
const toCode = to.charCodeAt(toStart + i);
|
||||
if (fromCode !== toCode) break;
|
||||
else if (fromCode === CHAR_BACKWARD_SLASH) lastCommonSep = i;
|
||||
}
|
||||
|
||||
// We found a mismatch before the first common path separator was seen, so
|
||||
// return the original `to`.
|
||||
if (i !== length && lastCommonSep === -1) {
|
||||
return toOrig;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
if (lastCommonSep === -1) lastCommonSep = 0;
|
||||
// Generate the relative path based on the path difference between `to` and
|
||||
// `from`
|
||||
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
|
||||
if (i === fromEnd || from.charCodeAt(i) === CHAR_BACKWARD_SLASH) {
|
||||
if (out.length === 0) out += "..";
|
||||
else out += "\\..";
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, append the rest of the destination (`to`) path that comes after
|
||||
// the common path parts
|
||||
if (out.length > 0) {
|
||||
return out + toOrig.slice(toStart + lastCommonSep, toEnd);
|
||||
} else {
|
||||
toStart += lastCommonSep;
|
||||
if (toOrig.charCodeAt(toStart) === CHAR_BACKWARD_SLASH) ++toStart;
|
||||
return toOrig.slice(toStart, toEnd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves path to a namespace path
|
||||
* @param path to resolve to namespace
|
||||
*/
|
||||
export function toNamespacedPath(path: string): string {
|
||||
// Note: this will *probably* throw somewhere.
|
||||
if (typeof path !== "string") return path;
|
||||
if (path.length === 0) return "";
|
||||
|
||||
const resolvedPath = resolve(path);
|
||||
|
||||
if (resolvedPath.length >= 3) {
|
||||
if (resolvedPath.charCodeAt(0) === CHAR_BACKWARD_SLASH) {
|
||||
// Possible UNC root
|
||||
|
||||
if (resolvedPath.charCodeAt(1) === CHAR_BACKWARD_SLASH) {
|
||||
const code = resolvedPath.charCodeAt(2);
|
||||
if (code !== CHAR_QUESTION_MARK && code !== CHAR_DOT) {
|
||||
// Matched non-long UNC root, convert the path to a long UNC path
|
||||
return `\\\\?\\UNC\\${resolvedPath.slice(2)}`;
|
||||
}
|
||||
}
|
||||
} else if (isWindowsDeviceRoot(resolvedPath.charCodeAt(0))) {
|
||||
// Possible device root
|
||||
|
||||
if (
|
||||
resolvedPath.charCodeAt(1) === CHAR_COLON &&
|
||||
resolvedPath.charCodeAt(2) === CHAR_BACKWARD_SLASH
|
||||
) {
|
||||
// Matched device root, convert the path to a long UNC path
|
||||
return `\\\\?\\${resolvedPath}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the directory path of a `path`.
|
||||
* @param path - path to extract the directory from.
|
||||
*/
|
||||
export function dirname(path: string): string {
|
||||
assertPath(path);
|
||||
const len = path.length;
|
||||
if (len === 0) return ".";
|
||||
let rootEnd = -1;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
let offset = 0;
|
||||
const code = path.charCodeAt(0);
|
||||
|
||||
// Try to match a root
|
||||
if (len > 1) {
|
||||
if (isPathSeparator(code)) {
|
||||
// Possible UNC root
|
||||
|
||||
rootEnd = offset = 1;
|
||||
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// Matched double path separator at beginning
|
||||
let j = 2;
|
||||
let last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more path separators
|
||||
for (; j < len; ++j) {
|
||||
if (!isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j === len) {
|
||||
// We matched a UNC root only
|
||||
return path;
|
||||
}
|
||||
if (j !== last) {
|
||||
// We matched a UNC root with leftovers
|
||||
|
||||
// Offset by 1 to include the separator after the UNC root to
|
||||
// treat it as a "normal root" on top of a (UNC) root
|
||||
rootEnd = offset = j + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isWindowsDeviceRoot(code)) {
|
||||
// Possible device root
|
||||
|
||||
if (path.charCodeAt(1) === CHAR_COLON) {
|
||||
rootEnd = offset = 2;
|
||||
if (len > 2) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPathSeparator(code)) {
|
||||
// `path` contains just a path separator, exit early to avoid
|
||||
// unnecessary work
|
||||
return path;
|
||||
}
|
||||
|
||||
for (let i = len - 1; i >= offset; --i) {
|
||||
if (isPathSeparator(path.charCodeAt(i))) {
|
||||
if (!matchedSlash) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// We saw the first non-path separator
|
||||
matchedSlash = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (end === -1) {
|
||||
if (rootEnd === -1) return ".";
|
||||
else end = rootEnd;
|
||||
}
|
||||
return stripTrailingSeparators(path.slice(0, end), isPosixPathSeparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last portion of a `path`.
|
||||
* Trailing directory separators are ignored, and optional suffix is removed.
|
||||
*
|
||||
* @param path - path to extract name from.
|
||||
* @param [suffix] - suffix to remove from extracted name.
|
||||
*/
|
||||
export function basename(path: string, suffix = ""): string {
|
||||
assertPath(path);
|
||||
|
||||
if (path.length === 0) return path;
|
||||
|
||||
if (typeof suffix !== "string") {
|
||||
throw new TypeError(
|
||||
`Suffix must be a string. Received ${JSON.stringify(suffix)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for a drive letter prefix so as not to mistake the following
|
||||
// path separator as an extra separator at the end of the path that can be
|
||||
// disregarded
|
||||
let start = 0;
|
||||
if (path.length >= 2) {
|
||||
const drive = path.charCodeAt(0);
|
||||
if (isWindowsDeviceRoot(drive)) {
|
||||
if (path.charCodeAt(1) === CHAR_COLON) start = 2;
|
||||
}
|
||||
}
|
||||
|
||||
const lastSegment = lastPathSegment(path, isPathSeparator, start);
|
||||
const strippedSegment = stripTrailingSeparators(lastSegment, isPathSeparator);
|
||||
return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the extension of the `path` with leading period.
|
||||
* @param path with extension
|
||||
* @returns extension (ex. for `file.ts` returns `.ts`)
|
||||
*/
|
||||
export function extname(path: string): string {
|
||||
assertPath(path);
|
||||
let start = 0;
|
||||
let startDot = -1;
|
||||
let startPart = 0;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
|
||||
// Check for a drive letter prefix so as not to mistake the following
|
||||
// path separator as an extra separator at the end of the path that can be
|
||||
// disregarded
|
||||
|
||||
if (
|
||||
path.length >= 2 &&
|
||||
path.charCodeAt(1) === CHAR_COLON &&
|
||||
isWindowsDeviceRoot(path.charCodeAt(0))
|
||||
) {
|
||||
start = startPart = 2;
|
||||
}
|
||||
|
||||
for (let i = path.length - 1; i >= start; --i) {
|
||||
const code = path.charCodeAt(i);
|
||||
if (isPathSeparator(code)) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CHAR_DOT) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) startDot = i;
|
||||
else if (preDotState !== 1) preDotState = 1;
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
return path.slice(startDot, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a path from `FormatInputPathObject` object.
|
||||
* @param pathObject with path
|
||||
*/
|
||||
export function format(pathObject: FormatInputPathObject): string {
|
||||
if (pathObject === null || typeof pathObject !== "object") {
|
||||
throw new TypeError(
|
||||
`The "pathObject" argument must be of type Object. Received type ${typeof pathObject}`,
|
||||
);
|
||||
}
|
||||
return _format("\\", pathObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a `ParsedPath` object of the `path`.
|
||||
* @param path to process
|
||||
*/
|
||||
export function parse(path: string): ParsedPath {
|
||||
assertPath(path);
|
||||
|
||||
const ret: ParsedPath = { root: "", dir: "", base: "", ext: "", name: "" };
|
||||
|
||||
const len = path.length;
|
||||
if (len === 0) return ret;
|
||||
|
||||
let rootEnd = 0;
|
||||
let code = path.charCodeAt(0);
|
||||
|
||||
// Try to match a root
|
||||
if (len > 1) {
|
||||
if (isPathSeparator(code)) {
|
||||
// Possible UNC root
|
||||
|
||||
rootEnd = 1;
|
||||
if (isPathSeparator(path.charCodeAt(1))) {
|
||||
// Matched double path separator at beginning
|
||||
let j = 2;
|
||||
let last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more path separators
|
||||
for (; j < len; ++j) {
|
||||
if (!isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j < len && j !== last) {
|
||||
// Matched!
|
||||
last = j;
|
||||
// Match 1 or more non-path separators
|
||||
for (; j < len; ++j) {
|
||||
if (isPathSeparator(path.charCodeAt(j))) break;
|
||||
}
|
||||
if (j === len) {
|
||||
// We matched a UNC root only
|
||||
|
||||
rootEnd = j;
|
||||
} else if (j !== last) {
|
||||
// We matched a UNC root with leftovers
|
||||
|
||||
rootEnd = j + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isWindowsDeviceRoot(code)) {
|
||||
// Possible device root
|
||||
|
||||
if (path.charCodeAt(1) === CHAR_COLON) {
|
||||
rootEnd = 2;
|
||||
if (len > 2) {
|
||||
if (isPathSeparator(path.charCodeAt(2))) {
|
||||
if (len === 3) {
|
||||
// `path` contains just a drive root, exit early to avoid
|
||||
// unnecessary work
|
||||
ret.root = ret.dir = path;
|
||||
ret.base = "\\";
|
||||
return ret;
|
||||
}
|
||||
rootEnd = 3;
|
||||
}
|
||||
} else {
|
||||
// `path` contains just a relative drive root, exit early to avoid
|
||||
// unnecessary work
|
||||
ret.root = ret.dir = path;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPathSeparator(code)) {
|
||||
// `path` contains just a path separator, exit early to avoid
|
||||
// unnecessary work
|
||||
ret.root = ret.dir = path;
|
||||
ret.base = "\\";
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (rootEnd > 0) ret.root = path.slice(0, rootEnd);
|
||||
|
||||
let startDot = -1;
|
||||
let startPart = rootEnd;
|
||||
let end = -1;
|
||||
let matchedSlash = true;
|
||||
let i = path.length - 1;
|
||||
|
||||
// Track the state of characters (if any) we see before our first dot and
|
||||
// after any path separator we find
|
||||
let preDotState = 0;
|
||||
|
||||
// Get non-dir info
|
||||
for (; i >= rootEnd; --i) {
|
||||
code = path.charCodeAt(i);
|
||||
if (isPathSeparator(code)) {
|
||||
// If we reached a path separator that was not part of a set of path
|
||||
// separators at the end of the string, stop now
|
||||
if (!matchedSlash) {
|
||||
startPart = i + 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (end === -1) {
|
||||
// We saw the first non-path separator, mark this as the end of our
|
||||
// extension
|
||||
matchedSlash = false;
|
||||
end = i + 1;
|
||||
}
|
||||
if (code === CHAR_DOT) {
|
||||
// If this is our first dot, mark it as the start of our extension
|
||||
if (startDot === -1) startDot = i;
|
||||
else if (preDotState !== 1) preDotState = 1;
|
||||
} else if (startDot !== -1) {
|
||||
// We saw a non-dot and non-path separator before our dot, so we should
|
||||
// have a good chance at having a non-empty extension
|
||||
preDotState = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
startDot === -1 ||
|
||||
end === -1 ||
|
||||
// We saw a non-dot character immediately before the dot
|
||||
preDotState === 0 ||
|
||||
// The (right-most) trimmed path component is exactly '..'
|
||||
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
||||
) {
|
||||
if (end !== -1) {
|
||||
ret.base = ret.name = path.slice(startPart, end);
|
||||
}
|
||||
} else {
|
||||
ret.name = path.slice(startPart, startDot);
|
||||
ret.base = path.slice(startPart, end);
|
||||
ret.ext = path.slice(startDot, end);
|
||||
}
|
||||
|
||||
// Fallback to '\' in case there is no basename
|
||||
ret.base = ret.base || "\\";
|
||||
|
||||
// If the directory is the root, use the entire root as the `dir` including
|
||||
// the trailing slash if any (`C:\abc` -> `C:\`). Otherwise, strip out the
|
||||
// trailing slash (`C:\abc\def` -> `C:\abc`).
|
||||
if (startPart > 0 && startPart !== rootEnd) {
|
||||
ret.dir = path.slice(0, startPart - 1);
|
||||
} else ret.dir = ret.root;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file URL to a path string.
|
||||
*
|
||||
* ```ts
|
||||
* import { fromFileUrl } from "https://deno.land/std@$STD_VERSION/path/win32.ts";
|
||||
* fromFileUrl("file:///home/foo"); // "\\home\\foo"
|
||||
* fromFileUrl("file:///C:/Users/foo"); // "C:\\Users\\foo"
|
||||
* fromFileUrl("file://localhost/home/foo"); // "\\\\localhost\\home\\foo"
|
||||
* ```
|
||||
* @param url of a file URL
|
||||
*/
|
||||
export function fromFileUrl(url: string | URL): string {
|
||||
url = url instanceof URL ? url : new URL(url);
|
||||
if (url.protocol != "file:") {
|
||||
throw new TypeError("Must be a file URL.");
|
||||
}
|
||||
let path = decodeURIComponent(
|
||||
url.pathname.replace(/\//g, "\\").replace(/%(?![0-9A-Fa-f]{2})/g, "%25"),
|
||||
).replace(/^\\*([A-Za-z]:)(\\|$)/, "$1\\");
|
||||
if (url.hostname != "") {
|
||||
// Note: The `URL` implementation guarantees that the drive letter and
|
||||
// hostname are mutually exclusive. Otherwise it would not have been valid
|
||||
// to append the hostname and path like this.
|
||||
path = `\\\\${url.hostname}${path}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a path string to a file URL.
|
||||
*
|
||||
* ```ts
|
||||
* import { toFileUrl } from "https://deno.land/std@$STD_VERSION/path/win32.ts";
|
||||
* toFileUrl("\\home\\foo"); // new URL("file:///home/foo")
|
||||
* toFileUrl("C:\\Users\\foo"); // new URL("file:///C:/Users/foo")
|
||||
* toFileUrl("\\\\127.0.0.1\\home\\foo"); // new URL("file://127.0.0.1/home/foo")
|
||||
* ```
|
||||
* @param path to convert to file URL
|
||||
*/
|
||||
export function toFileUrl(path: string): URL {
|
||||
if (!isAbsolute(path)) {
|
||||
throw new TypeError("Must be an absolute path.");
|
||||
}
|
||||
const [, hostname, pathname] = path.match(
|
||||
/^(?:[/\\]{2}([^/\\]+)(?=[/\\](?:[^/\\]|$)))?(.*)/,
|
||||
)!;
|
||||
const url = new URL("file:///");
|
||||
url.pathname = encodeWhitespace(pathname.replace(/%/g, "%25"));
|
||||
if (hostname != null && hostname != "localhost") {
|
||||
url.hostname = hostname;
|
||||
if (!url.hostname) {
|
||||
throw new TypeError("Invalid hostname.");
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
export const DEFAULT_CHUNK_SIZE = 16_640;
|
||||
export const DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
/** Generate longest proper prefix which is also suffix array. */
|
||||
export function createLPS(pat: Uint8Array): Uint8Array {
|
||||
const lps = new Uint8Array(pat.length);
|
||||
lps[0] = 0;
|
||||
let prefixEnd = 0;
|
||||
let i = 1;
|
||||
while (i < lps.length) {
|
||||
if (pat[i] == pat[prefixEnd]) {
|
||||
prefixEnd++;
|
||||
lps[i] = prefixEnd;
|
||||
i++;
|
||||
} else if (prefixEnd === 0) {
|
||||
lps[i] = 0;
|
||||
i++;
|
||||
} else {
|
||||
prefixEnd = lps[prefixEnd - 1];
|
||||
}
|
||||
}
|
||||
return lps;
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
import { copy } from "../bytes/copy.ts";
|
||||
|
||||
const MAX_SIZE = 2 ** 32 - 2;
|
||||
const DEFAULT_CHUNK_SIZE = 16_640;
|
||||
|
||||
/** A variable-sized buffer of bytes with `read()` and `write()` methods.
|
||||
*
|
||||
* Buffer is almost always used with some I/O like files and sockets. It allows
|
||||
* one to buffer up a download from a socket. Buffer grows and shrinks as
|
||||
* necessary.
|
||||
*
|
||||
* Buffer is NOT the same thing as Node's Buffer. Node's Buffer was created in
|
||||
* 2009 before JavaScript had the concept of ArrayBuffers. It's simply a
|
||||
* non-standard ArrayBuffer.
|
||||
*
|
||||
* ArrayBuffer is a fixed memory allocation. Buffer is implemented on top of
|
||||
* ArrayBuffer.
|
||||
*
|
||||
* Based on [Go Buffer](https://golang.org/pkg/bytes/#Buffer). */
|
||||
export class Buffer {
|
||||
#buf: Uint8Array; // contents are the bytes buf[off : len(buf)]
|
||||
#off = 0; // read at buf[off], write at buf[buf.byteLength]
|
||||
#readable: ReadableStream<Uint8Array> = new ReadableStream({
|
||||
type: "bytes",
|
||||
pull: (controller) => {
|
||||
const view = new Uint8Array(controller.byobRequest!.view!.buffer);
|
||||
if (this.empty()) {
|
||||
// Buffer is empty, reset to recover space.
|
||||
this.reset();
|
||||
controller.close();
|
||||
controller.byobRequest!.respond(0);
|
||||
return;
|
||||
}
|
||||
const nread = copy(this.#buf.subarray(this.#off), view);
|
||||
this.#off += nread;
|
||||
controller.byobRequest!.respond(nread);
|
||||
},
|
||||
autoAllocateChunkSize: DEFAULT_CHUNK_SIZE,
|
||||
});
|
||||
get readable() {
|
||||
return this.#readable;
|
||||
}
|
||||
#writable = new WritableStream<Uint8Array>({
|
||||
write: (chunk) => {
|
||||
const m = this.#grow(chunk.byteLength);
|
||||
copy(chunk, this.#buf, m);
|
||||
},
|
||||
});
|
||||
get writable() {
|
||||
return this.#writable;
|
||||
}
|
||||
|
||||
constructor(ab?: ArrayBufferLike | ArrayLike<number>) {
|
||||
this.#buf = ab === undefined ? new Uint8Array(0) : new Uint8Array(ab);
|
||||
}
|
||||
|
||||
/** Returns a slice holding the unread portion of the buffer.
|
||||
*
|
||||
* The slice is valid for use only until the next buffer modification (that
|
||||
* is, only until the next call to a method like `read()`, `write()`,
|
||||
* `reset()`, or `truncate()`). If `options.copy` is false the slice aliases
|
||||
* the buffer content at least until the next buffer modification, so
|
||||
* immediate changes to the slice will affect the result of future reads.
|
||||
*/
|
||||
bytes(options = { copy: true }): Uint8Array {
|
||||
if (options.copy === false) return this.#buf.subarray(this.#off);
|
||||
return this.#buf.slice(this.#off);
|
||||
}
|
||||
|
||||
/** Returns whether the unread portion of the buffer is empty. */
|
||||
empty(): boolean {
|
||||
return this.#buf.byteLength <= this.#off;
|
||||
}
|
||||
|
||||
/** A read only number of bytes of the unread portion of the buffer. */
|
||||
get length(): number {
|
||||
return this.#buf.byteLength - this.#off;
|
||||
}
|
||||
|
||||
/** The read only capacity of the buffer's underlying byte slice, that is,
|
||||
* the total space allocated for the buffer's data. */
|
||||
get capacity(): number {
|
||||
return this.#buf.buffer.byteLength;
|
||||
}
|
||||
|
||||
/** Discards all but the first `n` unread bytes from the buffer but
|
||||
* continues to use the same allocated storage. It throws if `n` is
|
||||
* negative or greater than the length of the buffer. */
|
||||
truncate(n: number) {
|
||||
if (n === 0) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
if (n < 0 || n > this.length) {
|
||||
throw Error("bytes.Buffer: truncation out of range");
|
||||
}
|
||||
this.#reslice(this.#off + n);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#reslice(0);
|
||||
this.#off = 0;
|
||||
}
|
||||
|
||||
#tryGrowByReslice(n: number) {
|
||||
const l = this.#buf.byteLength;
|
||||
if (n <= this.capacity - l) {
|
||||
this.#reslice(l + n);
|
||||
return l;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#reslice(len: number) {
|
||||
assert(len <= this.#buf.buffer.byteLength);
|
||||
this.#buf = new Uint8Array(this.#buf.buffer, 0, len);
|
||||
}
|
||||
|
||||
#grow(n: number) {
|
||||
const m = this.length;
|
||||
// If buffer is empty, reset to recover space.
|
||||
if (m === 0 && this.#off !== 0) {
|
||||
this.reset();
|
||||
}
|
||||
// Fast: Try to grow by means of a reslice.
|
||||
const i = this.#tryGrowByReslice(n);
|
||||
if (i >= 0) {
|
||||
return i;
|
||||
}
|
||||
const c = this.capacity;
|
||||
if (n <= Math.floor(c / 2) - m) {
|
||||
// We can slide things down instead of allocating a new
|
||||
// ArrayBuffer. We only need m+n <= c to slide, but
|
||||
// we instead let capacity get twice as large so we
|
||||
// don't spend all our time copying.
|
||||
copy(this.#buf.subarray(this.#off), this.#buf);
|
||||
} else if (c + n > MAX_SIZE) {
|
||||
throw new Error("The buffer cannot be grown beyond the maximum size.");
|
||||
} else {
|
||||
// Not enough space anywhere, we need to allocate.
|
||||
const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
|
||||
copy(this.#buf.subarray(this.#off), buf);
|
||||
this.#buf = buf;
|
||||
}
|
||||
// Restore this.#off and len(this.#buf).
|
||||
this.#off = 0;
|
||||
this.#reslice(Math.min(m + n, MAX_SIZE));
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Grows the buffer's capacity, if necessary, to guarantee space for
|
||||
* another `n` bytes. After `.grow(n)`, at least `n` bytes can be written to
|
||||
* the buffer without another allocation. If `n` is negative, `.grow()` will
|
||||
* throw. If the buffer can't grow it will throw an error.
|
||||
*
|
||||
* Based on Go Lang's
|
||||
* [Buffer.Grow](https://golang.org/pkg/bytes/#Buffer.Grow). */
|
||||
grow(n: number) {
|
||||
if (n < 0) {
|
||||
throw Error("Buffer.grow: negative count");
|
||||
}
|
||||
const m = this.#grow(n);
|
||||
this.#reslice(m);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { assert } from "../_util/asserts.ts";
|
||||
|
||||
/**
|
||||
* A transform stream that only transforms from the zero-indexed `start` and `end` bytes (both inclusive).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { ByteSliceStream } from "https://deno.land/std@$STD_VERSION/streams/byte_slice_stream.ts";
|
||||
* const response = await fetch("https://example.com");
|
||||
* const rangedStream = response.body!
|
||||
* .pipeThrough(new ByteSliceStream(3, 8));
|
||||
* ```
|
||||
*/
|
||||
export class ByteSliceStream extends TransformStream<Uint8Array, Uint8Array> {
|
||||
#offsetStart = 0;
|
||||
#offsetEnd = 0;
|
||||
|
||||
constructor(start = 0, end = Infinity) {
|
||||
super({
|
||||
start: () => {
|
||||
assert(start >= 0, "`start` must be greater than 0");
|
||||
end += 1;
|
||||
},
|
||||
transform: (chunk, controller) => {
|
||||
this.#offsetStart = this.#offsetEnd;
|
||||
this.#offsetEnd += chunk.byteLength;
|
||||
if (this.#offsetEnd > start) {
|
||||
if (this.#offsetStart < start) {
|
||||
chunk = chunk.slice(start - this.#offsetStart);
|
||||
}
|
||||
if (this.#offsetEnd >= end) {
|
||||
chunk = chunk.slice(0, chunk.byteLength - this.#offsetEnd + end);
|
||||
controller.enqueue(chunk);
|
||||
controller.terminate();
|
||||
} else {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { DEFAULT_BUFFER_SIZE } from "./_common.ts";
|
||||
import type { Reader, Writer } from "../types.d.ts";
|
||||
|
||||
/** Copies from `src` to `dst` until either EOF (`null`) is read from `src` or
|
||||
* an error occurs. It resolves to the number of bytes copied or rejects with
|
||||
* the first error encountered while copying.
|
||||
*
|
||||
* ```ts
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/streams/copy.ts";
|
||||
*
|
||||
* const source = await Deno.open("my_file.txt");
|
||||
* const bytesCopied1 = await copy(source, Deno.stdout);
|
||||
* const destination = await Deno.create("my_file_2.txt");
|
||||
* const bytesCopied2 = await copy(source, destination);
|
||||
* ```
|
||||
*
|
||||
* @param src The source to copy from
|
||||
* @param dst The destination to copy to
|
||||
* @param options Can be used to tune size of the buffer. Default size is 32kB
|
||||
*/
|
||||
export async function copy(
|
||||
src: Reader,
|
||||
dst: Writer,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): Promise<number> {
|
||||
let n = 0;
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
let gotEOF = false;
|
||||
while (gotEOF === false) {
|
||||
const result = await src.read(b);
|
||||
if (result === null) {
|
||||
gotEOF = true;
|
||||
} else {
|
||||
let nwritten = 0;
|
||||
while (nwritten < result) {
|
||||
nwritten += await dst.write(b.subarray(nwritten, result));
|
||||
}
|
||||
n += nwritten;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { BytesList } from "../bytes/bytes_list.ts";
|
||||
import { createLPS } from "./_common.ts";
|
||||
|
||||
/** Disposition of the delimiter. */
|
||||
export type DelimiterDisposition =
|
||||
/** Include delimiter in the found chunk. */
|
||||
| "suffix"
|
||||
/** Include delimiter in the subsequent chunk. */
|
||||
| "prefix"
|
||||
/** Discard the delimiter. */
|
||||
| "discard" // delimiter discarded
|
||||
;
|
||||
|
||||
export interface DelimiterStreamOptions {
|
||||
/** Disposition of the delimiter. */
|
||||
disposition?: DelimiterDisposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide a stream into chunks delimited by a given byte sequence.
|
||||
*
|
||||
* @example
|
||||
* Divide a CSV stream by commas, discarding the commas:
|
||||
* ```ts
|
||||
* import { DelimiterStream } from "https://deno.land/std@$STD_VERSION/streams/delimiter_stream.ts";
|
||||
* const res = await fetch("https://example.com/data.csv");
|
||||
* const parts = res.body!
|
||||
* .pipeThrough(new DelimiterStream(new TextEncoder().encode(",")))
|
||||
* .pipeThrough(new TextDecoderStream());
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Divide a stream after semi-colons, keeping the semi-colons in the output:
|
||||
* ```ts
|
||||
* import { DelimiterStream } from "https://deno.land/std@$STD_VERSION/streams/delimiter_stream.ts";
|
||||
* const res = await fetch("https://example.com/file.js");
|
||||
* const parts = res.body!
|
||||
* .pipeThrough(
|
||||
* new DelimiterStream(
|
||||
* new TextEncoder().encode(";"),
|
||||
* { disposition: "suffix" },
|
||||
* )
|
||||
* )
|
||||
* .pipeThrough(new TextDecoderStream());
|
||||
* ```
|
||||
*
|
||||
* @param delimiter Delimiter byte sequence
|
||||
* @param options Options for the transform stream
|
||||
* @returns Transform stream
|
||||
*/
|
||||
export class DelimiterStream extends TransformStream<Uint8Array, Uint8Array> {
|
||||
#bufs = new BytesList();
|
||||
#delimiter: Uint8Array;
|
||||
#inspectIndex = 0;
|
||||
#matchIndex = 0;
|
||||
#delimLen: number;
|
||||
#delimLPS: Uint8Array;
|
||||
#disp?: DelimiterDisposition;
|
||||
|
||||
constructor(
|
||||
delimiter: Uint8Array,
|
||||
options?: DelimiterStreamOptions,
|
||||
) {
|
||||
super({
|
||||
transform: (chunk, controller) => {
|
||||
this.#handle(chunk, controller);
|
||||
},
|
||||
flush: (controller) => {
|
||||
controller.enqueue(this.#bufs.concat());
|
||||
},
|
||||
});
|
||||
|
||||
this.#delimiter = delimiter;
|
||||
this.#delimLen = delimiter.length;
|
||||
this.#delimLPS = createLPS(delimiter);
|
||||
this.#disp = options?.disposition ?? "discard";
|
||||
}
|
||||
|
||||
#handle(
|
||||
chunk: Uint8Array,
|
||||
controller: TransformStreamDefaultController<Uint8Array>,
|
||||
) {
|
||||
this.#bufs.add(chunk);
|
||||
let localIndex = 0;
|
||||
while (this.#inspectIndex < this.#bufs.size()) {
|
||||
if (chunk[localIndex] === this.#delimiter[this.#matchIndex]) {
|
||||
this.#inspectIndex++;
|
||||
localIndex++;
|
||||
this.#matchIndex++;
|
||||
if (this.#matchIndex === this.#delimLen) {
|
||||
// Full match
|
||||
const start = this.#inspectIndex - this.#delimLen;
|
||||
const end = this.#disp == "suffix" ? this.#inspectIndex : start;
|
||||
const copy = this.#bufs.slice(0, end);
|
||||
controller.enqueue(copy);
|
||||
const shift = this.#disp == "prefix" ? start : this.#inspectIndex;
|
||||
this.#bufs.shift(shift);
|
||||
this.#inspectIndex = this.#disp == "prefix" ? this.#delimLen : 0;
|
||||
this.#matchIndex = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.#matchIndex === 0) {
|
||||
this.#inspectIndex++;
|
||||
localIndex++;
|
||||
} else {
|
||||
this.#matchIndex = this.#delimLPS[this.#matchIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Merge multiple streams into a single one, taking order into account, and each stream
|
||||
* will wait for a chunk to enqueue before the next stream can append another chunk.
|
||||
* If a stream ends before other ones, the others will be cancelled.
|
||||
*/
|
||||
export function earlyZipReadableStreams<T>(
|
||||
...streams: ReadableStream<T>[]
|
||||
): ReadableStream<T> {
|
||||
const readers = streams.map((s) => s.getReader());
|
||||
return new ReadableStream<T>({
|
||||
async start(controller) {
|
||||
try {
|
||||
loop:
|
||||
while (true) {
|
||||
for (const reader of readers) {
|
||||
const { value, done } = await reader.read();
|
||||
if (!done) {
|
||||
controller.enqueue(value!);
|
||||
} else {
|
||||
await Promise.all(readers.map((reader) => reader.cancel()));
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { DEFAULT_BUFFER_SIZE } from "./_common.ts";
|
||||
import type { Reader, ReaderSync } from "../types.d.ts";
|
||||
|
||||
/** Turns a Reader, `r`, into an async iterator.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReader } from "https://deno.land/std@$STD_VERSION/streams/iterate_reader.ts";
|
||||
*
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* for await (const chunk of iterateReader(f)) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Second argument can be used to tune size of a buffer.
|
||||
* Default size of the buffer is 32kB.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReader } from "https://deno.land/std@$STD_VERSION/streams/iterate_reader.ts";
|
||||
*
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* const it = iterateReader(f, {
|
||||
* bufSize: 1024 * 1024
|
||||
* });
|
||||
* for await (const chunk of it) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*/
|
||||
export async function* iterateReader(
|
||||
r: Reader,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): AsyncIterableIterator<Uint8Array> {
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
while (true) {
|
||||
const result = await r.read(b);
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield b.slice(0, result);
|
||||
}
|
||||
}
|
||||
|
||||
/** Turns a ReaderSync, `r`, into an iterator.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReaderSync } from "https://deno.land/std@$STD_VERSION/streams/iterate_reader.ts";
|
||||
*
|
||||
* let f = Deno.openSync("/etc/passwd");
|
||||
* for (const chunk of iterateReaderSync(f)) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Second argument can be used to tune size of a buffer.
|
||||
* Default size of the buffer is 32kB.
|
||||
*
|
||||
* ```ts
|
||||
* import { iterateReaderSync } from "https://deno.land/std@$STD_VERSION/streams/iterate_reader.ts";
|
||||
|
||||
* let f = await Deno.open("/etc/passwd");
|
||||
* const iter = iterateReaderSync(f, {
|
||||
* bufSize: 1024 * 1024
|
||||
* });
|
||||
* for (const chunk of iter) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* f.close();
|
||||
* ```
|
||||
*
|
||||
* Iterator uses an internal buffer of fixed size for efficiency; it returns
|
||||
* a view on that buffer on each iteration. It is therefore caller's
|
||||
* responsibility to copy contents of the buffer if needed; otherwise the
|
||||
* next iteration will overwrite contents of previously returned chunk.
|
||||
*/
|
||||
export function* iterateReaderSync(
|
||||
r: ReaderSync,
|
||||
options?: {
|
||||
bufSize?: number;
|
||||
},
|
||||
): IterableIterator<Uint8Array> {
|
||||
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||||
const b = new Uint8Array(bufSize);
|
||||
while (true) {
|
||||
const result = r.readSync(b);
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield b.slice(0, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** A TransformStream that will only read & enqueue `size` amount of bytes.
|
||||
* This operation is chunk based and not BYOB based,
|
||||
* and as such will read more than needed.
|
||||
*
|
||||
* if options.error is set, then instead of terminating the stream,
|
||||
* an error will be thrown.
|
||||
*
|
||||
* ```ts
|
||||
* import { LimitedBytesTransformStream } from "https://deno.land/std@$STD_VERSION/streams/limited_bytes_transform_stream.ts";
|
||||
* const res = await fetch("https://example.com");
|
||||
* const parts = res.body!
|
||||
* .pipeThrough(new LimitedBytesTransformStream(512 * 1024));
|
||||
* ```
|
||||
*/
|
||||
export class LimitedBytesTransformStream
|
||||
extends TransformStream<Uint8Array, Uint8Array> {
|
||||
#read = 0;
|
||||
constructor(size: number, options: { error?: boolean } = {}) {
|
||||
super({
|
||||
transform: (chunk, controller) => {
|
||||
if ((this.#read + chunk.byteLength) > size) {
|
||||
if (options.error) {
|
||||
throw new RangeError(`Exceeded byte size limit of '${size}'`);
|
||||
} else {
|
||||
controller.terminate();
|
||||
}
|
||||
} else {
|
||||
this.#read += chunk.byteLength;
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** A TransformStream that will only read & enqueue `size` amount of chunks.
|
||||
*
|
||||
* if options.error is set, then instead of terminating the stream,
|
||||
* an error will be thrown.
|
||||
*
|
||||
* ```ts
|
||||
* import { LimitedTransformStream } from "https://deno.land/std@$STD_VERSION/streams/limited_transform_stream.ts";
|
||||
* const res = await fetch("https://example.com");
|
||||
* const parts = res.body!.pipeThrough(new LimitedTransformStream(50));
|
||||
* ```
|
||||
*/
|
||||
export class LimitedTransformStream<T> extends TransformStream<T, T> {
|
||||
#read = 0;
|
||||
constructor(size: number, options: { error?: boolean } = {}) {
|
||||
super({
|
||||
transform: (chunk, controller) => {
|
||||
if ((this.#read + 1) > size) {
|
||||
if (options.error) {
|
||||
throw new RangeError(`Exceeded chunk limit of '${size}'`);
|
||||
} else {
|
||||
controller.terminate();
|
||||
}
|
||||
} else {
|
||||
this.#read++;
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
import { deferred } from "../async/deferred.ts";
|
||||
|
||||
/**
|
||||
* Merge multiple streams into a single one, not taking order into account.
|
||||
* If a stream ends before other ones, the other will continue adding data,
|
||||
* and the finished one will not add any more data.
|
||||
*/
|
||||
export function mergeReadableStreams<T>(
|
||||
...streams: ReadableStream<T>[]
|
||||
): ReadableStream<T> {
|
||||
const resolvePromises = streams.map(() => deferred<void>());
|
||||
return new ReadableStream<T>({
|
||||
start(controller) {
|
||||
Promise.all(resolvePromises).then(() => {
|
||||
controller.close();
|
||||
});
|
||||
try {
|
||||
for (const [key, stream] of Object.entries(streams)) {
|
||||
(async () => {
|
||||
for await (const data of stream) {
|
||||
controller.enqueue(data);
|
||||
}
|
||||
resolvePromises[+key].resolve();
|
||||
})();
|
||||
}
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
/**
|
||||
* Utilities for working with the
|
||||
* [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
|
||||
*
|
||||
* Includes buffering and conversion.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export * from "./buffer.ts";
|
||||
export * from "./byte_slice_stream.ts";
|
||||
export * from "./copy.ts";
|
||||
export * from "./delimiter_stream.ts";
|
||||
export * from "./early_zip_readable_streams.ts";
|
||||
export * from "./iterate_reader.ts";
|
||||
export * from "./limited_bytes_transform_stream.ts";
|
||||
export * from "./limited_transform_stream.ts";
|
||||
export * from "./merge_readable_streams.ts";
|
||||
export * from "./read_all.ts";
|
||||
export * from "./readable_stream_from_iterable.ts";
|
||||
export * from "./readable_stream_from_reader.ts";
|
||||
export * from "./reader_from_iterable.ts";
|
||||
export * from "./reader_from_stream_reader.ts";
|
||||
export * from "./text_delimiter_stream.ts";
|
||||
export * from "./text_line_stream.ts";
|
||||
export * from "./to_transform_stream.ts";
|
||||
export * from "./writable_stream_from_writer.ts";
|
||||
export * from "./write_all.ts";
|
||||
export * from "./writer_from_stream_writer.ts";
|
||||
export * from "./zip_readable_streams.ts";
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { Buffer } from "../io/buffer.ts";
|
||||
import type { Reader, ReaderSync } from "../types.d.ts";
|
||||
|
||||
/** Read Reader `r` until EOF (`null`) and resolve to the content as
|
||||
* Uint8Array`.
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "https://deno.land/std@$STD_VERSION/io/buffer.ts";
|
||||
* import { readAll } from "https://deno.land/std@$STD_VERSION/streams/read_all.ts";
|
||||
*
|
||||
* // Example from stdin
|
||||
* const stdinContent = await readAll(Deno.stdin);
|
||||
*
|
||||
* // Example from file
|
||||
* const file = await Deno.open("my_file.txt", {read: true});
|
||||
* const myFileContent = await readAll(file);
|
||||
* file.close();
|
||||
*
|
||||
* // Example from buffer
|
||||
* const myData = new Uint8Array(100);
|
||||
* // ... fill myData array with data
|
||||
* const reader = new Buffer(myData.buffer);
|
||||
* const bufferContent = await readAll(reader);
|
||||
* ```
|
||||
*/
|
||||
export async function readAll(r: Reader): Promise<Uint8Array> {
|
||||
const buf = new Buffer();
|
||||
await buf.readFrom(r);
|
||||
return buf.bytes();
|
||||
}
|
||||
|
||||
/** Synchronously reads Reader `r` until EOF (`null`) and returns the content
|
||||
* as `Uint8Array`.
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "https://deno.land/std@$STD_VERSION/io/buffer.ts";
|
||||
* import { readAllSync } from "https://deno.land/std@$STD_VERSION/streams/read_all.ts";
|
||||
*
|
||||
* // Example from stdin
|
||||
* const stdinContent = readAllSync(Deno.stdin);
|
||||
*
|
||||
* // Example from file
|
||||
* const file = Deno.openSync("my_file.txt", {read: true});
|
||||
* const myFileContent = readAllSync(file);
|
||||
* file.close();
|
||||
*
|
||||
* // Example from buffer
|
||||
* const myData = new Uint8Array(100);
|
||||
* // ... fill myData array with data
|
||||
* const reader = new Buffer(myData.buffer);
|
||||
* const bufferContent = readAllSync(reader);
|
||||
* ```
|
||||
*/
|
||||
export function readAllSync(r: ReaderSync): Uint8Array {
|
||||
const buf = new Buffer();
|
||||
buf.readFromSync(r);
|
||||
return buf.bytes();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/** Create a `ReadableStream` from any kind of iterable.
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable } from "https://deno.land/std@$STD_VERSION/streams/readable_stream_from_iterable.ts";
|
||||
*
|
||||
* const r1 = readableStreamFromIterable(["foo, bar, baz"]);
|
||||
* const r2 = readableStreamFromIterable(async function* () {
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "foo";
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "bar";
|
||||
* await new Promise(((r) => setTimeout(r, 1000)));
|
||||
* yield "baz";
|
||||
* }());
|
||||
* ```
|
||||
*
|
||||
* If the produced iterator (`iterable[Symbol.asyncIterator]()` or
|
||||
* `iterable[Symbol.iterator]()`) is a generator, or more specifically is found
|
||||
* to have a `.throw()` method on it, that will be called upon
|
||||
* `readableStream.cancel()`. This is the case for the second input type above:
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable } from "https://deno.land/std@$STD_VERSION/streams/readable_stream_from_iterable.ts";
|
||||
*
|
||||
* const r3 = readableStreamFromIterable(async function* () {
|
||||
* try {
|
||||
* yield "foo";
|
||||
* } catch (error) {
|
||||
* console.log(error); // Error: Cancelled by consumer.
|
||||
* }
|
||||
* }());
|
||||
* const reader = r3.getReader();
|
||||
* console.log(await reader.read()); // { value: "foo", done: false }
|
||||
* await reader.cancel(new Error("Cancelled by consumer."));
|
||||
* ```
|
||||
*/
|
||||
export function readableStreamFromIterable<T>(
|
||||
iterable: Iterable<T> | AsyncIterable<T>,
|
||||
): ReadableStream<T> {
|
||||
const iterator: Iterator<T> | AsyncIterator<T> =
|
||||
(iterable as AsyncIterable<T>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<T>)[Symbol.iterator]?.();
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const { value, done } = await iterator.next();
|
||||
if (done) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.enqueue(value);
|
||||
}
|
||||
},
|
||||
async cancel(reason) {
|
||||
if (typeof iterator.throw == "function") {
|
||||
try {
|
||||
await iterator.throw(reason);
|
||||
} catch { /* `iterator.throw()` always throws on site. We catch it. */ }
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { DEFAULT_CHUNK_SIZE } from "./_common.ts";
|
||||
import type { Closer, Reader } from "../types.d.ts";
|
||||
|
||||
function isCloser(value: unknown): value is Closer {
|
||||
return typeof value === "object" && value != null && "close" in value &&
|
||||
// deno-lint-ignore no-explicit-any
|
||||
typeof (value as Record<string, any>)["close"] === "function";
|
||||
}
|
||||
|
||||
export interface ReadableStreamFromReaderOptions {
|
||||
/** If the `reader` is also a `Closer`, automatically close the `reader`
|
||||
* when `EOF` is encountered, or a read error occurs.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
autoClose?: boolean;
|
||||
|
||||
/** The size of chunks to allocate to read, the default is ~16KiB, which is
|
||||
* the maximum size that Deno operations can currently support. */
|
||||
chunkSize?: number;
|
||||
|
||||
/** The queuing strategy to create the `ReadableStream` with. */
|
||||
strategy?: { highWaterMark?: number | undefined; size?: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `ReadableStream<Uint8Array>` from a `Reader`.
|
||||
*
|
||||
* When the pull algorithm is called on the stream, a chunk from the reader
|
||||
* will be read. When `null` is returned from the reader, the stream will be
|
||||
* closed along with the reader (if it is also a `Closer`).
|
||||
*
|
||||
* An example converting a `Deno.FsFile` into a readable stream:
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromReader } from "https://deno.land/std@$STD_VERSION/streams/readable_stream_from_reader.ts";
|
||||
*
|
||||
* const file = await Deno.open("./file.txt", { read: true });
|
||||
* const fileStream = readableStreamFromReader(file);
|
||||
* ```
|
||||
*/
|
||||
export function readableStreamFromReader(
|
||||
reader: Reader | (Reader & Closer),
|
||||
options: ReadableStreamFromReaderOptions = {},
|
||||
): ReadableStream<Uint8Array> {
|
||||
const {
|
||||
autoClose = true,
|
||||
chunkSize = DEFAULT_CHUNK_SIZE,
|
||||
strategy,
|
||||
} = options;
|
||||
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const chunk = new Uint8Array(chunkSize);
|
||||
try {
|
||||
const read = await reader.read(chunk);
|
||||
if (read === null) {
|
||||
if (isCloser(reader) && autoClose) {
|
||||
reader.close();
|
||||
}
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(chunk.subarray(0, read));
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
if (isCloser(reader)) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
if (isCloser(reader) && autoClose) {
|
||||
reader.close();
|
||||
}
|
||||
},
|
||||
}, strategy);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { Buffer } from "../io/buffer.ts";
|
||||
import { writeAll } from "./write_all.ts";
|
||||
import { Reader } from "../types.d.ts";
|
||||
|
||||
/** Create a `Reader` from an iterable of `Uint8Array`s.
|
||||
*
|
||||
* ```ts
|
||||
* import { readerFromIterable } from "https://deno.land/std@$STD_VERSION/streams/reader_from_iterable.ts";
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/streams/copy.ts";
|
||||
*
|
||||
* const file = await Deno.open("metrics.txt", { write: true });
|
||||
* const reader = readerFromIterable((async function* () {
|
||||
* while (true) {
|
||||
* await new Promise((r) => setTimeout(r, 1000));
|
||||
* const message = `data: ${JSON.stringify(Deno.metrics())}\n\n`;
|
||||
* yield new TextEncoder().encode(message);
|
||||
* }
|
||||
* })());
|
||||
* await copy(reader, file);
|
||||
* ```
|
||||
*/
|
||||
export function readerFromIterable(
|
||||
iterable: Iterable<Uint8Array> | AsyncIterable<Uint8Array>,
|
||||
): Reader {
|
||||
const iterator: Iterator<Uint8Array> | AsyncIterator<Uint8Array> =
|
||||
(iterable as AsyncIterable<Uint8Array>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<Uint8Array>)[Symbol.iterator]?.();
|
||||
const buffer = new Buffer();
|
||||
return {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
if (buffer.length == 0) {
|
||||
const result = await iterator.next();
|
||||
if (result.done) {
|
||||
return null;
|
||||
} else {
|
||||
if (result.value.byteLength <= p.byteLength) {
|
||||
p.set(result.value);
|
||||
return result.value.byteLength;
|
||||
}
|
||||
p.set(result.value.subarray(0, p.byteLength));
|
||||
await writeAll(buffer, result.value.subarray(p.byteLength));
|
||||
return p.byteLength;
|
||||
}
|
||||
} else {
|
||||
const n = await buffer.read(p);
|
||||
if (n == null) {
|
||||
return this.read(p);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { Buffer } from "../io/buffer.ts";
|
||||
import { writeAll } from "./write_all.ts";
|
||||
import type { Reader } from "../types.d.ts";
|
||||
|
||||
/**
|
||||
* Create a `Reader` from a `ReadableStreamDefaultReader`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { copy } from "https://deno.land/std@$STD_VERSION/streams/copy.ts";
|
||||
* import { readerFromStreamReader } from "https://deno.land/std@$STD_VERSION/streams/reader_from_stream_reader.ts";
|
||||
*
|
||||
* const res = await fetch("https://deno.land");
|
||||
* const file = await Deno.open("./deno.land.html", { create: true, write: true });
|
||||
*
|
||||
* const reader = readerFromStreamReader(res.body!.getReader());
|
||||
* await copy(reader, file);
|
||||
* file.close();
|
||||
* ```
|
||||
*/
|
||||
export function readerFromStreamReader(
|
||||
streamReader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
): Reader {
|
||||
const buffer = new Buffer();
|
||||
|
||||
return {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
if (buffer.empty()) {
|
||||
const res = await streamReader.read();
|
||||
if (res.done) {
|
||||
return null; // EOF
|
||||
}
|
||||
|
||||
await writeAll(buffer, res.value);
|
||||
}
|
||||
|
||||
return buffer.read(p);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { createLPS } from "./_common.ts";
|
||||
|
||||
import type {
|
||||
DelimiterDisposition,
|
||||
DelimiterStreamOptions,
|
||||
} from "./delimiter_stream.ts";
|
||||
|
||||
/** Transform a stream into a stream where each chunk is divided by a given delimiter.
|
||||
*
|
||||
* ```ts
|
||||
* import { TextDelimiterStream } from "https://deno.land/std@$STD_VERSION/streams/text_delimiter_stream.ts";
|
||||
* const res = await fetch("https://example.com");
|
||||
* const parts = res.body!
|
||||
* .pipeThrough(new TextDecoderStream())
|
||||
* .pipeThrough(new TextDelimiterStream("foo"));
|
||||
* ```
|
||||
*/
|
||||
export class TextDelimiterStream extends TransformStream<string, string> {
|
||||
#buf = "";
|
||||
#delimiter: string;
|
||||
#inspectIndex = 0;
|
||||
#matchIndex = 0;
|
||||
#delimLPS: Uint8Array;
|
||||
#disp: DelimiterDisposition;
|
||||
|
||||
constructor(delimiter: string, options?: DelimiterStreamOptions) {
|
||||
super({
|
||||
transform: (chunk, controller) => {
|
||||
this.#handle(chunk, controller);
|
||||
},
|
||||
flush: (controller) => {
|
||||
controller.enqueue(this.#buf);
|
||||
},
|
||||
});
|
||||
|
||||
this.#delimiter = delimiter;
|
||||
this.#delimLPS = createLPS(new TextEncoder().encode(delimiter));
|
||||
this.#disp = options?.disposition ?? "discard";
|
||||
}
|
||||
|
||||
#handle(
|
||||
chunk: string,
|
||||
controller: TransformStreamDefaultController<string>,
|
||||
) {
|
||||
this.#buf += chunk;
|
||||
let localIndex = 0;
|
||||
while (this.#inspectIndex < this.#buf.length) {
|
||||
if (chunk[localIndex] === this.#delimiter[this.#matchIndex]) {
|
||||
this.#inspectIndex++;
|
||||
localIndex++;
|
||||
this.#matchIndex++;
|
||||
if (this.#matchIndex === this.#delimiter.length) {
|
||||
// Full match
|
||||
const start = this.#inspectIndex - this.#delimiter.length;
|
||||
const end = this.#disp === "suffix" ? this.#inspectIndex : start;
|
||||
const copy = this.#buf.slice(0, end);
|
||||
controller.enqueue(copy);
|
||||
const shift = this.#disp == "prefix" ? start : this.#inspectIndex;
|
||||
this.#buf = this.#buf.slice(shift);
|
||||
this.#inspectIndex = this.#disp == "prefix"
|
||||
? this.#delimiter.length
|
||||
: 0;
|
||||
this.#matchIndex = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.#matchIndex === 0) {
|
||||
this.#inspectIndex++;
|
||||
localIndex++;
|
||||
} else {
|
||||
this.#matchIndex = this.#delimLPS[this.#matchIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
interface TextLineStreamOptions {
|
||||
/** Allow splitting by solo \r */
|
||||
allowCR: boolean;
|
||||
}
|
||||
|
||||
/** Transform a stream into a stream where each chunk is divided by a newline,
|
||||
* be it `\n` or `\r\n`. `\r` can be enabled via the `allowCR` option.
|
||||
*
|
||||
* ```ts
|
||||
* import { TextLineStream } from "https://deno.land/std@$STD_VERSION/streams/text_line_stream.ts";
|
||||
* const res = await fetch("https://example.com");
|
||||
* const lines = res.body!
|
||||
* .pipeThrough(new TextDecoderStream())
|
||||
* .pipeThrough(new TextLineStream());
|
||||
* ```
|
||||
*/
|
||||
export class TextLineStream extends TransformStream<string, string> {
|
||||
readonly #allowCR: boolean;
|
||||
#buf = "";
|
||||
|
||||
constructor(options?: TextLineStreamOptions) {
|
||||
super({
|
||||
transform: (chunk, controller) => this.#handle(chunk, controller),
|
||||
flush: (controller) => {
|
||||
if (this.#buf.length > 0) {
|
||||
if (
|
||||
this.#allowCR &&
|
||||
this.#buf[this.#buf.length - 1] === "\r"
|
||||
) controller.enqueue(this.#buf.slice(0, -1));
|
||||
else controller.enqueue(this.#buf);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.#allowCR = options?.allowCR ?? false;
|
||||
}
|
||||
|
||||
#handle(chunk: string, controller: TransformStreamDefaultController<string>) {
|
||||
chunk = this.#buf + chunk;
|
||||
|
||||
for (;;) {
|
||||
const lfIndex = chunk.indexOf("\n");
|
||||
|
||||
if (this.#allowCR) {
|
||||
const crIndex = chunk.indexOf("\r");
|
||||
|
||||
if (
|
||||
crIndex !== -1 && crIndex !== (chunk.length - 1) &&
|
||||
(lfIndex === -1 || (lfIndex - 1) > crIndex)
|
||||
) {
|
||||
controller.enqueue(chunk.slice(0, crIndex));
|
||||
chunk = chunk.slice(crIndex + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (lfIndex !== -1) {
|
||||
let crOrLfIndex = lfIndex;
|
||||
if (chunk[lfIndex - 1] === "\r") {
|
||||
crOrLfIndex--;
|
||||
}
|
||||
controller.enqueue(chunk.slice(0, crOrLfIndex));
|
||||
chunk = chunk.slice(lfIndex + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.#buf = chunk;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
/**
|
||||
* Convert the generator function into a TransformStream.
|
||||
*
|
||||
* ```ts
|
||||
* import { readableStreamFromIterable } from "https://deno.land/std@$STD_VERSION/streams/readable_stream_from_iterable.ts";
|
||||
* import { toTransformStream } from "https://deno.land/std@$STD_VERSION/streams/to_transform_stream.ts";
|
||||
*
|
||||
* const readable = readableStreamFromIterable([0, 1, 2])
|
||||
* .pipeThrough(toTransformStream(async function* (src) {
|
||||
* for await (const chunk of src) {
|
||||
* yield chunk * 100;
|
||||
* }
|
||||
* }));
|
||||
*
|
||||
* for await (const chunk of readable) {
|
||||
* console.log(chunk);
|
||||
* }
|
||||
* // output: 0, 100, 200
|
||||
* ```
|
||||
*
|
||||
* @param transformer A function to transform.
|
||||
* @param writableStrategy An object that optionally defines a queuing strategy for the stream.
|
||||
* @param readableStrategy An object that optionally defines a queuing strategy for the stream.
|
||||
*/
|
||||
export function toTransformStream<I, O>(
|
||||
transformer: (src: ReadableStream<I>) => Iterable<O> | AsyncIterable<O>,
|
||||
writableStrategy?: QueuingStrategy<I>,
|
||||
readableStrategy?: QueuingStrategy<O>,
|
||||
): TransformStream<I, O> {
|
||||
const {
|
||||
writable,
|
||||
readable,
|
||||
} = new TransformStream<I, I>(undefined, writableStrategy);
|
||||
|
||||
const iterable = transformer(readable);
|
||||
const iterator: Iterator<O> | AsyncIterator<O> =
|
||||
(iterable as AsyncIterable<O>)[Symbol.asyncIterator]?.() ??
|
||||
(iterable as Iterable<O>)[Symbol.iterator]?.();
|
||||
return {
|
||||
writable,
|
||||
readable: new ReadableStream<O>({
|
||||
async pull(controller) {
|
||||
let result: IteratorResult<O>;
|
||||
try {
|
||||
result = await iterator.next();
|
||||
} catch (error) {
|
||||
// Propagate error to stream from iterator
|
||||
// If the stream status is "errored", it will be thrown, but ignore.
|
||||
await readable.cancel(error).catch(() => {});
|
||||
controller.error(error);
|
||||
return;
|
||||
}
|
||||
if (result.done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(result.value);
|
||||
},
|
||||
async cancel(reason) {
|
||||
// Propagate cancellation to readable and iterator
|
||||
if (typeof iterator.throw == "function") {
|
||||
try {
|
||||
await iterator.throw(reason);
|
||||
} catch {
|
||||
/* `iterator.throw()` always throws on site. We catch it. */
|
||||
}
|
||||
}
|
||||
await readable.cancel(reason);
|
||||
},
|
||||
}, readableStrategy),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import { writeAll } from "./write_all.ts";
|
||||
import type { Closer, Writer } from "../types.d.ts";
|
||||
|
||||
function isCloser(value: unknown): value is Closer {
|
||||
return typeof value === "object" && value != null && "close" in value &&
|
||||
// deno-lint-ignore no-explicit-any
|
||||
typeof (value as Record<string, any>)["close"] === "function";
|
||||
}
|
||||
|
||||
export interface WritableStreamFromWriterOptions {
|
||||
/**
|
||||
* If the `writer` is also a `Closer`, automatically close the `writer`
|
||||
* when the stream is closed, aborted, or a write error occurs.
|
||||
*
|
||||
* @default {true}
|
||||
*/
|
||||
autoClose?: boolean;
|
||||
}
|
||||
|
||||
/** Create a `WritableStream` from a `Writer`. */
|
||||
export function writableStreamFromWriter(
|
||||
writer: Writer,
|
||||
options: WritableStreamFromWriterOptions = {},
|
||||
): WritableStream<Uint8Array> {
|
||||
const { autoClose = true } = options;
|
||||
|
||||
return new WritableStream({
|
||||
async write(chunk, controller) {
|
||||
try {
|
||||
await writeAll(writer, chunk);
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
},
|
||||
abort() {
|
||||
if (isCloser(writer) && autoClose) {
|
||||
writer.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||
// This module is browser compatible.
|
||||
|
||||
import type { Writer, WriterSync } from "../types.d.ts";
|
||||
|
||||
/** Write all the content of the array buffer (`arr`) to the writer (`w`).
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "https://deno.land/std@$STD_VERSION/io/buffer.ts";
|
||||
* import { writeAll } from "https://deno.land/std@$STD_VERSION/streams/write_all.ts";
|
||||
|
||||
* // Example writing to stdout
|
||||
* let contentBytes = new TextEncoder().encode("Hello World");
|
||||
* await writeAll(Deno.stdout, contentBytes);
|
||||
*
|
||||
* // Example writing to file
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const file = await Deno.open('test.file', {write: true});
|
||||
* await writeAll(file, contentBytes);
|
||||
* file.close();
|
||||
*
|
||||
* // Example writing to buffer
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const writer = new Buffer();
|
||||
* await writeAll(writer, contentBytes);
|
||||
* console.log(writer.bytes().length); // 11
|
||||
* ```
|
||||
*/
|
||||
export async function writeAll(w: Writer, arr: Uint8Array) {
|
||||
let nwritten = 0;
|
||||
while (nwritten < arr.length) {
|
||||
nwritten += await w.write(arr.subarray(nwritten));
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronously write all the content of the array buffer (`arr`) to the
|
||||
* writer (`w`).
|
||||
*
|
||||
* ```ts
|
||||
* import { Buffer } from "https://deno.land/std@$STD_VERSION/io/buffer.ts";
|
||||
* import { writeAllSync } from "https://deno.land/std@$STD_VERSION/streams/write_all.ts";
|
||||
*
|
||||
* // Example writing to stdout
|
||||
* let contentBytes = new TextEncoder().encode("Hello World");
|
||||
* writeAllSync(Deno.stdout, contentBytes);
|
||||
*
|
||||
* // Example writing to file
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const file = Deno.openSync('test.file', {write: true});
|
||||
* writeAllSync(file, contentBytes);
|
||||
* file.close();
|
||||
*
|
||||
* // Example writing to buffer
|
||||
* contentBytes = new TextEncoder().encode("Hello World");
|
||||
* const writer = new Buffer();
|
||||
* writeAllSync(writer, contentBytes);
|
||||
* console.log(writer.bytes().length); // 11
|
||||
* ```
|
||||
*/
|
||||
export function writeAllSync(w: WriterSync, arr: Uint8Array) {
|
||||
let nwritten = 0;
|
||||
while (nwritten < arr.length) {
|
||||
nwritten += w.writeSync(arr.subarray(nwritten));
|
||||
}
|
||||
}
|
||||
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user