{"record":{"id":"3d34ec74e3a08a49","repo":"can1357/oh-my-pi","slug":"archive-path-normalizedpath-is-a-directory","errorCode":null,"errorMessage":"Archive path '${normalizedPath}' is a directory","messagePattern":"Archive path '(.+?)' is a directory","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/reader.ts","lineNumber":135,"sourceCode":"\t\t\t\tmode: childEntry?.mode ?? entry.mode,\n\t\t\t});\n\t\t}\n\n\t\treturn [...children.values()].sort((left, right) =>\n\t\t\tleft.name.toLowerCase().localeCompare(right.name.toLowerCase()),\n\t\t);\n\t}\n\n\t/** Extract one file member's bytes, following symlink aliases. */\n\tasync readFile(subPath: string): Promise<ExtractedArchiveFile> {\n\t\tconst normalizedPath = normalizeArchiveLookupPath(subPath);\n\t\tif (!normalizedPath) {\n\t\t\tthrow new ArchiveError(\"Archive file path is required\");\n\t\t}\n\n\t\tconst resolvedPath = resolveArchiveLinkPath(this.#entries, normalizedPath, this.limits.maxLinkDepth);\n\t\tif (resolvedPath === \"\") {\n\t\t\tthrow new ArchiveError(`Archive path '${normalizedPath}' is a directory`);\n\t\t}\n\t\tconst entry = this.#entries.get(resolvedPath);\n\t\tif (!entry) {\n\t\t\tthrow new ArchiveError(`Archive file '${normalizedPath}' not found`);\n\t\t}\n\t\tif (entry.isDirectory) {\n\t\t\tthrow new ArchiveError(`Archive path '${normalizedPath}' is a directory`);\n\t\t}\n\t\tif (!entry.storage) {\n\t\t\tthrow new ArchiveError(`Archive file '${normalizedPath}' has no readable storage`);\n\t\t}\n\t\tassertArchiveMemberSize(entry.size, normalizedPath, this.limits);\n\n\t\tif (entry.storage.type === \"link\") {\n\t\t\tthrowUnreadableArchiveLink(entry.storage.targetPath, normalizedPath);\n\t\t}\n\t\tconst bytes = await entry.storage.source.read(entry.size, normalizedPath);\n\t\treturn {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/reader.ts#L117-L153","documentation":"`readFile` resolved the requested path and found a matching entry, but that entry is a directory — either directly, or because a symlink chain ended at a directory. It throws ArchiveError(`Archive path '${normalizedPath}' is a directory`) since byte extraction only applies to file members (packages/utils/src/ar/reader.ts:135).","triggerScenarios":"Calling `reader.readFile('src')` where 'src' is a directory entry; calling readFile on a symlink that resolves to a directory; passing a path without a filename; mixing up readFile and listDirectory targets.","commonSituations":"Recursive walkers that must read 'everything' and hit directory entries; user supplies a folder instead of a file path; archives where trailing-slash-less directory names look like files (e.g. entry 'data' being a folder).","solutions":["Verify the entry is a file first: check `isDirectory === false` on the matching entry from allEntries(), then readFile it.","To read a whole folder, enumerate it with listDirectory(dir) and call readFile on each file child.","If a trailing component is missing, append the actual file name to the path.","Catch this ArchiveError and switch to directory listing rather than failing the whole operation."],"exampleFix":"// before: assumes every path is a file\nfor (const p of paths) results.push(await reader.readFile(p));\n\n// after: skip or expand directories\nfor (const p of paths) {\n  const e = reader.allEntries().find(x => x.path === p);\n  if (e?.isDirectory) {\n    for (const child of reader.listDirectory(p)) {\n      if (!child.isDirectory) results.push(await reader.readFile(child.path));\n    }\n  } else {\n    results.push(await reader.readFile(p));\n  }\n}","handlingStrategy":"type-guard","validationCode":"// Only pass file entries to readFile\nconst entry = reader.allEntries().find(e => e.path === targetPath);\nif (!entry || entry.isDirectory) {\n  throw new Error(`${targetPath} is not a file in the archive`);\n}\nawait reader.readFile(targetPath);","typeGuard":"function isFileEntry(\n  entries: { path: string; isDirectory: boolean }[],\n  p: string,\n): entries is { path: string; isDirectory: boolean }[] & { fileFound: true } {\n  const e = entries.find(x => x.path === p);\n  return e !== undefined && !e.isDirectory;\n}","tryCatchPattern":"try {\n  return await reader.readFile(p);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.endsWith('is a directory')) {\n    const children = reader.listDirectory(p);\n    logger.warn('Requested archive path is a directory; expand it explicitly', { p, children: children.length });\n    return null;\n  }\n  throw err;\n}","preventionTips":["Check isDirectory before calling readFile; enumerate directories with listDirectory instead.","Include an explicit file name in paths — never point readFile at a folder.","For recursive extraction, walk listDirectory results and readFile only non-directory children.","Handle symlink entries by checking the final resolved entry's isDirectory flag."],"tags":["archive","wrong-type","directory"],"backgroundTag":"not-a-file","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}