/* * SPDX-FileCopyrightText: 2024 KindSpells Labs S.L. * * SPDX-License-Identifier: MIT */ import { createHash } from 'node:crypto' import { readFile, readdir, stat, writeFile } from 'node:fs/promises' import { extname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' /** * @param {string | ArrayBuffer | Buffer} data * @returns {string} */ const generateSRIHash = data => { const hash = createHash('sha256') if (data instanceof ArrayBuffer) { hash.update(Buffer.from(data)) } else if (data instanceof Buffer) { hash.update(data) } else { hash.update(data, 'utf8') } return `sha256-${hash.digest('base64')}` } /** * @param {RegExp} elemRegex * @param {string} content * @param {Set} inlineHashes */ const extractKnownSriHashes = (elemRegex, content, inlineHashes) => { let match // biome-ignore lint/suspicious/noAssignInExpressions: safe while ((match = elemRegex.exec(content)) !== null) { const sriHash = match[1] if (sriHash) { inlineHashes.add(sriHash) } } } /** * @param {'script' | 'style'} elemType * @param {string} content * @param {Set} inlineHashes * @returns {string} */ const updateInlineSriHashes = (elemType, content, inlineHashes) => { let updatedContent = content let match const elemRegex = new RegExp(`<${elemType}>([\\s\\S]*?)<\\/${elemType}>`, 'g') // biome-ignore lint/suspicious/noAssignInExpressions: safe while ((match = elemRegex.exec(content)) !== null) { const elemContent = match[1]?.trim() if (elemContent) { const sriHash = generateSRIHash(elemContent) updatedContent = updatedContent.replace( match[0], `<${elemType} integrity="${sriHash}">${elemContent}`, ) inlineHashes.add(sriHash) } } return updatedContent } /** * @param {import('astro').AstroIntegrationLogger} logger * @param {string} distDir * @param {'script' | 'style'} elemType * @param {string} content * @param {Set} extHashes * @returns {Promise} */ const updateExternalSriHashes = async ( logger, distDir, elemType, content, extHashes, ) => { let updatedContent = content let match const elemRegex = elemType === 'script' ? /\s+(type="module"\s+src="(?[\s\S]*?)"|src="(?[\s\S]*?)"\s+type="module"|src="(?[\s\S]*?)"))\s*(\/>|><\/script>)/gi : /\s+(rel="stylesheet"\s+href="(?[\s\S]*?)"|href="(?[\s\S]*?)"\s+rel="stylesheet"))\s*\/>/gi // biome-ignore lint/suspicious/noAssignInExpressions: safe while ((match = elemRegex.exec(content)) !== null) { const attrs = match.groups?.attrs const href = match.groups?.href1 ?? match.groups?.href2 ?? match.groups?.href3 if (!attrs || !href) { continue } /** @type {string | ArrayBuffer | Buffer} */ let resourceContent if (href.startsWith('/')) { const resourcePath = resolve(distDir, `.${href}`) resourceContent = await readFile(resourcePath) } else if (href.startsWith('http')) { const resourceResponse = await fetch(href, { method: 'GET' }) resourceContent = await resourceResponse.arrayBuffer() } else { logger.warn(`Unable to process external resource: "${href}"`) continue } const sriHash = generateSRIHash(resourceContent) updatedContent = updatedContent.replace( match[0], elemType === 'script' ? `` : ``, ) extHashes.add(sriHash) } return updatedContent } /** * @param {import('astro').AstroIntegrationLogger} logger * @param {string} filePath * @param {string} distDir */ const processHTMLFile = async (logger, filePath, distDir) => { const content = await readFile(filePath, 'utf8') const inlineScriptHashes = /** @type {Set} */ (new Set()) const inlineStyleHashes = /** @type {Set} */ (new Set()) const extScriptHashes = /** @type {Set} */ (new Set()) const extStyleHashes = /** @type {Set} */ (new Set()) // Known Inline Resources (just a precaution) extractKnownSriHashes( /