{"version":3,"file":"cleanWorkingDirectory.cjs","names":["path","forceRemove","fs","sandboxUserName"],"sources":["../../src/helpers/cleanWorkingDirectory.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { forceRemove, relaxPermissionsAsSandboxUser, sandboxUserName } from './sandboxUser.js';\n\n/** Snapshot of the working directory: each entry's relative path and the kind of entry it was. */\nexport type WorkingDirectorySnapshot = ReadonlyMap<string, WorkingDirectoryEntryKind>;\n\ntype WorkingDirectoryEntryKind = 'directory' | 'file' | 'symlink' | 'other';\n\n// Currently, it does not support changing file contents and deleting files.\nexport async function snapshotWorkingDirectory(cwd: string): Promise<WorkingDirectorySnapshot> {\n  const snapshot = new Map<string, WorkingDirectoryEntryKind>();\n  await collectEntries(cwd, cwd, snapshot);\n  return snapshot;\n}\n\nexport async function cleanWorkingDirectory(cwd: string, snapshot: WorkingDirectorySnapshot): Promise<void> {\n  await removeUnsnapshotted(cwd, cwd, snapshot);\n}\n\nasync function collectEntries(root: string, dir: string, into: Map<string, WorkingDirectoryEntryKind>): Promise<void> {\n  for (const entry of await readdirWithTypes(dir)) {\n    const absolutePath = path.join(dir, entry.name);\n    into.set(path.relative(root, absolutePath), toEntryKind(entry));\n    // Never descend into a symlink: a sandboxed submission could point it outside the directory.\n    if (entry.isDirectory()) await collectEntries(root, absolutePath, into);\n  }\n}\n\nasync function removeUnsnapshotted(root: string, dir: string, snapshot: WorkingDirectorySnapshot): Promise<void> {\n  for (const entry of await readdirWithTypes(dir)) {\n    const absolutePath = path.join(dir, entry.name);\n    const snapshottedKind = snapshot.get(path.relative(root, absolutePath));\n    // The kind must still match: a sandboxed submission can replace one of its own snapshotted\n    // entries with a symlink, and matching by path alone would preserve it across test cases.\n    if (snapshottedKind === undefined || snapshottedKind !== toEntryKind(entry)) {\n      // `absolutePath` has no symlink ancestor (we only recurse into real directories), and\n      // `forceRemove`/`fs.rm` unlinks a leaf symlink instead of following it, so this cannot delete\n      // outside the working directory even if the submission planted symlinks.\n      await forceRemove(absolutePath);\n      continue;\n    }\n    if (entry.isDirectory()) await removeUnsnapshotted(root, absolutePath, snapshot);\n  }\n}\n\nfunction toEntryKind(entry: fs.Dirent): WorkingDirectoryEntryKind {\n  if (entry.isSymbolicLink()) return 'symlink';\n  if (entry.isDirectory()) return 'directory';\n  if (entry.isFile()) return 'file';\n  return 'other';\n}\n\n/**\n * `readdir` with entry types (which reflect `lstat`, so a symlink reads as a symlink, not a\n * directory), with a fallback for directories a sandboxed process made untraversable.\n */\nasync function readdirWithTypes(dir: string): Promise<fs.Dirent[]> {\n  try {\n    return await fs.promises.readdir(dir, { withFileTypes: true });\n  } catch (error) {\n    if (!sandboxUserName) throw error;\n    relaxPermissionsAsSandboxUser(dir);\n    return await fs.promises.readdir(dir, { withFileTypes: true });\n  }\n}\n"],"mappings":"uLAWA,eAAsB,EAAyB,EAAgD,CAC7F,IAAM,EAAW,IAAI,IAErB,OADA,MAAM,EAAe,EAAK,EAAK,CAAQ,EAChC,CACT,CAEA,eAAsB,EAAsB,EAAa,EAAmD,CAC1G,MAAM,EAAoB,EAAK,EAAK,CAAQ,CAC9C,CAEA,eAAe,EAAe,EAAc,EAAa,EAA6D,CACpH,IAAK,IAAM,KAAS,MAAM,EAAiB,CAAG,EAAG,CAC/C,IAAM,EAAeA,EAAAA,QAAK,KAAK,EAAK,EAAM,IAAI,EAC9C,EAAK,IAAIA,EAAAA,QAAK,SAAS,EAAM,CAAY,EAAG,EAAY,CAAK,CAAC,EAE1D,EAAM,YAAY,GAAG,MAAM,EAAe,EAAM,EAAc,CAAI,CACxE,CACF,CAEA,eAAe,EAAoB,EAAc,EAAa,EAAmD,CAC/G,IAAK,IAAM,KAAS,MAAM,EAAiB,CAAG,EAAG,CAC/C,IAAM,EAAeA,EAAAA,QAAK,KAAK,EAAK,EAAM,IAAI,EACxC,EAAkB,EAAS,IAAIA,EAAAA,QAAK,SAAS,EAAM,CAAY,CAAC,EAGtE,GAAI,IAAoB,IAAA,IAAa,IAAoB,EAAY,CAAK,EAAG,CAI3E,MAAMC,EAAAA,YAAY,CAAY,EAC9B,QACF,CACI,EAAM,YAAY,GAAG,MAAM,EAAoB,EAAM,EAAc,CAAQ,CACjF,CACF,CAEA,SAAS,EAAY,EAA6C,CAIhE,OAHI,EAAM,eAAe,EAAU,UAC/B,EAAM,YAAY,EAAU,YAC5B,EAAM,OAAO,EAAU,OACpB,OACT,CAMA,eAAe,EAAiB,EAAmC,CACjE,GAAI,CACF,OAAO,MAAMC,EAAAA,QAAG,SAAS,QAAQ,EAAK,CAAE,cAAe,EAAK,CAAC,CAC/D,OAAS,EAAO,CACd,GAAI,CAACC,EAAAA,gBAAiB,MAAM,EAE5B,OADA,EAAA,8BAA8B,CAAG,EAC1B,MAAMD,EAAAA,QAAG,SAAS,QAAQ,EAAK,CAAE,cAAe,EAAK,CAAC,CAC/D,CACF"}