From 2df31a718e43037cfee21578e6f1dd3178c38de4 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Mon, 6 Apr 2026 10:20:50 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Base32=20encoding=20support?= =?UTF-8?q?=20and=20enhance=20caching=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libraries/deno-commons-mod.ts | 33 +++++++++++++++++++------ single-scripts/get-rfc.ts | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100755 single-scripts/get-rfc.ts diff --git a/libraries/deno-commons-mod.ts b/libraries/deno-commons-mod.ts index e7b3c87..f36bd2c 100644 --- a/libraries/deno-commons-mod.ts +++ b/libraries/deno-commons-mod.ts @@ -2,6 +2,7 @@ // - https://docs.deno.com/runtime/fundamentals/testing/ import {decodeBase64, encodeBase64} from "jsr:@std/encoding/base64"; +import {encodeBase32} from "jsr:@std/encoding/base32"; import {dirname, fromFileUrl} from "jsr:@std/path"; import {toArrayBuffer} from "jsr:@std/streams"; import {spawn, SpawnOptionsWithoutStdio} from "node:child_process"; @@ -812,12 +813,14 @@ export function joinPath(path1: string, ...paths: string[]): string { if (paths != null && paths.length > 0) { for (let i = 0; i < paths.length; i++) { const path2 = paths[i]; - if (basePath.endsWith("/") && path2.startsWith("/")) { - basePath += path2.substring(1); - } else if (basePath.endsWith("/") || path2.startsWith("/")) { - basePath += path2; - } else { - basePath += "/" + path2; + if (path2) { + if (basePath.endsWith("/") && path2.startsWith("/")) { + basePath += path2.substring(1); + } else if (basePath.endsWith("/") || path2.startsWith("/")) { + basePath += path2; + } else { + basePath += "/" + path2; + } } } } @@ -952,6 +955,10 @@ export function uint8ArrayToHexString(uint8: Uint8Array): string { .join(""); } +export function uint8ArrayToBase32String(uint8: Unit8Array): string { + return encodeBase32(uint8).replaceAll(/=/g, "").toLowerCase(); +} + export function hexStringToUint8Array(hex: string): Uint8Array { hex = hex.trim(); if (hex.startsWith("0x") || hex.startsWith("0X")) { @@ -1149,6 +1156,12 @@ export async function sha256AndHexOfString(input: string): Promise { return uint8ArrayToHexString(new Uint8Array(await sha256OfString(input))); } +export async function sha256AndBase32OfString(input: string): Promise { + return uint8ArrayToBase32String( + new Uint8Array(await sha256OfString(input)), + ); +} + export async function sha256OfString(input: string): Promise { const data = new TextEncoder().encode(input); return await crypto.subtle.digest("SHA-256", data); @@ -1165,6 +1178,8 @@ export interface FetchFileWithCacheMeta { export interface FetchFileWithCacheOptions { baseDir?: string; + hierarchicalCacheDir?: boolean; + base32Filename?: boolean; tag?: string; timeoutMillis?: number; check_cache_file?: ( @@ -1190,9 +1205,13 @@ export async function fetchFileWithCache( url: string, options?: FetchFileWithCacheOptions, ): Promise { - const urlSha256 = await sha256AndHexOfString(url); + const urlSha256 = options?.base32Filename + ? await sha256AndBase32OfString(url) + : await sha256AndHexOfString(url); const fileCacheDir = joinPath( resolveFilename(options?.baseDir ?? BASE_FILE_CACHE_DIR), + options?.hierarchicalCacheDir ? urlSha256.substring(0, 2) : null, + options?.hierarchicalCacheDir ? urlSha256.substring(2, 4) : null, urlSha256, ); const fileCacheMetaFile = fileCacheDir + ".meta"; diff --git a/single-scripts/get-rfc.ts b/single-scripts/get-rfc.ts new file mode 100755 index 0000000..b9b3842 --- /dev/null +++ b/single-scripts/get-rfc.ts @@ -0,0 +1,45 @@ +#!/usr/bin/env -S deno run -A + +import { parseArgs } from "jsr:@std/cli/parse-args"; +import { + args, + exit, + fetchFileWithCache, + readFileToString, +} from "https://global.hatter.ink/script/get/@62/deno-commons-mod.ts"; + +const flags = parseArgs(args(), { + boolean: ["help"], + string: ["id"], +}); + +function printHelp() { + console.log("get-rfc.ts --id "); +} + +if (flags.help) { + printHelp(); + exit(0); +} + +if (flags.id) { + const rfcId = parseInt(flags.id, 10); + if (isNaN(rfcId)) { + console.error("Invalid RFC id"); + exit(1); + } + await getAndPrintRfc(rfcId); +} else { + printHelp(); + exit(1); +} + +async function getAndPrintRfc(rfcId: number): Promise { + const url = `https://play.hatter.me/ietf/rfc${rfcId}.txt`; + const fileMeta = await fetchFileWithCache(url, { + base32Filename: true, + hierarchicalCacheDir: true, + }); + const content = await readFileToString(fileMeta.cache_full_path); + console.log(content); +}