Started to implement server

This commit is contained in:
didier amyot committed 2023-06-11 15:43:45 -04:00
1 parent 1f2ea8b7e5
commit 17a1b4bed3
177 files changed
+29894 -74

No files matched your search

+1
View File
@@ -31,6 +31,7 @@
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [deno
hey
psrecord];
};
});
+61 -18
View File
@@ -1,22 +1,31 @@
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";
export interface FileService {
list(): AsyncIterable<FileMetaData>;
createFromFullBody(
path: string,
body: string | Uint8Array,
body: string | Uint8Array | Deno.Reader,
): Promise<FileMetaData>;
list(): AsyncIterable<FileMetaData>;
deleteByPath(path: string): Promise<void>;
createFromReader(path: string, body: Deno.Reader): Promise<FileMetaData>;
getFullBodyByPath(path: string): Promise<string | null>;
getRawBodyByPath(path: string): Promise<Uint8Array | null>;
getMetaDataByID(id: string): Promise<FileMetaData | null>;
renameById(id: string, newPath: string): Promise<FileMetaData | null>;
renameByPath(
originalPath: string,
newPath: string,
): Promise<FileMetaData | null>;
deleteByPath(path: string): Promise<void>;
}
interface SerializedFile {
@@ -70,28 +79,26 @@ export class DenoKVBlobService {
}
async persistBlob(index: string, body: Uint8Array): Promise<void> {
async persistBlob(index: string, body: Uint8Array): Promise<number> {
let blobCount = 0;
let size = 0;
for (const txt of this.splitText(body)) {
await this.unsafePersistBlob(index, blobCount, txt);
size += txt.length;
blobCount += 1;
}
return size;
}
async persistReader(index: string, body: Deno.Reader): Promise<void>{
async persistReader(index: string, body: Deno.Reader): Promise<number>{
let i = 0;
let size = 0;
for await (const chunk of iterateReader(body, {bufSize: this.blobSize})){
this.unsafePersistBlob(index, i, chunk);
size += chunk.length;
i++;
}
}
// todo: remove
private concatUint8Array(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {
const mergedArray = new Uint8Array(arr1.length + arr2.length);
mergedArray.set(arr1);
mergedArray.set(arr2, arr1.length);
return mergedArray;
return size;
}
async getBlob(index: string): Promise<Uint8Array> {
@@ -105,11 +112,25 @@ export class DenoKVBlobService {
`Inconsistant blob index for key "${foo.value}", this is very bad.`,
);
}
ret = this.concatUint8Array(ret, foo.value);
ret = concatUint8Array(ret, foo.value);
}
return ret;
}
private async *iterRawBlobs(index: string): AsyncIterable<Uint8Array>{
const prefix = ['blob_index', index];
for await (const itm of this.kv.list<string>({ prefix })) {
const foo = await this.kv.get<Uint8Array>(['blob', itm.value]);
if (foo.value === null) {
throw new Error(
`Inconsistant blob index for key "${foo.value}", this is very bad.`,
);
}
yield foo.value;
}
}
async getNumberOfBlobs(index: string): Promise<number> {
let ret = 0;
for await (
@@ -119,6 +140,10 @@ export class DenoKVBlobService {
}
return ret;
}
streamBlob(index: string): Deno.Reader{
return new AsyncIterableReader(this.iterRawBlobs(index));
}
}
export class DenoKVFileService implements FileService {
@@ -135,17 +160,35 @@ export class DenoKVFileService implements FileService {
body: string | Uint8Array,
): Promise<FileMetaData> {
const newId = crypto.randomUUID();
const bodyBytes: Uint8Array = body instanceof Uint8Array ? body : new TextEncoder().encode(body);
const size = await this.blobService.persistBlob(newId, bodyBytes);
const serializedFile: SerializedFile = {
id: newId,
path,
bodySize: body.length,
bodySize: size,
};
const idx: SerializedFilePathIndex = { fileId: newId };
await this.kv.set(['file', newId], serializedFile);
await this.kv.set(['file_index_path', path], idx);
const bodyBytes: Uint8Array = body instanceof Uint8Array ? body : new TextEncoder().encode(body);
await this.blobService.persistBlob(newId, bodyBytes);
return { id: newId, path, bodySize: body.length };
return { id: newId, path, bodySize: size };
}
async createFromReader(path: string, body: Deno.Reader): Promise<FileMetaData>{
const newId = crypto.randomUUID();
const size = await this.blobService.persistReader(newId, body);
const serializedFile: SerializedFile = {
id: newId,
path,
bodySize: size,
};
const idx: SerializedFilePathIndex = { fileId: newId };
await this.kv.set(['file', newId], serializedFile);
await this.kv.set(['file_index_path', path], idx);
return { id: newId, path, bodySize: size };
}
async *list(): AsyncIterable<FileMetaData> {
+48
View File
@@ -0,0 +1,48 @@
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";
type Handler = (ctx: Context) => void;
function listFileHandlerFactory(fileService: FileService): Handler {
return async (ctx: Context) => {
ctx.response.type = "application/json";
ctx.response.body = await asyncIterableToArray(fileService.list());
};
}
function createFileHandlerFactory(fileService: FileService): Handler {
return async (ctx: Context) => {
const untypedCtx: any = ctx;
ctx.response.body = 'This is a post';
const path = untypedCtx.request.url.searchParams.get('path');
const reqBody = await ctx.request.body({ type: "stream" }).value;
const reader = readerFromStreamReader(reqBody.getReader())
fileService.createFromReader(path, reader);
};
}
function getFileHandlerFactory(fileService: FileService): Handler {
return (ctx: Context) => {
const untypedCtx: any = ctx;
const fileId = untypedCtx.params.file_id;
// ctx.response.body = await fileService.getRawBodyByPath()
};
}
export function buildAPI(router: Router, fileService: FileService): Router {
router.get('/files', listFileHandlerFactory(fileService));
router.get('/files/:file_id', getFileHandlerFactory(fileService));
router.post('/files', createFileHandlerFactory(fileService));
return router;
}
+15 -6
View File
@@ -1,9 +1,18 @@
import { Application } from 'https://deno.land/x/oak/mod.ts';
import { Application, Router } from 'https://deno.land/x/oak/mod.ts';
import { buildAPI } from './restapi.ts';
import { DenoKVFileService } from "./fileservice.ts";
const app = new Application();
async function main(){
const app = new Application();
const kv = await Deno.openKv();
const fileService = new DenoKVFileService(kv);
const router = new Router({ prefix: '/v1' });
app.use((ctx) => {
ctx.response.body = 'Hello World!';
});
app.use(
buildAPI(router, fileService).routes(),
);
await app.listen({ port: 9000 });
await app.listen({ port: 9000 });
}
await main();
+69
View File
@@ -0,0 +1,69 @@
export async function asyncIterableToArray<T>(ite: AsyncIterable<T>): Promise<T[]> {
const ret: T[] = [];
for await (const e of ite) {
ret.push(e);
}
return ret;
}
export function concatUint8Array(arr1: Uint8Array, arr2: Uint8Array): Uint8Array {
const mergedArray = new Uint8Array(arr1.length + arr2.length);
mergedArray.set(arr1);
mergedArray.set(arr2, arr1.length);
return mergedArray;
}
export class AsyncIterableReader implements Deno.Reader {
private readonly source: AsyncIterable<Uint8Array>;
private interReadBuffer = new Uint8Array();
private isSourceEmpty = false;
constructor(source: AsyncIterable<Uint8Array>) {
this.source = source;
}
private async readMinimumOf(n: number): Promise<Uint8Array> {
let ret = new Uint8Array();
console.log('))) ' + n )
for await(const piece of this.source){
console.log(piece.length)
ret = concatUint8Array(ret, piece);
if(ret.length >= n){
console.log('*******************************')
return ret;
}
}
console.log('---------------');
console.log(this.source);
this.isSourceEmpty = true;
return ret;
}
private transferData(source: Uint8Array, dest: Uint8Array): number {
const limit = Math.min(source.length, dest.length);
for(let i = 0; i < limit; i ++){
dest[i] = source[i];
}
return limit;
}
private readFromInternalReadBuffer(buffer: Uint8Array): number {
const n = this.transferData(this.interReadBuffer, buffer);
this.interReadBuffer = this.interReadBuffer.subarray(n);
return n;
}
async read(buffer: Uint8Array): Promise<number | null> {
if(this.isSourceEmpty && this.interReadBuffer.length < 1){
console.log("done mother fucker");
return null;
}
const internalReadBufferTransferdSize = this.readFromInternalReadBuffer(buffer);
const data = await this.readMinimumOf(buffer.length - internalReadBufferTransferdSize);
const transferedSize = this.transferData(data.subarray(internalReadBufferTransferdSize), buffer);
this.interReadBuffer = data.subarray(transferedSize);
return internalReadBufferTransferdSize + transferedSize;
}
}
+23 -50
View File
@@ -6,50 +6,10 @@ import {
FileMetaData,
FileService,
} from "../src/fileservice.ts";
import { asyncIterableToArray } from "../src/utils.ts";
import { generateLargeText } from "./utils.ts";
function generateLargeText(seed: number, size: number): string {
const words = [
"Norway",
"Germany",
"Ecuador",
"Chile",
"Colombia",
"Sri Lanka",
"Guinea",
"United Kingdom",
"Egypt",
"Palau",
"Latvia",
"Congo, Democratic Republic of the",
"Malawi",
"Senegal",
"Trinidad and Tobago",
"Guatemala",
"Chad",
"New Zealand",
"Malaysia",
"Lithuania",
"Taiwan",
"Solomon Islands",
"Grenada",
"Hungary",
"Korea, South",
];
let ret: string[] = [];
for (let i = 0; i < size; i++) {
seed = (seed * 4931 + 2753) % words.length;
ret.push(words[seed]);
}
return ret.join(" ");
}
async function asyncIterableToArray<T>(ite: AsyncIterable<T>): Promise<T[]> {
const ret: T[] = [];
for await (const e of ite) {
ret.push(e);
}
return ret;
}
Deno.test("DenoKVBlobService happy path", async (t) => {
localStorage.clear();
@@ -62,25 +22,29 @@ Deno.test("DenoKVBlobService happy path", async (t) => {
}
await t.step("read write small text", async () => {
await bs.persistBlob("0002", toUint8Array("hello world"));
const size = await bs.persistBlob("0002", toUint8Array("hello world"));
const body = await bs.getBlob("0002");
assertEquals(body, toUint8Array("hello world"));
assertEquals(await bs.getNumberOfBlobs("0002"), 1);
assertEquals(size, "hello world".length);
});
await t.step("read write large text", async () => {
const largeText = toUint8Array(generateLargeText(33, 100));
await bs.persistBlob("0001", largeText);
const size = await bs.persistBlob("0001", largeText);
const body = await bs.getBlob("0001");
assertEquals(body, largeText);
assertEquals(await bs.getNumberOfBlobs("0001"), 21);
assertEquals(size, largeText.length);
});
await t.step("read write large file", async () => {
const f = await Deno.open('tests/examples/teapot.obj');
await bs.persistReader("large-stream", f);
const fileName = 'tests/examples/teapot.obj';
const f = await Deno.open(fileName);
const size = await bs.persistReader("large-stream", f);
assertEquals(await bs.getNumberOfBlobs("large-stream"), 1030);
assertEquals((await Deno.readFile(fileName)).length, size);
f.close();
});
@@ -95,12 +59,12 @@ Deno.test("DenoKVFileService happy path", async (t) => {
let file1: FileMetaData | undefined;
let file2: FileMetaData | undefined;
await t.step("Create 2 files", async () => {
await t.step("Create 2 files xyzabc", async () => {
file1 = await fs.createFromFullBody(fileName1, "bob le chef");
file2 = await fs.createFromFullBody(fileName2, "le geant vert ☀️");
assertEquals(file1.path, fileName1);
assertEquals(file2.bodySize, "le geant vert ".length + 2);
assertEquals(file2.bodySize, 20);
});
await t.step("Get file metadata", async () => {
@@ -133,14 +97,23 @@ Deno.test("DenoKVFileService happy path", async (t) => {
assertEquals(await asyncIterableToArray(ret), expected);
});
await t.step("Create File from reader", async () => {
const newPath = "boris/streamed-file.txt";
const file = await Deno.open('tests/examples/teapot.obj');
const ret = await fs.createFromReader(newPath, file);
assertEquals(ret.bodySize, 51493);
file.close();
});
await t.step("Rename file", async () => {
const newPath = "boris/file2.txt";
const ret = await fs.renameById(file2?.id ?? "404", newPath);
assertEquals(ret?.path, newPath);
assertEquals(
(await fs.getFullBodyByPath(newPath))?.length,
file2?.bodySize ?? -1,
await fs.getFullBodyByPath(newPath),
'le geant vert ☀️',
);
});
+56
View File
@@ -0,0 +1,56 @@
export function* randomNumbers(seed: number, max: number) : Generator<number> {
const a = 48271;
const m = 2147483647;
max = Math.floor(max);
while (true) {
seed = (a * seed) % m;
yield seed % max;
}
}
export function range(min: number, max: number): number[]{
const ret = []
for(let i = min; i < max; i ++){
ret.push(i);
}
return ret;
}
export function generateLargeText(seed: number, size: number): string {
const words = [
"Norway",
"Germany",
"Ecuador",
"Chile",
"Colombia",
"Sri Lanka",
"Guinea",
"United Kingdom",
"Egypt",
"Palau",
"Latvia",
"Congo, Democratic Republic of the",
"Malawi",
"Senegal",
"Trinidad and Tobago",
"Guatemala",
"Chad",
"New Zealand",
"Malaysia",
"Lithuania",
"Taiwan",
"Solomon Islands",
"Grenada",
"Hungary",
"Korea, South",
];
let ret: string[] = [];
for (let i = 0; i < size; i++) {
seed = (seed * 4931 + 2753) % words.length;
ret.push(words[seed]);
}
return ret.join(" ");
}
+83
View File
@@ -0,0 +1,83 @@
import { readAll } from "https://deno.land/std@0.191.0/streams/read_all.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { AsyncIterableReader } from "../src/utils.ts";
import { randomNumbers, range } from "./utils.ts";
Deno.test("AsyncIterableReader", async (t) => {
async function* miniSource(){
yield Promise.resolve(new Uint8Array([1, 2, 3]));
yield Promise.resolve(new Uint8Array([4, 5, 6, 7]));
yield Promise.resolve(new Uint8Array([8, 9]));
}
async function* largeSource(size: number): AsyncIterable<Uint8Array>{
let val = 0
const random = randomNumbers(77, size / 4);
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)));
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('largeSource', async () =>{
const source = largeSource(32920);
for await(const _piece of source){
//
}
});
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
+25
View File
@@ -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
View File
@@ -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
View File
@@ -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>;
}
+161
View File
@@ -0,0 +1,161 @@
// 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 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");
}
}
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+27
View File
@@ -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;
}
+49
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+21
View File
@@ -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
View File
@@ -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,33 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* {@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;
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* {@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)]);
}
+805
View File
@@ -0,0 +1,805 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** 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];
}
}
}
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** 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
View File
@@ -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;
}
+186
View File
@@ -0,0 +1,186 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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)
: ["*"];
}
+434
View File
@@ -0,0 +1,434 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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);
} else if (err instanceof Error) {
const e = new PartialReadError();
e.partial = p.subarray(0, bytesRead);
e.stack = err.stack;
e.message = err.message;
e.cause = err.cause;
throw err;
}
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;
} else if (err instanceof Error) {
const e = new PartialReadError();
e.partial = slice;
e.stack = err.stack;
e.message = err.message;
e.cause = err.cause;
throw err;
}
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);
} else if (err instanceof Error) {
const e = new PartialReadError();
e.partial = this.#buf.subarray(this.#r, this.#w);
e.stack = err.stack;
e.message = err.message;
e.cause = err.cause;
throw err;
}
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);
}
}
+221
View File
@@ -0,0 +1,221 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
}
+247
View File
@@ -0,0 +1,247 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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
View File
@@ -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";
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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
View File
@@ -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;
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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);
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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;
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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);
}
}
+73
View File
@@ -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
View File
@@ -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
View File
@@ -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,75 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,31 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,68 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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
View File
@@ -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,129 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,24 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 });
}
+53
View File
@@ -0,0 +1,53 @@
// 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/
/**
* 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,
sep,
toFileUrl,
toNamespacedPath,
} = path;
export * from "./common.ts";
export { SEP, SEP_PATTERN } from "./separator.ts";
export * from "./_interface.ts";
export * from "./glob.ts";
+487
View File
@@ -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;
}
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,44 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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);
}
}
},
});
}
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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;
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,33 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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);
}
},
});
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,36 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** 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,32 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** 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
View File
@@ -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";
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,62 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** 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,80 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,55 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,42 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,67 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import { createLPS } from "./_common.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;
constructor(delimiter: string) {
super({
transform: (chunk, controller) => {
this.#handle(chunk, controller);
},
flush: (controller) => {
controller.enqueue(this.#buf);
},
});
this.#delimiter = delimiter;
this.#delimLPS = createLPS(new TextEncoder().encode(delimiter));
}
#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 matchEnd = this.#inspectIndex - this.#delimiter.length;
const readyString = this.#buf.slice(0, matchEnd);
controller.enqueue(readyString);
// Reset match, different from KMP.
this.#buf = this.#buf.slice(this.#inspectIndex);
this.#inspectIndex = 0;
this.#matchIndex = 0;
}
} else {
if (this.#matchIndex === 0) {
this.#inspectIndex++;
localIndex++;
} else {
this.#matchIndex = this.#delimLPS[this.#matchIndex - 1];
}
}
}
}
}
@@ -0,0 +1,73 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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,74 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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,51 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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();
}
},
});
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
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));
}
}
@@ -0,0 +1,34 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import type { Writer } from "../types.d.ts";
/** Create a `Writer` from a `WritableStreamDefaultWriter`.
*
* @example
* ```ts
* import { copy } from "https://deno.land/std@$STD_VERSION/streams/copy.ts";
* import { writerFromStreamWriter } from "https://deno.land/std@$STD_VERSION/streams/writer_from_stream_writer.ts";
*
* const file = await Deno.open("./deno.land.html", { read: true });
*
* const writableStream = new WritableStream({
* write(chunk): void {
* console.log(chunk);
* },
* });
* const writer = writerFromStreamWriter(writableStream.getWriter());
* await copy(file, writer);
* file.close();
* ```
*/
export function writerFromStreamWriter(
streamWriter: WritableStreamDefaultWriter<Uint8Array>,
): Writer {
return {
async write(p: Uint8Array): Promise<number> {
await streamWriter.ready;
await streamWriter.write(p);
return p.length;
},
};
}
@@ -0,0 +1,34 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/**
* 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 continue adding data in order,
* and the finished one will not add any more data.
*/
export function zipReadableStreams<T>(
...streams: ReadableStream<T>[]
): ReadableStream<T> {
const readers = streams.map((s) => s.getReader());
return new ReadableStream<T>({
async start(controller) {
try {
let resolved = 0;
while (resolved != streams.length) {
for (const [key, reader] of Object.entries(readers)) {
const { value, done } = await reader.read();
if (!done) {
controller.enqueue(value!);
} else {
resolved++;
readers.splice(+key, 1);
}
}
}
controller.close();
} catch (e) {
controller.error(e);
}
},
});
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
/** See the Contributing > Types section in the README for an explanation of this file. */
/** An abstract interface which when implemented provides an interface to read bytes into an array buffer asynchronously. */
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@$STD_VERSION/streams/iterate_reader.ts to turn a Reader into an
* AsyncIterator.
*/
read(p: Uint8Array): Promise<number | null>;
}
/** An abstract interface which when implemented provides an interface to read bytes into an array buffer synchronously. */
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@$STD_VERSION/streams/iterate_reader.ts to turn a ReaderSync
* into an Iterator.
*/
readSync(p: Uint8Array): number | null;
}
/** An abstract interface which when implemented provides an interface to write bytes from an array buffer to a file/resource asynchronously. */
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>;
}
/** An abstract interface which when implemented provides an interface to write bytes from an array buffer to a file/resource synchronously. */
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;
}
/** An abstract interface which when implemented provides an interface to close files/resources that were previously opened. */
export interface Closer {
/** Closes the resource, "freeing" the backing file/resource. */
close(): void;
}
+25
View File
@@ -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");
}
+48
View File
@@ -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
View File
@@ -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");
}
}
+38
View File
@@ -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;
}
+249
View File
@@ -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;
}
}
}
Loaded 100 of 177 files, more files were not shown because too many files have changed in this diff. Show more