{"record":{"id":"3b1df06532eb551e","repo":"jackwener/OpenCLI","slug":"output-must-be-a-non-empty-directory-path","errorCode":null,"errorMessage":"output must be a non-empty directory path","messagePattern":"output must be a non-empty directory path","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel-download-utils.js","lineNumber":64,"sourceCode":"\nexport function normalizeNovelFileFormat(value) {\n  if (value !== undefined && typeof value !== 'string') {\n    throw new ArgumentError('Novel download format must be txt or md');\n  }\n  const format = (value ?? 'txt').toLowerCase();\n  if (format !== 'txt' && format !== 'md') {\n    throw new ArgumentError(`Unsupported novel download format: ${format}. Supported formats: txt, md.`);\n  }\n  return format;\n}\n\nexport function normalizePixivOutputRoot(value, fallback) {\n  if (value !== undefined && typeof value !== 'string') {\n    throw new ArgumentError('output must be a directory path');\n  }\n  const raw = value ?? fallback;\n  if (!raw || raw.includes('\\0')) {\n    throw new ArgumentError('output must be a non-empty directory path');\n  }\n  const resolved = path.resolve(raw);\n  let ancestor = resolved;\n  const missingParts = [];\n  let ancestorStat;\n  while (!ancestorStat) {\n    try {\n      ancestorStat = fs.lstatSync(ancestor);\n    } catch (error) {\n      if (error?.code !== 'ENOENT') {\n        throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);\n      }\n      const parent = path.dirname(ancestor);\n      if (parent === ancestor) {\n        throw new ArgumentError(`output path is not a safe directory: ${resolved}`);\n      }\n      missingParts.unshift(path.basename(ancestor));\n      ancestor = parent;","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel-download-utils.js#L46-L82","documentation":"After type-checking, normalizePixivOutputRoot resolves the output value against the fallback and rejects values that are empty (after nullish coalescing with the fallback) or that contain a NUL byte ('\\0'), which Node filesystem APIs reject. This guards against producing an unusable or malicious destination path.","triggerScenarios":"Calling normalizePixivOutputRoot(''), normalizePixivOutputRoot(null, ''), or a path containing an embedded NUL character such as 'out\\0dir'; also when both value and fallback are empty/undefined.","commonSituations":"Empty OUTPUT env var or shell option (-o \"\") that overrides the fallback; NUL bytes injected in a path from unsanitized user input or a corrupted config.","solutions":["Provide a non-empty directory path string, e.g. './pixiv-downloads/novels'.","If relying on the default, pass undefined (not '' or null with an empty fallback) so the fallback './pixiv-downloads/novels' applies.","Strip or reject NUL bytes from user-supplied paths before calling the API."],"exampleFix":"// before\nconst dir = process.env.OUTPUT ?? ''; // '' bypasses fallback\nnormalizePixivOutputRoot(dir);\n// after\nconst dir = process.env.OUTPUT || undefined;\nnormalizePixivOutputRoot(dir, './pixiv-downloads/novels');","handlingStrategy":"validation","validationCode":"const dir = output ?? fallback;\nif (typeof dir !== 'string' || dir.length === 0 || dir.includes('\\0')) {\n  throw new TypeError('output must be a non-empty path string without NUL bytes');\n}","typeGuard":"function isUsablePath(v) {\n  return typeof v === 'string' && v.length > 0 && !v.includes('\\0');\n}","tryCatchPattern":"try {\n  await downloadNovel(id, { output });\n} catch (e) {\n  if (/output must be a non-empty directory path/.test(e.message)) {\n    console.error('Set a valid --output directory, e.g. ./pixiv-downloads/novels');\n    process.exitCode = 2;\n  } else throw e;\n}","preventionTips":["Use || undefined instead of ?? '' for env-var paths so empty strings fall back to defaults.","Sanitize user-supplied paths to strip control characters and NUL bytes.","Give empty env/CLI overrides a sane default rather than an empty string."],"tags":["argument-validation","path","empty-value"],"backgroundTag":"invalid-path","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}