Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | 2811x 2811x 2135x 2135x 988x 288x 288x 857x 857x 857x 1x 233x 27x 102x 101x 101x 75x 74x 74x 74x 74x 74x 974x 953x 953x 953x 1333x 4x 70x 70x 70x 260x 70x 74x 167x 5x 65x 69x 65x 65x 65x 65x 65x 523x 523x 65x 65x 249x 249x 64x 62x 62x 61x 61x 61x 61x 61x 308x 308x 392x 61x 157x 159x 159x 159x 61x 308x 308x 306x 306x 303x 58x 58x 514x 58x 7x 9x 7x 7x 7x 7x 7x 9x 9x 9x 9x 7x 7x 2x 2x 7x 65x | import { constants } from "node:fs";
import {
chmod,
copyFile,
lstat,
mkdir,
mkdtemp,
realpath,
rm,
rmdir,
unlink,
writeFile,
} from "node:fs/promises";
import { dirname, isAbsolute, parse, relative, resolve, sep } from "node:path";
import { stageReportAssets, type ReportAsset } from "./report-assets.js";
function contains(parent: string, child: string): boolean {
const path = relative(parent, child);
return (
path === "" ||
(!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path))
);
}
async function stat(path: string) {
try {
return await lstat(path);
} catch (error) {
Eif ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
throw error;
}
}
/** Reject symlinks in every existing path component, including dangling links. */
async function checkParents(path: string): Promise<void> {
let current = parse(path).root;
for (const part of relative(current, path).split(sep).filter(Boolean)) {
current = resolve(current, part);
const info = await stat(current);
if (info && (info.isSymbolicLink() || !info.isDirectory()))
throw new Error(`Unsafe output path component: ${current}`);
}
}
async function requireAbsent(output: string): Promise<void> {
if (await stat(output))
throw new Error(
`Output already exists: ${output}. Remove it yourself before regenerating the report.`,
);
}
/** Create only new output. Exclusive creation never replaces an existing destination. */
export async function writeReportOutput(
root: string,
requested: string,
protectedInputs: readonly string[],
files: ReadonlyMap<string, string>,
assets: readonly ReportAsset[] = [],
): Promise<string> {
if (!requested.trim()) throw new Error("Output directory must not be empty");
const requestedOutput = resolve(root, requested);
await requireAbsent(requestedOutput);
await checkParents(requestedOutput);
// Normalize existing parent aliases (including Windows 8.3 paths) only after
// rejecting symlink components; append the still-absent destination suffix.
let ancestor = dirname(requestedOutput);
while (!(await stat(ancestor))) ancestor = dirname(ancestor);
const output = resolve(
await realpath(ancestor),
relative(ancestor, requestedOutput),
);
Iif (output === parse(output).root || contains(output, root))
throw new Error(
`Unsafe output ${output}: must not be the filesystem root, project root, or an ancestor of the project`,
);
const protectedPaths = [
"src",
"lib",
"test",
"tests",
"docs",
"node_modules",
".git",
"allure-results",
"coverage",
"allure-report",
...protectedInputs,
];
for (const input of protectedPaths.map((path) => resolve(root, path))) {
const paths = [input];
if (await stat(input)) paths.push(await realpath(input));
for (const path of paths)
if (contains(output, path) || contains(path, output))
throw new Error(
`Unsafe output ${output}: overlaps protected input ${path}`,
);
}
await requireAbsent(output);
await checkParents(output);
for (const file of files.keys()) {
Iif (
file !== "index.html" &&
file !== "moura/index.html" &&
!/^moura\/sources\/source-[a-f0-9]{64}\.html$/u.test(file)
)
throw new Error(`Invalid report file: ${file}`);
}
for (const asset of assets) {
if (
!/^(allure|coverage)\//u.test(asset.destination) ||
!contains(
resolve(output, asset.destination.split("/")[0]!),
resolve(output, asset.destination),
) ||
asset.destination
.split(process.platform === "win32" ? /[/\\]/u : "/")
.some((part) => !part || part === "." || part === "..")
)
throw new Error(`Invalid report asset: ${asset.destination}`);
}
const destinations = [
...files.keys(),
...assets.map((asset) => asset.destination),
];
await mkdir(dirname(output), { recursive: true });
await checkParents(dirname(output));
const stage = await mkdtemp(resolve(dirname(output), ".moura-stage-"));
const created: {
path: string;
ino: number;
dev: number;
directory: boolean;
}[] = [];
const remember = async (path: string, directory: boolean) => {
const info = await lstat(path);
created.push({ path, ino: info.ino, dev: info.dev, directory });
};
try {
for (const [file, contents] of files) {
await mkdir(dirname(resolve(stage, file)), {
recursive: true,
mode: 0o700,
});
await writeFile(resolve(stage, file), contents, {
flag: "wx",
mode: 0o600,
});
}
await stageReportAssets(stage, assets);
await checkParents(output);
await requireAbsent(output);
try {
await mkdir(output, { mode: 0o700 });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "EEXIST")
await requireAbsent(output);
throw error;
}
await remember(output, true);
const directories = new Set<string>();
for (const file of destinations) {
const parts = file.split("/").slice(0, -1);
for (let count = 1; count <= parts.length; count++)
directories.add(parts.slice(0, count).join("/"));
}
for (const directory of [...directories].sort(
(a, b) => a.split("/").length - b.split("/").length,
)) {
const path = resolve(output, directory);
await mkdir(path, { mode: 0o700 });
await remember(path, true);
}
// Publish the Overview last so interrupted copying has no normal entry point.
for (const file of [
...destinations.filter((file) => file !== "index.html"),
"index.html",
].filter((file) => destinations.includes(file))) {
const path = resolve(output, file);
await copyFile(resolve(stage, file), path, constants.COPYFILE_EXCL);
await remember(path, false);
}
Eif (process.platform !== "win32") {
for (const entry of [...created].reverse())
await chmod(
entry.path,
(entry.directory ? 0o777 : 0o666) & ~process.umask(),
);
}
return output;
} catch (error) {
// Only remove objects created by this invocation, never competing user data.
Eif (process.platform !== "win32") {
for (const entry of created.filter((entry) => entry.directory)) {
await checkParents(dirname(entry.path));
const info = await stat(entry.path);
Eif (
info?.isDirectory() &&
info.ino === entry.ino &&
info.dev === entry.dev
)
await chmod(entry.path, info.mode | 0o700);
}
}
for (const entry of [...created].reverse()) {
await checkParents(dirname(entry.path));
const info = await stat(entry.path);
Iif (
!info ||
info.isSymbolicLink() ||
info.ino !== entry.ino ||
info.dev !== entry.dev
)
continue;
if (entry.directory) {
try {
await rmdir(entry.path);
} catch (cleanupError) {
Iif (
!["ENOTEMPTY", "EEXIST"].includes(
(cleanupError as NodeJS.ErrnoException).code ?? "",
)
)
throw cleanupError;
}
} else await unlink(entry.path);
}
throw error;
} finally {
// This private temporary directory is never a user-selected output.
await rm(stage, { recursive: true, force: true });
}
}
|