Skip to content

fix: correct ENOTDIR vs ENOENT error when using file as directory#1147

Merged
streamich merged 3 commits into
masterfrom
copilot/fix-1050
Aug 1, 2025
Merged

fix: correct ENOTDIR vs ENOENT error when using file as directory#1147
streamich merged 3 commits into
masterfrom
copilot/fix-1050

Conversation

Copilot AI commented Aug 1, 2025

Copy link
Copy Markdown
Contributor

This PR fixes an issue where memfs throws ENOENT instead of the more semantically correct ENOTDIR when attempting to use a file as a directory on non-Windows platforms.

Problem

When trying to access a path that traverses through a file (e.g., /foo/bar where /foo is a file), memfs was throwing ENOENT: no such file or directory instead of ENOTDIR: not a directory. This behavior didn't match Node.js's platform-specific behavior:

// Before this fix
const { fs } = require('memfs');
fs.writeFileSync('/foo', 'hello');
fs.readFileSync('/foo/bar'); // throws ENOENT on all platforms

// Node.js behavior (what we should match)
const fs = require('node:fs');
fs.writeFileSync('/foo', 'hello');
fs.readFileSync('/foo/bar'); // throws ENOTDIR on Linux, ENOENT on Windows

Root Cause

The issue was in the walk method in src/core/Superblock.ts. When attempting to traverse through a non-directory, the error check was happening at the wrong time and wasn't accounting for symlink resolution.

Solution

  1. Fixed the traversal logic: Added a check after symlink resolution to detect when attempting to traverse through a non-directory.

  2. Added platform-specific behavior: Implemented platform-aware error codes to match Node.js behavior:

    • Non-Windows platforms: Throws ENOTDIR (more semantically correct)
    • Windows platform: Throws ENOENT (matches Node.js Windows behavior)
  3. Preserved compatibility: The check only applies to filesystem operations (when checkExistence=true), ensuring internal APIs and symlink traversals continue to work correctly.

Changes

  • src/core/Superblock.ts: Added platform-specific error handling after symlink resolution in the walk method
  • src/node/__tests__/volume/readFileSync-enotdir.test.ts: Added comprehensive tests for both platform behaviors

The fix ensures that all file operations (readFileSync, writeFileSync, statSync, etc.) correctly handle file-as-directory scenarios while maintaining backward compatibility for valid operations and symlink traversals.

Fixes #1050.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Using a file as a directory throws ENOENT instead of ENOTDIR fix: correct ENOTDIR vs ENOENT error when using file as directory Aug 1, 2025
Copilot AI requested a review from streamich August 1, 2025 15:32
@streamich streamich marked this pull request as ready for review August 1, 2025 18:42

@streamich streamich left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are failing:

PASS src/__tests__/setTimeoutUnref.test.ts

Summary of all failing tests
FAIL src/node/__tests__/volume.test.ts (6.046 s)
  ● volume › Volume › .getLink(steps) › ["dir", "child.sh"] - Get a child link in a dir

    ENOTDIR: not a directory,  '/dir/child.sh'

      146 |
      147 | export function createError(errorCode: string, func = '', path = '', path2 = '', Constructor = Error) {
    > 148 |   const error = new Constructor(formatError(errorCode, func, path, path2));
          |                 ^
      149 |   (error as any).code = errorCode;
      150 |
      151 |   if (path) {

      at createError (src/node/util.ts:148:17)
      at Superblock.walk (src/core/Superblock.ts:[226](https://github.com/streamich/memfs/actions/runs/16679004993/job/47224825085?pr=1147#step:5:227):28)
      at Superblock.getLink (src/core/Superblock.ts:259:17)
      at Object.<anonymous> (src/node/__tests__/volume.test.ts:60:33)

When done, make sure to run Prettier.

Copilot AI commented Aug 1, 2025

Copy link
Copy Markdown
Contributor Author

Tests are failing:

Fixed the test failures. The issue was that my ENOTDIR check was happening before symlink resolution, causing legitimate symlink traversals like /symlink/file.txt to fail. I moved the check to after symlink resolution and ensured it only applies to filesystem operations, not internal API calls. All tests now pass. (63b5cb3)

Copilot AI requested a review from streamich August 1, 2025 19:02
@streamich streamich merged commit 87d6bdd into master Aug 1, 2025
12 checks passed
@streamich streamich deleted the copilot/fix-1050 branch August 1, 2025 21:50
@github-actions

github-actions Bot commented Aug 1, 2025

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 4.33.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using a file as a directory throws ENOENT instead of ENOTDIR

2 participants