@unikvs/utils
Shared helpers for plugin authors: filename validation, byte operations, stream utilities, and 2 error types.
Overview
@unikvs/utils Shared is a set of small helpers shared by plugin implementations: filename and dirname validation, hex conversion and splitting of bytes, shared UTF-8 instances, owned-byte copies, ReadableStream builders, proxied streams with conversion, and 2 validation-failure error types.
Exported functions:
| Function | Description | Details |
|---|---|---|
assertValidFilename |
Validates a filename, throwing when invalid | Details |
assertValidDirname |
Validates a dirname, throwing when invalid | Details |
isValidFilename |
Returns whether a valid filename | Details |
isValidDirname |
Returns whether a valid dirname | Details |
bytesToHex |
Converts bytes to a lowercase hex string | Details |
chunks |
Splits binary data into sized pieces | Details |
defaultUtf8Encoder |
Shared UTF-8 encoder | Details |
defaultUtf8Decoder |
Shared UTF-8 decoder | Details |
toOwnedBytes |
Returns bytes as owned | Details |
toReadableStream |
Converts an iterable to a stream | Details |
withReadableStreamFrom |
Temporarily polyfills ReadableStream.from |
Details |
proxyTransformStream |
Builds a stream with in/out conversion | Details |
Validation failures are the 2 errors InvalidFilenameError and InvalidDirnameError. Both extend @unikvs/core ErrorBase with registered Japanese messages.
Installation
npm install @unikvs/utilspnpm add @unikvs/utilsyarn add @unikvs/utilsbun add @unikvs/utilsnub add @unikvs/utilsaube add @unikvs/utilsESM only (sideEffects: false). Depends on @unikvs/core, fast-utf8, filename-reserved-regex, ninja-promise, and try-capture-stack-trace, with @logtape/logtape as a peer dependency for logging.
Usage
For filename validation, use the boolean-returning and throwing variants appropriately:
import { assertValidFilename, InvalidFilenameError, isValidFilename } from "@unikvs/utils";
if (!isValidFilename(name)) {
console.log(`${name} is not usable`);
}
try {
assertValidFilename(name);
} catch (ex) {
if (ex instanceof InvalidFilenameError) {
console.log(ex.meta); // { filename: name }
} else {
throw ex;
}
}
For byte conversion and splitting, combine hex conversion with view-based slicing:
import { bytesToHex, chunks } from "@unikvs/utils";
const bytes = new Uint8Array([0, 171, 255]);
console.log(bytesToHex(bytes)); // "00abff"
for (const view of chunks(bytes, 2)) {
// view is a view of the original bytes; no copy
console.log(bytesToHex(view));
}
For stream building, convert from iterables and delegate with conversion:
import { proxyTransformStream, toReadableStream } from "@unikvs/utils";
const stream = toReadableStream([1, 2, 3]);
const proxied = proxyTransformStream(
source,
(chunk, controller) => controller.enqueue(encode(chunk)),
(chunk, controller) => controller.enqueue(decode(chunk)),
);
await proxied.writable.getWriter().close();
await proxied.closePromise;
Errors
Both validation-failure errors keep matching values in meta. Discriminate with instanceof and take the offending name from meta.
InvalidFilenameError
Used for invalid filenames. Thrown by assertValidFilename.
filenamestring
The offending filename.
string- Extends:
ErrorBase(the@unikvs/coreerror foundation) name:UniKvsInvalidFilenameError- English:
Invalid file name: foo/bar - Japanese:
無効なファイル名: foo/bar
InvalidDirnameError
Used for invalid dirnames. Thrown by assertValidDirname.
dirnamestring
The offending dirname.
string- Extends:
ErrorBase(the@unikvs/coreerror foundation) name:UniKvsInvalidDirnameError- English:
Invalid directory name: foo/bar - Japanese:
無効なディレクトリー名: foo/bar
API reference
Per-function reference. For errors, see Errors.
assertValidFilename()
Validates a filename, throwing InvalidFilenameError when invalid. Returns nothing when valid. The stack trace excludes this function itself, so it points at the caller rather than the throw site.
import { assertValidFilename } from "@unikvs/utils";
assertValidFilename("photo.png"); // ok
assertValidFilename("a/b"); // throws InvalidFilenameError
assertValidDirname()
Validates a dirname, throwing InvalidDirnameError when invalid. Returns nothing when valid. The stack trace excludes this function itself, so it points at the caller rather than the throw site.
import { assertValidDirname } from "@unikvs/utils";
assertValidDirname("photos"); // ok
assertValidDirname(".."); // throws InvalidDirnameError
isValidFilename()
Returns whether a string is a valid filename. Rules:
- Rejects empty strings,
".", and"..". - Rejects anything over 255 bytes in UTF-8 encoding.
- Rejects strings that do not round-trip through encode/decode (e.g. containing a BOM).
- Rejects strings not matching NFC normalization.
- Rejects reserved characters and Windows reserved names.
- Explicitly rejects
":".
import { isValidFilename } from "@unikvs/utils";
isValidFilename("photo.png"); // true
isValidFilename(""); // false
isValidFilename("a/b"); // false
isValidDirname()
Returns whether a string is a valid dirname. Defers to isValidFilename, so the rules are the same.
import { isValidDirname } from "@unikvs/utils";
isValidDirname("photos"); // true
isValidDirname(".."); // false
bytesToHex()
Converts bytes to a lowercase hex string. Uses Uint8Array.prototype.toHex when the runtime has it, otherwise an internal 256-entry table. Throws RangeError when invalid byte values (e.g. holes in sparse arrays) are mixed in.
import { bytesToHex } from "@unikvs/utils";
bytesToHex(new Uint8Array([0, 171, 255])); // "00abff"
chunks()
Yields views of binary data sliced at the given size. Accepts DataView and typed arrays. Produced values are same-type views with no copying: a DataView input returns a new DataView sliced by buffer range; typed-array inputs return subarray views. Fractional sizes truncate, with a minimum of 1. Empty data yields nothing.
import { chunks } from "@unikvs/utils";
for (const view of chunks(new Uint8Array([1, 2, 3]), 2)) {
console.log(view); // Uint8Array(2) [1, 2], Uint8Array(1) [3]
}
Shared UTF-8 instances
Shared UTF-8 instances used as defaults when none are injected. defaultUtf8Encoder is a non-strict (strict: false) encoder assuming ASCII input; defaultUtf8Decoder is a strict (strict: true) decoder throwing TypeError on invalid input. Use it for bulk decoding needing TextDecoder-fatal-equivalent behavior.
import { defaultUtf8Decoder, defaultUtf8Encoder } from "@unikvs/utils";
const bytes = defaultUtf8Encoder.encode("hello");
console.log(defaultUtf8Decoder.decode(bytes)); // "hello"
toOwnedBytes()
Returns a view as an owned Uint8Array<ArrayBuffer>. When the backing is an ArrayBuffer covering everything with the Uint8Array constructor itself, returns it as-is without copying. A subclass view covering everything is rewrapped in Uint8Array over the same buffer. Anything else (range views, shared-buffer origins, etc.) is copied into a new array.
import { toOwnedBytes } from "@unikvs/utils";
const owned: Uint8Array<ArrayBuffer> = toOwnedBytes(view);
toReadableStream()
Converts an Iterable or AsyncIterable to a ReadableStream. Passing a string directly yields primitives on iteration, which WebKit’s native from rejects, so it converts to a character array first. Uses withReadableStreamFrom for the conversion.
import { toReadableStream } from "@unikvs/utils";
const stream = toReadableStream([1, 2, 3]);
withReadableStreamFrom()
Runs a callback with a from-capable ReadableStream, whether or not ReadableStream.from exists in the runtime. Runs as-is when from exists; otherwise defines a temporary polyfill and always removes it afterwards. Sync callbacks return their value as-is. The overload taking a PromiseLike-returning async callback has return type never, so carrying a stream built during polyfilling to the outside is untyped. Use it to build streams in sync callbacks like toReadableStream. Iterator-cleanup failures are logged to the Logtape logger.
import { withReadableStreamFrom } from "@unikvs/utils";
const stream = withReadableStreamFrom((ReadableStream) => ReadableStream.from([1, 2, 3]));
proxyTransformStream()
Builds a new stream with input/output conversion around a source. Written values are converted by proxyInput before reaching the source; source output is converted by proxyOutput for reading. proxyInput and proxyOutput take either conversion callbacks or Transformer objects. The returned closePromise resolves when both the input-to-source and source-to-output connections finish, including the output transform’s flush. Mid-stream failures propagate to reads and closePromise, which rejects without waiting for the other connection to finish.
import { proxyTransformStream } from "@unikvs/utils";
const proxied = proxyTransformStream(
source,
(chunk, controller) => controller.enqueue(encode(chunk)),
(chunk, controller) => controller.enqueue(decode(chunk)),
);
IProxiedTransformStream
The stream returned by proxyTransformStream, with input/output conversion.@unikvs/core Extends @unikvs/core ITransformStream.
closePromiseNinjaPromise<void>
Resolves when both the input-to-source and source-to-output connections finish.
NinjaPromise<void>