Repository: denoland/deno
Pull request: fix(ext/node): add `truncate` method to the `FileHandle` class

- Implements `FileHandle.truncate`
- Adds a promisified `fs.ftruncate` under `fs/promises` which is used internally by `FileHandle.ftruncate`
- Adds a couple of formatting changes due to running `./tools/format.js`
- Adds tests
- `filehandle.truncate` Node documentation: [click here](https://nodejs.org/api/fs.html#filehandletruncatelen)

part of https://github.com/denoland/deno/issues/25554

Changed files:
filename: ext/node/polyfills/_fs/_fs_ftruncate.ts
patch: @@ -16,16 +16,24 @@ export function ftruncate(
     : undefined;
   const callback: CallbackWithError = typeof lenOrCallback === "function"
     ? lenOrCallback
-    : maybeCallback as CallbackWithError;
+    : (maybeCallback as CallbackWithError);
 
   if (!callback) throw new Error("No callback function supplied");
 
-  new FsFile(fd, Symbol.for("Deno.internal.FsFile")).truncate(len).then(
-    () => callback(null),
-    callback,
-  );
+  new FsFile(fd, Symbol.for("Deno.internal.FsFile"))
+    .truncate(len)
+    .then(() => callback(null), callback);
 }
 
 export function ftruncateSync(fd: number, len?: number) {
   new FsFile(fd, Symbol.for("Deno.internal.FsFile")).truncateSync(len);
 }
+
+export function ftruncatePromise(fd: number, len?: number): Promise<void> {
+  return new Promise((resolve, reject) => {
+    ftruncate(fd, len, (err) => {
+      if (err) reject(err);
+      else resolve();
+    });
+  });
+}

filename: ext/node/polyfills/internal/fs/handle.ts
patch: @@ -13,6 +13,7 @@ import {
   ReadOptions,
   TextOptionsArgument,
 } from "ext:deno_node/_fs/_fs_common.ts";
+import { ftruncatePromise } from "ext:deno_node/_fs/_fs_ftruncate.ts";
 import { core } from "ext:core/mod.js";
 
 interface WriteResult {
@@ -73,6 +74,10 @@ export class FileHandle extends EventEmitter {
     }
   }
 
+  truncate(len?: number): Promise<void> {
+    return fsCall(ftruncatePromise, this, len);
+  }
+
   readFile(
     opt?: TextOptionsArgument | BinaryOptionsArgument | FileOptionsArgument,
   ): Promise<string | Buffer> {
@@ -85,11 +90,7 @@ export class FileHandle extends EventEmitter {
     length: number,
     position: number,
   ): Promise<WriteResult>;
-  write(
-    str: string,
-    position: number,
-    encoding: string,
-  ): Promise<WriteResult>;
+  write(str: string, position: number, encoding: string): Promise<WriteResult>;
   write(
     bufferOrStr: Uint8Array | string,
     offsetOrPosition: number,
@@ -120,16 +121,10 @@ export class FileHandle extends EventEmitter {
       const encoding = lengthOrEncoding;
 
       return new Promise((resolve, reject) => {
-        write(
-          this.fd,
-          str,
-          position,
-          encoding,
-          (err, bytesWritten, buffer) => {
-            if (err) reject(err);
-            else resolve({ buffer, bytesWritten });
-          },
-        );
+        write(this.fd, str, position, encoding, (err, bytesWritten, buffer) => {
+          if (err) reject(err);
+          else resolve({ buffer, bytesWritten });
+        });
       });
     }
   }

filename: tests/unit_node/_fs/_fs_handle_test.ts
patch: @@ -117,3 +117,85 @@ Deno.test("[node/fs filehandle.writeFile] Write to file", async function () {
 
   assertEquals(decoder.decode(data), "hello world");
 });
+
+Deno.test(
+  "[node/fs filehandle.truncate] Truncate file with length",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w+");
+
+    await fileHandle.writeFile("hello world");
+
+    await fileHandle.truncate(5);
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    assertEquals(decoder.decode(data), "hello");
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.truncate] Truncate file without length",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w+");
+
+    await fileHandle.writeFile("hello world");
+
+    await fileHandle.truncate();
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    assertEquals(decoder.decode(data), "");
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.truncate] Truncate file with extension",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w+");
+
+    await fileHandle.writeFile("hi");
+
+    await fileHandle.truncate(5);
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    const expected = new Uint8Array(5);
+    expected.set(new TextEncoder().encode("hi"));
+
+    assertEquals(data, expected);
+    assertEquals(data.length, 5);
+    assertEquals(decoder.decode(data.subarray(0, 2)), "hi");
+    // Verify null bytes
+    assertEquals(data[2], 0);
+    assertEquals(data[3], 0);
+    assertEquals(data[4], 0);
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.truncate] Truncate file with negative length",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w+");
+
+    await fileHandle.writeFile("hello world");
+
+    await fileHandle.truncate(-1);
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    assertEquals(decoder.decode(data), "");
+    assertEquals(data.length, 0);
+  },
+);