// Reference: // - https://docs.deno.com/runtime/fundamentals/testing/ import { assert } from "jsr:@std/assert/assert"; import { assertEquals } from "jsr:@std/assert"; import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64"; import { dirname } from "https://deno.land/std@0.208.0/path/mod.ts"; // reference: https://docs.deno.com/examples/hex_base64_encoding/ // import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64"; // import { decodeHex, encodeHex } from "jsr:@std/encoding/hex"; export async function sleep(timeoutMillis: number): Promise { await new Promise((resolve) => setTimeout(resolve, timeoutMillis)); } export function compareVersion(ver1: string, ver2: string): 0 | 1 | -1 { if (ver1 === ver2) return 0; const ver1Parts = ver1.split("."); const ver2Parts = ver2.split("."); const ver1Main = parseInt(ver1Parts[0]); const ver2Main = parseInt(ver2Parts[0]); if (ver1Main > ver2Main) return 1; if (ver1Main < ver2Main) return -1; const ver1Second = parseInt(ver1Parts[1]); const ver2Second = parseInt(ver2Parts[1]); if (ver1Second > ver2Second) return 1; if (ver1Second < ver2Second) return -1; const ver1Third = parseInt(ver1Parts[2]); const ver2Third = parseInt(ver2Parts[2]); if (ver1Third > ver2Third) return 1; if (ver1Third < ver2Third) return -1; return 0; } export function isOn(val: string | undefined | null): boolean { if ((val === null) || (val === undefined)) { return false; } const lowerVal = val.toLowerCase(); return lowerVal === "on" || lowerVal === "yes" || lowerVal === "1" || lowerVal === "true"; } export function getEnv(envKey: string): string | null { const homeDir = getHomeDir(); if ((homeDir !== null) && envKey) { const envValue = readFileToStringSync( `${homeDir}/.config/envs/${envKey}`, ); if (envValue !== null) { return envValue.trim(); } } return Deno.env.get(envKey) || null; } export function isEnvOn(envKey: string): boolean { return isOn(getEnv(envKey)); } export function formatHumanTime(timeMillis: number): string { const times = []; if (timeMillis < 1000) { return `${timeMillis}ms`; } const timeSecs = Math.floor(timeMillis / 1000); const timeSecsLow = timeSecs % 60; if (timeSecsLow > 0) { times.push(`${timeSecsLow}s`); } const timeMinutes = Math.floor(timeSecs / 60); const timeMinutesLow = timeMinutes % 60; if (timeMinutesLow > 0) { times.push(`${timeMinutesLow}m`); } const timeHours = Math.floor(timeMinutes / 60); const timeHoursLow = timeHours % 24; if (timeHoursLow > 0) { times.push(`${timeHoursLow}h`); } const timeDays = Math.floor(timeHours / 24); if (timeDays > 0) { times.push(`${timeDays}d`); } return times.reverse().join(" "); } export function formatSize(size: number): string { if (size < 0) { return "N/A"; } if (size == 0) { return "0B"; } const sizes = []; const bytesLow = size % 1024; if (bytesLow > 0) { sizes.push(`${bytesLow}B`); } const kb = Math.floor(size / 1024); const kbLow = kb % 1024; if (kbLow > 0) { sizes.push(`${kbLow}KiB`); } const mb = Math.floor(kb / 1024); const mbLow = mb % 1024; if (mbLow > 0) { sizes.push(`${mbLow}MiB`); } const gb = Math.floor(mb / 1024); if (gb > 0) { sizes.push(`${gb}GiB`); } return sizes.reverse().join(" "); } export function formatSize2(size: number): string { if (size < 0) { return "N/A"; } if (size < 1024) { return `${size}B`; } if (size < 1024 * 1024) { return `${formatNumber(size / 1024)}KiB`; } return `${formatNumber(size / (1024 * 1024))}MiB`; } export function formatPercent(a: number, b: number): string { if (b == null || b <= 0) { return "N/A"; } return formatNumber((a * 100) / b) + "%"; } export function formatNumber(num: number): string { const p = num.toString(); const pointIndex = p.indexOf("."); if (pointIndex < 0) { return p + ".00"; } const decimal = p.substring(pointIndex + 1); const decimalPart = decimal.length == 1 ? (decimal + "0") : decimal.substring(0, 2); return p.substring(0, pointIndex) + "." + decimalPart; } export async function clearLastLine() { await printLastLine(""); } export async function printLastLine(line: string) { await Deno.stdout.write( new TextEncoder().encode( `\x1b[1000D${line}\x1b[K`, ), ); } class Term { constructor() { } blink(message: string): string { return `\x1b[5m${message}\x1b[0m`; } bold(message: string): string { return `\x1b[1m${message}\x1b[0m`; } red(message: string): string { return `\x1b[31m${message}\x1b[0m`; } green(message: string): string { return `\x1b[32m${message}\x1b[0m`; } yellow(message: string): string { return `\x1b[33m${message}\x1b[0m`; } } export const term = new Term(); function pad(message: string, length: number): string { if (message.length >= length) { return message; } return message + " ".repeat(length - message.length); } const LOGGER_PREFIX_LEN: number = 8; class Logger { constructor() { } // deno-lint-ignore no-explicit-any success(...data: any[]) { this.log( term.bold(term.green(`[${pad("SUCCESS", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any error(...data: any[]) { this.log( term.bold(term.red(`[${pad("ERROR", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warn(...data: any[]) { this.log( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warning(...data: any[]) { this.log( term.blink( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), ), data, ); } // deno-lint-ignore no-explicit-any info(...data: any[]) { this.log(term.bold(`[${pad("INFO", LOGGER_PREFIX_LEN)}]`), data); } // deno-lint-ignore no-explicit-any debug(...data: any[]) { this.log(`[${pad("DEBUG", LOGGER_PREFIX_LEN)}]`, data); } // deno-lint-ignore no-explicit-any log(prefix: string, data: any[]) { const args = [prefix]; for (let i = 0; i < data.length; i++) { args.push(data[i]); } console.log.apply(console, args); } } export const log = new Logger(); export function getHomeDirOrDie(): string { const homeDir = getHomeDir(); if (homeDir === null) { throw new Error("Cannot find home dir"); } return homeDir; } export function getHomeDir(): string | null { if (Deno.build.os === "windows") { const userProfile = Deno.env.get("USERPROFILE"); if (userProfile) { return userProfile; } const homeDrive = Deno.env.get("HOMEDRIVE"); const homePath = Deno.env.get("HOMEPATH"); if (homeDrive && homePath) { return homeDrive + homePath; } return null; } return Deno.env.get("HOME") || null; } export function resolveFilename(filename: string): string { if (filename.startsWith("~/")) { return getHomeDir() + filename.substring(1); } return filename; } export function joinPath(path1: string, ...paths: string[]): string { let basePath = path1; 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; } } } return basePath; } export async function existsPath(path: string): Promise { try { const stat = await Deno.stat(path); return stat != null; } catch { return false; } } export async function readFileToString( filename: string, ): Promise { try { return await Deno.readTextFile(resolveFilename(filename)); } catch (e) { if (e instanceof Error && e.name == "NotFound") { return null; } throw e; } } export function readFileToStringSync(filename: string): string | null { try { return Deno.readTextFileSync(resolveFilename(filename)); } catch (e) { if (e instanceof Error && e.name == "NotFound") { return null; } throw e; } } export async function writeStringToFile( filename: string, data: string | null, ): Promise { const newFilename = resolveFilename(filename); if (data == null) { if (await existsPath(newFilename)) { await Deno.remove(newFilename); } } else { const parentDirname = dirname(newFilename); if (!await existsPath(parentDirname)) { await Deno.mkdir(parentDirname, { recursive: true }); } await Deno.writeTextFile(newFilename, data); } } export function uint8ArrayToHexString(uint8: Uint8Array): string { return Array.from(uint8) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export function hexStringToUint8Array(hex: string): Uint8Array { hex = hex.trim(); if (hex.startsWith("0x") || hex.startsWith("0X")) { hex = hex.slice(2); } if (hex.length % 2 !== 0) { throw new Error("Hex string must have an even number of characters"); } if (!/^[0-9a-fA-F]*$/.test(hex)) { throw new Error("Invalid hex string"); } const byteLength = hex.length / 2; const uint8 = new Uint8Array(byteLength); for (let i = 0; i < byteLength; i++) { uint8[i] = parseInt(hex.substring(i * 2, (i + 1) * 2), 16); } return uint8; } export function decodeBase64Url(base64UrlString: string): Uint8Array { let standardBase64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/"); while (standardBase64.length % 4) { standardBase64 += "="; } return decodeBase64(standardBase64); } export function encodeBase64Url(input: ArrayBufferLike): string { let standardBased64 = encodeBase64(input); return standardBased64.replace(/\+/g, "-").replace(/\//g, "_").replace( /=/g, "", ); } export function getKeyRingPassword( service: string, user: string, ): string | null { const command = new Deno.Command("keyring.rs", { args: ["-g", "--json", "-S", service, "-U", user], }); const { code, stdout, stderr } = command.outputSync(); const stdoutString = new TextDecoder().decode(stdout); const stderrString = new TextDecoder().decode(stderr); if (code != 0) { if (stderrString && stderrString.includes("Error: NoEntry")) { return null; } throw new Error( `keyring.rs -g failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, ); } const result = JSON.parse(stdoutString) as { password: string; }; return result.password; } export function setKeyRingPassword( service: string, user: string, password: string, ): void { const command = new Deno.Command("keyring.rs", { args: ["-s", "-S", service, "-U", user, "-P", password], }); const { code, stdout, stderr } = command.outputSync(); const stdoutString = new TextDecoder().decode(stdout); const stderrString = new TextDecoder().decode(stderr); if (code != 0) { throw new Error( `keyring.rs -s failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`, ); } return; } Deno.test("isOn", () => { assertEquals(false, isOn(undefined)); assertEquals(false, isOn("")); assertEquals(true, isOn("true")); assertEquals(true, isOn("TRUE")); assertEquals(true, isOn("yes")); assertEquals(true, isOn("YES")); assertEquals(true, isOn("on")); assertEquals(true, isOn("ON")); assertEquals(true, isOn("1")); }); Deno.test("formatHumanTime", () => { assertEquals("0ms", formatHumanTime(0)); assertEquals("1ms", formatHumanTime(1)); assertEquals("1s", formatHumanTime(1000)); assertEquals("1s", formatHumanTime(1001)); assertEquals("1m", formatHumanTime(60001)); assertEquals("1m 1s", formatHumanTime(61001)); assertEquals("1h", formatHumanTime(3600000)); assertEquals("1h 1s", formatHumanTime(3601000)); assertEquals("1h 1m 1s", formatHumanTime(3661000)); }); Deno.test("formatSize", () => { assertEquals("N/A", formatSize(-1)); assertEquals("0B", formatSize(0)); assertEquals("1B", formatSize(1)); assertEquals("1KiB", formatSize(1024)); assertEquals("1KiB 1B", formatSize(1024 + 1)); assertEquals("1MiB 1KiB 1B", formatSize(1024 * 1024 + 1024 + 1)); assertEquals( "1GiB 1MiB 1KiB 1B", formatSize(1024 * 1024 * 1024 + 1024 * 1024 + 1024 + 1), ); }); Deno.test("formatSize2", () => { assertEquals("N/A", formatSize2(-1)); assertEquals("0B", formatSize2(0)); assertEquals("1B", formatSize2(1)); assertEquals("1.00KiB", formatSize2(1024)); assertEquals("10.00KiB", formatSize2(1024 * 10)); assertEquals("1.00MiB", formatSize2(1024 * 1024)); }); Deno.test("formatPercent", () => { assertEquals("N/A", formatPercent(100, -1)); assertEquals("N/A", formatPercent(100, 0)); assertEquals("N/A", formatPercent(100, 0)); assertEquals("10.00%", formatPercent(10, 100)); assertEquals("11.00%", formatPercent(11, 100)); assertEquals("1.10%", formatPercent(11, 1000)); assertEquals("0.10%", formatPercent(1, 1000)); assertEquals("0.00%", formatPercent(1, 100000)); assertEquals("100.00%", formatPercent(100, 100)); }); Deno.test("sleep", async () => { const t1 = new Date().getTime(); await sleep(1000); const t2 = new Date().getTime(); assert(Math.abs(1000 - (t2 - t1)) < 20); }); Deno.test("base64Url", () => { assertEquals( "_dxhVwI3qd9fMBlpEMmi6Q", encodeBase64Url(decodeBase64Url("_dxhVwI3qd9fMBlpEMmi6Q")), ); assertEquals( "1dxJeD7erjAYUNEmdVNE8KdhpPZs0pAHtb-kbSqYIe5j039PkTHbrQYOEoeEWN4UsDERhnUg7mY", encodeBase64Url( decodeBase64Url( "1dxJeD7erjAYUNEmdVNE8KdhpPZs0pAHtb-kbSqYIe5j039PkTHbrQYOEoeEWN4UsDERhnUg7mY", ), ), ); }); Deno.test("test-key-ring-rs", () => { setKeyRingPassword("test-service", "test-user", "test-password"); assertEquals( "test-password", getKeyRingPassword("test-service", "test-user"), ); }); Deno.test("join-path", () => { assertEquals("a/b", joinPath("a/", "/b")); assertEquals("a/b", joinPath("a/", "b")); assertEquals("a/b", joinPath("a", "/b")); assertEquals("a/b", joinPath("a", "b")); assertEquals("a/b/c", joinPath("a", "b", "/c")); assertEquals("a/b/c", joinPath("a", "b", "c")); });