denoland / deno

A modern runtime for JavaScript and TypeScript.
https://deno.com
MIT License
93.5k stars 5.19k forks source link

ctime value differs from node in windows deno #24453

Open fushihara opened 2 weeks ago

fushihara commented 2 weeks ago

I am writing software to display timestamps of files. I am porting a program that was running on node to deno, and I noticed that the ctime value is different between node and deno. I think this is a windows NTFS specific problem.

The test code is as follows Please specify a file that was created long ago and edited many times, not a new file.

import { format } from "jsr:@std/datetime@0.224.0/format"
import { stat, Stats } from "node:fs"
const fromFile = "ANYFILE";
const localFileStat = await Deno.stat(fromFile)
const nodeStat =await  new Promise<Stats>((resolve, reject) => {
  stat(fromFile, (err, stat) => {
    if (err) {
      reject(err);
    } else {
      resolve(stat);
    }
  });
});
console.log(`${format(nodeStat.ctime, "yyyy-MM-dd HH:mm:ss:SSS")} node:ctime`);
console.log(`${format(nodeStat.mtime, "yyyy-MM-dd HH:mm:ss:SSS")} node:mtime`);
console.log(`${format(nodeStat.atime, "yyyy-MM-dd HH:mm:ss:SSS")} node:atime`);
console.log(`${format(nodeStat.birthtime, "yyyy-MM-dd HH:mm:ss:SSS")} node:birthtime`);

console.log(`${format(localFileStat.atime!, "yyyy-MM-dd HH:mm:ss:SSS")} Deno:atime`);
console.log(`${format(localFileStat.birthtime!, "yyyy-MM-dd HH:mm:ss:SSS")} Deno:birthtime`);
console.log(`${format(localFileStat.mtime!, "yyyy-MM-dd HH:mm:ss:SSS")} Deno:mtime`);

The results are as follows. I found it strange that the ctime and mtime of node are exactly the same value.

2024-07-06 17:52:28:857 node:ctime
2024-07-06 17:52:28:857 node:mtime
2024-07-06 21:27:15:750 node:atime
2024-07-06 17:52:21:438 node:birthtime

2024-07-06 21:27:15:750 Deno:atime
2024-07-06 17:52:21:438 Deno:birthtime
2024-07-06 17:52:28:857 Deno:mtime

Looking at the Deno source code, ctime refers to the exact same value as mtime. https://github.com/denoland/deno/blob/main/ext/node/polyfills/_fs/_fs_stat.ts#L293

Here is the result of running the equivalent code in node The mtime and ctime values are different.

2024-07-06 17:52:43:446 node:ctime
2024-07-06 17:52:28:857 node:mtime
2024-07-06 21:27:15:751 node:atime
2024-07-06 17:52:21:438 node:birthtime

The following is the result of displaying the timestamps with SetMace, a tool that displays Windows file system details. The decimal point of milliseconds differs, but we consider this to be no problem within the range of rounding error.

$ SetMace.exe "ANYFILE" -d
STANDARD_INFORMATION
CreationTime: 2024-07-06 08:52:21:438:1243
LastWriteTime: 2024-07-06 08:52:28:857:4623
ChangeTime(MFT): 2024-07-06 08:52:43:445:7160
LastAccessTime: 2024-07-06 12:27:15:750:6429

The ctime value in nodejs refers to the date in ChangeTime (MFT). We expect to get the equivalent value in deno's node:fs package. I would also like to add ctime to the FileInfo type in the return value of Deno.stat(fromFile).

Checking the official Deno compatibility list, there is no mention of stat in the fs section. image https://docs.deno.com/runtime/manual/node/compatibility/#:~:text=node%3Afs,-%E2%84%B9%EF%B8%8F

SetMace is the repository for https://github.com/jschicht/SetMace

The versions of deno and node are as follows

>deno --version
deno 1.44.4 (release, x86_64-pc-windows-msvc)
v8 12.6.228.9
typescript 5.4.5

>node --version
v22.4.0
fushihara commented 2 weeks ago

In node, the last four constructors of the BigIntStats class each store a different timestamp value. https://github.com/nodejs/node/blob/main/lib/internal/fs/utils.js#L565

I assume the process is called from the following location I couldn't understand the code from c++. https://github.com/nodejs/node/blob/main/lib/fs.js#L1661 https://github.com/nodejs/node/blob/main/src/node_file.cc#L1203

felipecrs commented 2 weeks ago

Not only on Windows. It seems that Deno is simply copying the value of mtime to ctime. Note that ctime isn't even available in Deno's native stat:

https://deno.land/api@v1.44.4?s=Deno.FileInfo

❯ deno eval 'console.log(Deno.statSync("/home/felipecrs/.local/share/pkgx/pantry/projects"))'
{
  isFile: false,
  isDirectory: true,
  isSymlink: false,
  size: 28672,
  mtime: 2024-07-05T15:16:48.000Z,
  atime: 2024-07-06T21:48:07.873Z,
  birthtime: 2024-04-04T17:43:10.927Z,
  dev: 2064,
  ino: 175481,
  mode: 16877,
  nlink: 728,
  uid: 1000,
  gid: 1000,
  rdev: 0,
  blksize: 4096,
  blocks: 64,
  isBlockDevice: false,
  isCharDevice: false,
  isFifo: false,
  isSocket: false
}

❯ deno eval 'console.log((await import("node:fs")).statSync("/home/felipecrs/.local/share/pkgx/pantry/projects"))'
Stats {
  dev: 2064,
  ino: 175481,
  mode: 16877,
  nlink: 728,
  uid: 1000,
  gid: 1000,
  rdev: 0,
  size: 28672,
  blksize: 4096,
  blocks: 64,
  mtime: 2024-07-05T15:16:48.000Z,
  atime: 2024-07-06T21:48:07.873Z,
  birthtime: 2024-04-04T17:43:10.927Z,
  mtimeMs: 1720192608000,
  atimeMs: 1720302487873,
  birthtimeMs: 1712252590927,
  isFile: [Function: isFile],
  isDirectory: [Function: isDirectory],
  isSymbolicLink: [Function: isSymbolicLink],
  isBlockDevice: [Function: isBlockDevice],
  isFIFO: [Function: isFIFO],
  isCharacterDevice: [Function: isCharacterDevice],
  isSocket: [Function: isSocket],
  ctime: 2024-07-05T15:16:48.000Z,
  ctimeMs: 1720192608000
}

❯ stat /home/felipecrs/.local/share/pkgx/pantry/projects
  File: /home/felipecrs/.local/share/pkgx/pantry/projects
  Size: 28672           Blocks: 64         IO Block: 4096   directory
Device: 810h/2064d      Inode: 175481      Links: 728
Access: (0755/drwxr-xr-x)  Uid: ( 1000/felipecrs)   Gid: ( 1000/felipecrs)
Access: 2024-07-06 18:48:07.873866606 -0300
Modify: 2024-07-05 12:16:48.000000000 -0300
Change: 2024-07-06 18:46:10.893881669 -0300
 Birth: 2024-04-04 14:43:10.927790747 -0300
lucacasonato commented 2 weeks ago

This looks like a good first issue:

  1. Expose ctime in Deno.stat
  2. Use the correct ctime value in node:fs's stat instead of using the mtime value.