@cspell/cspell-tools
Version:
Tools to assist with the development of cSpell
150 lines • 6.21 kB
JavaScript
import { readFile, writeFile } from 'node:fs/promises';
import { resolve, sep as pathSep } from 'node:path';
import { toError } from '../util/errors.js';
import { isDefined } from '../util/index.js';
import { calcFileChecksum, checkFile } from './checksum.js';
export async function shasumFile(filename, root) {
try {
const file = resolve(root || '.', filename);
const checksum = await calcFileChecksum(file);
return `${checksum} ${filename}`;
}
catch {
// const err = toError(error);
// Reject with a string.
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
return Promise.reject(`shasum: ${filename}: Unable to read file.`);
}
}
/**
*
* @param filename - name of checksum file
* @param files - optional list of files to check
* @param root - optional root, default cwd.
*/
export async function checkShasumFile(filename, files, root) {
files = !files ? files : files.length ? files : undefined;
const shaFiles = await readAndParseShasumFile(filename);
const filesToCheck = !files ? shaFiles.map(({ filename }) => filename) : files;
const mapNameToChecksum = new Map(shaFiles.map((r) => [normalizeFilename(r.filename), r.checksum]));
const resolvedRoot = resolve(root || '.');
const results = await Promise.all(filesToCheck.map(normalizeFilename).map((filename) => {
return tryToCheckFile(filename, resolvedRoot, mapNameToChecksum.get(filename));
}));
const passed = !results.some((v) => !v.passed);
return { passed, results };
}
async function tryToCheckFile(filename, root, checksum) {
if (!checksum) {
return { filename, passed: false, error: new Error('Missing Checksum.') };
}
const file = resolve(root, filename);
try {
const passed = await checkFile(checksum, file);
return { filename, passed };
}
catch {
return { filename, passed: false, error: new Error('Failed to read file.') };
}
}
const regLine = /([a-f0-9]{40,}) {2}(.*)/;
export async function readAndParseShasumFile(filename) {
const content = await readFile(resolve(filename), 'utf8');
const shaFiles = parseShasumFile(content);
return shaFiles;
}
export function parseShasumFile(content) {
const lines = content.split(/\r?\n|\r/g);
return lines.map(parseLine).filter(isDefined);
function parseLine(line, index) {
const m = line.match(regLine);
const lineNumber = index + 1;
if (!m) {
if (line.trim()) {
throw new Error(`Failed to parse line ${lineNumber} of checksum file.`);
}
return undefined;
}
const checksum = m[1];
const filename = m[2];
return { checksum, filename, lineNumber };
}
}
export async function reportChecksumForFiles(files, options) {
const root = options.root;
const filesToCheck = await resolveFileList(files, options.listFile);
let numFailed = 0;
const result = await Promise.all(filesToCheck.map((file) => shasumFile(file, root).catch((e) => {
++numFailed;
if (typeof e !== 'string')
throw e;
return e;
})));
const report = result.join('\n');
const passed = !numFailed;
return { report, passed };
}
export async function reportCheckChecksumFile(filename, files, options) {
const root = options.root;
const filesToCheck = await resolveFileList(files, options.listFile);
const checkResult = await checkShasumFile(filename, filesToCheck, root);
const results = checkResult.results;
const lines = results.map(({ filename, passed, error }) => `${filename}: ${passed ? 'OK' : 'FAILED'} ${error ? '- ' + error.message : ''}`.trim());
const withErrors = results.filter((a) => !a.passed);
const passed = !withErrors.length;
if (!passed) {
lines.push(`shasum: WARNING: ${withErrors.length} computed checksum${withErrors.length > 1 ? 's' : ''} did NOT match`);
}
return { report: lines.join('\n'), passed };
}
async function resolveFileList(files, listFile) {
files = files || [];
listFile = listFile || [];
const setOfFiles = new Set(files);
const pending = listFile.map((filename) => readFile(filename, 'utf8'));
for await (const content of pending) {
content
.split('\n')
.map((a) => a.trim())
.filter((a) => a)
.forEach((file) => setOfFiles.add(file));
}
return [...setOfFiles].map(normalizeFilename);
}
export async function calcUpdateChecksumForFiles(filename, files, options) {
const root = options.root || '.';
const filesToCheck = await resolveFileList(files, options.listFile);
const currentEntries = (await readAndParseShasumFile(filename).catch((err) => {
const e = toError(err);
if (e.code !== 'ENOENT')
throw e;
return [];
})).map((entry) => ({ ...entry, filename: normalizeFilename(entry.filename) }));
const entriesToUpdate = new Set([...filesToCheck, ...currentEntries.map((e) => e.filename)]);
const mustExist = new Set(filesToCheck);
const checksumMap = new Map(currentEntries.map(({ filename, checksum }) => [filename, checksum]));
for (const file of entriesToUpdate) {
try {
const checksum = await calcFileChecksum(resolve(root, file));
checksumMap.set(file, checksum);
}
catch (e) {
if (mustExist.has(file) || toError(e).code !== 'ENOENT')
throw e;
checksumMap.delete(file);
}
}
const updatedEntries = [...checksumMap]
.map(([filename, checksum]) => ({ filename, checksum }))
.sort((a, b) => (a.filename < b.filename ? -1 : 1));
return updatedEntries.map((e) => `${e.checksum} ${e.filename}`).join('\n') + '\n';
}
export async function updateChecksumForFiles(filename, files, options) {
const content = await calcUpdateChecksumForFiles(filename, files, options);
await writeFile(filename, content);
return { passed: true, report: content };
}
function normalizeFilename(filename) {
return filename.split(pathSep).join('/');
}
//# sourceMappingURL=shasum.js.map