import { readKey } from "openpgp"; export const armored: unique symbol = Symbol(); export const binary: unique symbol = Symbol(); export type KeyFileFormat = typeof armored | typeof binary; export interface KeyDiscoveryRules { formats?: Partial | undefined>>; recursive?: boolean | number; } export const DEFAULT_KEY_DISCOVERY_RULES = { formats: { [armored]: new Set(["asc"]), [binary]: new Set(["gpg"]), }, } satisfies KeyDiscoveryRules; export async function* createKeysFromFs( key: string | URL, rules: KeyDiscoveryRules = DEFAULT_KEY_DISCOVERY_RULES, coders: { decoder?: TextDecoder; encoder?: TextEncoder } = {}, ): AsyncGenerator>, void, void> { key = new URL(key); validateKeyDiscoveryRules(rules); const stat = await Deno.stat(key); if (stat.isDirectory) { const generator = createKeysFromDir(key, rules, coders); yield* generator; } else if (stat.isFile) { const period = key.pathname.lastIndexOf("."); const ext = period === -1 ? "" : key.pathname.slice(period + 1); if ( rules.formats?.[armored] !== undefined && rules.formats[armored].has(ext) ) { yield createKeyFromFile( key, armored, coders?.decoder, ); } else if ( rules.formats?.[binary] !== undefined && rules.formats[binary].has(ext) ) { yield createKeyFromFile( key, binary, coders?.encoder, ); } } } export async function* createKeysFromDir( key: string | URL, rules: KeyDiscoveryRules = DEFAULT_KEY_DISCOVERY_RULES, coders: { decoder?: TextDecoder; encoder?: TextEncoder } = {}, ): AsyncGenerator>, void, void> { key = new URL(key); validateKeyDiscoveryRules(rules); for await (const dirEntry of Deno.readDir(key)) { const filePath = new URL(dirEntry.name, key); if (dirEntry.isFile) { const period = filePath.pathname.lastIndexOf("."); const ext = period === -1 ? "" : filePath.pathname.slice(period + 1); if ( rules.formats?.[armored] !== undefined && rules.formats[armored].has(ext) ) { yield createKeyFromFile( filePath, armored, coders?.decoder, ); } else if ( rules.formats?.[binary] !== undefined && rules.formats[binary].has(ext) ) { yield createKeyFromFile( filePath, binary, coders?.encoder, ); } } else if (dirEntry.isDirectory) { const depth = typeof rules.recursive === "number" ? rules.recursive : rules.recursive ? Infinity : 0; if (depth > 0) { yield* createKeysFromDir(filePath, { ...rules, recursive: depth - 1, }, coders); } } } } export async function createKeyFromFile( key: string | URL, type: typeof armored, coder?: TextDecoder, ): ReturnType; export async function createKeyFromFile( key: string | URL, type: typeof binary, coder?: TextEncoder, ): ReturnType; export async function createKeyFromFile( key: string | URL, type: typeof armored | typeof binary, coder?: TextDecoder | TextEncoder, ): ReturnType { switch (type) { case armored: return await Deno.readTextFile(key).then((key) => createKeyFromArmor(key, coder as TextDecoder) ); case binary: return await Deno.readFile(key).then((key) => createKeyFromBinary(key, coder as TextEncoder) ); } } export function createKeyFromArmor( key: string | Uint8Array, decoder?: TextDecoder, ): ReturnType { return readKey({ armoredKey: typeof key === "string" ? key : (decoder ?? new TextDecoder()).decode(key), }); } export function createKeyFromBinary( key: string | Uint8Array, encoder?: TextEncoder, ): ReturnType { return readKey({ binaryKey: typeof key === "string" ? (encoder ?? new TextEncoder()).encode(key) : key, }); } function validateKeyDiscoveryRules(rules: KeyDiscoveryRules) { let disjoint = true; let union: Set | undefined = undefined; const keys = rules.formats !== undefined ? Object.getOwnPropertySymbols(rules.formats) as KeyFileFormat[] : []; for (const i of keys) { const set = rules.formats?.[i]; if (union === undefined) { union = set; continue; } if (set === undefined) { continue; } disjoint &&= union.isDisjointFrom(set); union = union.union(set); if (!disjoint) { break; } } if (!disjoint) { throw new Error( `\`Set\`s from \`rules.formats\` aren't disjoint`, ); } }