Repository: streamich/memfs (ts)

Issue:
`readdir` with `withFileTypes:true` incorrectly includes file name in `path`
The test here
https://github.com/streamich/memfs/blob/6f4e72e889b578a62110c2e57f79f78e9dbca408/src/__tests__/volume/readdirSync.test.ts#L108

is wrong - node.js actually doesn't include the file name in the path.

So the expectation should be
`{ mode: 33206, name: 'af1', path: '/z' },`



Pull request description:
fix: don't include filename in `path` when calling `readdir` with `withFileTypes: true`
This solves issue #1007 and is in large part the solution of @zzzzBuv though with some tests changed for conformance with node and readdir recursive fix.

src/dirent.path is only the path of the parent dir and dirent.name is either the filename+extension or the directory name. This is now reflected in readdir. Because readdir constructs the filenames from the readdir we join(path,name).

I had to change the test with readdir({withFileType: true}) because they were out of spec with node.

To test for conformance i used a simple script (node version v21.7.2)

```js
const fs = require('fs');
const path = require('path');

async function Test() {
  const dir = path.join(process.cwd());

  console.log('readdirSync(withFileTypes, recursive)');

  let res = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
  console.log(res);

  console.log('promise readdir(withFileTypes, recursive)');

  res = await fs.promises.readdir(dir, { withFileTypes: true, recursive: true });
  console.log(res);

  console.log('promise readdir(recursive)');

  res = fs.readdirSync(dir, { withFileTypes: false, recursive: true });
  console.log(res);
}

Test();
```
in the following file structure
![image](https://github.com/streamich/memfs/assets/46542964/db22a549-634e-43aa-9cb0-5e50640ef5db)

it results in the following log:

```
readdirSync(withFileTypes, recursive)
[
  Dirent {
    name: 'a',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 2
  },
  Dirent {
    name: 'rf1',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 1
  },
  Dirent {
    name: 'test.js',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 1
  },
  Dirent {
    name: '.af2',
    parentPath: '/home/jonas/src/temp/readdir-test/a',
    path: '/home/jonas/src/temp/readdir-test/a',
    [Symbol(type)]: 1
  },
  Dirent {
    name: 'af1',
    parentPath: '/home/jonas/src/temp/readdir-test/a',
    path: '/home/jonas/src/temp/readdir-test/a',
    [Symbol(type)]: 1
  }
]
promise readdir(withFileTypes, recursive)
[
  Dirent {
    name: 'a',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 2
  },
  Dirent {
    name: 'rf1',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 1
  },
  Dirent {
    name: 'test.js',
    parentPath: '/home/jonas/src/temp/readdir-test',
    path: '/home/jonas/src/temp/readdir-test',
    [Symbol(type)]: 1
  },
  Dirent {
    name: '.af2',
    parentPath: '/home/jonas/src/temp/readdir-test/a',
    path: '/home/jonas/src/temp/readdir-test/a',
    [Symbol(type)]: 1
  },
  Dirent {
    name: 'af1',
    parentPath: '/home/jonas/src/temp/readdir-test/a',
    path: '/home/jonas/src/temp/readdir-test/a',
    [Symbol(type)]: 1
  }
]
promise readdir(recursive)
[ 'a', 'rf1', 'test.js', 'a/.af2', 'a/af1' ]
```

as i can see the test reflect that.

Resolves #1007



Patch:
diff --git a/src/Dirent.ts b/src/Dirent.ts
index e92542ba..32cafd51 100644
--- a/src/Dirent.ts
+++ b/src/Dirent.ts
@@ -15,7 +15,7 @@ export class Dirent implements IDirent {
 
     dirent.name = strToEncoding(link.getName(), encoding);
     dirent.mode = mode;
-    dirent.path = link.getPath();
+    dirent.path = link.getParentPath();
 
     return dirent;
   }
diff --git a/src/node.ts b/src/node.ts
index c5419153..9df2ead2 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -409,6 +409,10 @@ export class Link extends EventEmitter {
     return this.steps.join(SEP);
   }
 
+  getParentPath(): string {
+    return this.steps.slice(0, -1).join(SEP);
+  }
+
   getName(): string {
     return this.steps[this.steps.length - 1];
   }
diff --git a/src/volume.ts b/src/volume.ts
index 4f289b07..32ff2c99 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -1496,7 +1496,11 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
 
     return list.map(dirent => {
       if (options.recursive) {
-        return dirent.path.replace(filename2 + pathModule.posix.sep, '');
+        let fullPath = pathModule.join(dirent.path, dirent.name.toString());
+        if (isWin) {
+          fullPath = fullPath.replace(/\\/g, '/');
+        }
+        return fullPath.replace(filename2 + pathModule.posix.sep, '');
       }
       return dirent.name;
     });


Test patch:
diff --git a/src/__tests__/volume/readdirSync.test.ts b/src/__tests__/volume/readdirSync.test.ts
index 23dd37e4..adf03fd2 100644
--- a/src/__tests__/volume/readdirSync.test.ts
+++ b/src/__tests__/volume/readdirSync.test.ts
@@ -67,9 +67,9 @@ describe('readdirSync()', () => {
       return { ...dirent };
     });
     expect(mapped).toEqual([
-      { mode: 33206, name: 'af', path: '/x/af' },
-      { mode: 16895, name: 'b', path: '/x/b' },
-      { mode: 16895, name: 'c', path: '/x/c' },
+      { mode: 33206, name: 'af', path: '/x' },
+      { mode: 16895, name: 'b', path: '/x' },
+      { mode: 16895, name: 'c', path: '/x' },
     ]);
   });
 
@@ -105,16 +105,16 @@ describe('readdirSync()', () => {
       })
       .sort((a, b) => a.path.localeCompare(b.path));
     expect(mapped).toEqual([
-      { mode: 33206, name: 'af1', path: '/z/af1' },
-      { mode: 33206, name: 'af2', path: '/z/af2' },
-      { mode: 16895, name: 'b', path: '/z/b' },
-      { mode: 33206, name: 'bf1', path: '/z/b/bf1' },
-      { mode: 33206, name: 'bf2', path: '/z/b/bf2' },
+      { mode: 33206, name: 'af1', path: '/z' },
+      { mode: 33206, name: 'af2', path: '/z' },
+      { mode: 16895, name: 'b', path: '/z' },
+      { mode: 16895, name: 'c', path: '/z' },
+      { mode: 33206, name: 'bf1', path: '/z/b' },
+      { mode: 33206, name: 'bf2', path: '/z/b' },
       { mode: 16895, name: 'c', path: '/z/c' },
-      { mode: 16895, name: 'c', path: '/z/c/c' },
-      { mode: 33206, name: '.cf0', path: '/z/c/c/.cf0' },
-      { mode: 33206, name: 'cf1', path: '/z/c/c/cf1' },
-      { mode: 33206, name: 'cf2', path: '/z/c/c/cf2' },
+      { mode: 33206, name: '.cf0', path: '/z/c/c' },
+      { mode: 33206, name: 'cf1', path: '/z/c/c' },
+      { mode: 33206, name: 'cf2', path: '/z/c/c' },
     ]);
   });
 });