Repository: Thomasdezeeuw/a10 (rust)

Issue:
Add Linux kernel v6.9 operations
`IORING_OP_FTRUNCATE`:
https://github.com/torvalds/linux/commit/b4bb1900c12e6a0fe11ff51e1aa6eea19a4ad635

Pull request:
Add AsyncFd::truncate
Calls `ftruncate(2)`, truncating a file to a specified length.

Closes #125

Patch:
diff --git a/Makefile b/Makefile
index f67e142..761cbe4 100644
--- a/Makefile
+++ b/Makefile
@@ -35,8 +35,8 @@ check:
 # * `single-match-else`: prefer match statements over if statements.
 # * `use-self`: strongly disagree.
 # TODO: resolve `cast-possible-truncation` errors.
-# TODO: resolve `manual-c-str-listerals` and `ref-as-ptr` once the related
-# features are a little older (1.77 and 1.76).
+# TODO: resolve `manual-c-str-listerals`, `ref-as-ptr` and `inspect_err` once
+# the related features are a little older (1.77, 1.76 and 1.75).
 lint: clippy
 clippy:
 	cargo clippy --all-features --workspace -- \
@@ -61,6 +61,7 @@ clippy:
 		--allow clippy::ref-as-ptr \
 		--allow clippy::single-match-else \
 		--allow clippy::use-self \
+		--allow clippy::manual-inspect \
 		\
 		--allow clippy::cast-possible-truncation \
 		--allow clippy::cast-possible-wrap \
diff --git a/src/fs.rs b/src/fs.rs
index cf60804..0cbbc9a 100644
--- a/src/fs.rs
+++ b/src/fs.rs
@@ -365,6 +365,17 @@ impl<D: Descriptor> AsyncFd<D> {
     ) -> Allocate<'fd, D> {
         Allocate::new(self, (offset, length, mode))
     }
+
+    /// Truncate the file to `length`.
+    ///
+    /// If the file previously was larger than this size, the extra data is
+    /// lost. If the file previously was shorter, it is extended, and the
+    /// extended part reads as null bytes.
+    #[doc = man_link!(ftruncate(2))]
+    #[doc(alias = "ftruncate")]
+    pub const fn truncate<'fd>(&'fd self, length: u64) -> Truncate<'fd, D> {
+        Truncate::new(self, length)
+    }
 }
 
 // SyncData.
@@ -430,6 +441,22 @@ op_future! {
     },
 }
 
+// Truncate.
+op_future! {
+    fn AsyncFd::truncate -> (),
+    struct Truncate<'fd> {
+        // Doesn't need any fields.
+    },
+    setup_state: flags: u64,
+    setup: |submission, fd, (), length| unsafe {
+        submission.ftruncate(fd.fd(), length);
+    },
+    map_result: |this, (), res| {
+        debug_assert!(res == 0);
+        Ok(())
+    },
+}
+
 /// Metadata information about a file.
 ///
 /// See [`AsyncFd::metadata`] and [`Stat`].
diff --git a/src/op.rs b/src/op.rs
index fddf102..5a1df9a 100644
--- a/src/op.rs
+++ b/src/op.rs
@@ -745,6 +745,12 @@ impl Submission {
         };
     }
 
+    pub(crate) unsafe fn ftruncate(&mut self, fd: RawFd, offset: u64) {
+        self.inner.opcode = libc::IORING_OP_FTRUNCATE as u8;
+        self.inner.fd = fd;
+        self.inner.__bindgen_anon_1 = libc::io_uring_sqe__bindgen_ty_1 { off: offset };
+    }
+
     pub(crate) unsafe fn wake(&mut self, ring_fd: RawFd) {
         self.msg(ring_fd, u64::MAX, 0, 0);
         self.no_completion_event();


Test patch:
diff --git a/tests/async_fd/fs.rs b/tests/async_fd/fs.rs
index 8795b98..c0d58e7 100644
--- a/tests/async_fd/fs.rs
+++ b/tests/async_fd/fs.rs
@@ -5,13 +5,13 @@ use std::path::{Path, PathBuf};
 use std::{panic, str};
 
 use a10::fd::File;
-use a10::fs::{self, Advise, Allocate, CreateDir, Delete, Open, OpenOptions, Rename};
+use a10::fs::{self, Advise, Allocate, CreateDir, Delete, Open, OpenOptions, Rename, Truncate};
 use a10::io::{Read, ReadVectored, Write, WriteVectored};
 use a10::{Extract, SubmissionQueue};
 
 use crate::util::{
-    defer, is_send, is_sync, page_size, remove_test_dir, remove_test_file, test_queue, TestFile,
-    Waker, LOREM_IPSUM_5, LOREM_IPSUM_50,
+    defer, is_send, is_sync, page_size, remove_test_dir, remove_test_file, require_kernel,
+    test_queue, TestFile, Waker, LOREM_IPSUM_5, LOREM_IPSUM_50,
 };
 
 const DATA: &[u8] = b"Hello, World";
@@ -521,6 +521,44 @@ fn fadvise() {
     assert!(buf == test_file.content, "read content is different");
 }
 
+#[test]
+fn ftruncate() {
+    require_kernel!(6, 9);
+
+    let sq = test_queue();
+    let waker = Waker::new();
+
+    is_send::<Truncate>();
+    is_sync::<Truncate>();
+
+    let mut path = temp_dir();
+    path.push("ftruncate");
+    let _d = defer(|| remove_test_file(&path));
+
+    let open_file: Open<File> = OpenOptions::new()
+        .write()
+        .create()
+        .truncate()
+        .open(sq, path.clone());
+    let file = waker.block_on(open_file).unwrap();
+
+    waker
+        .block_on(file.write_all(LOREM_IPSUM_50.content))
+        .unwrap();
+
+    let metadata = waker.block_on(file.metadata()).unwrap();
+    assert_eq!(metadata.len(), LOREM_IPSUM_50.content.len() as u64);
+
+    const SIZE: u64 = 4096;
+
+    waker
+        .block_on(file.truncate(SIZE))
+        .expect("failed fallocate");
+
+    let metadata = waker.block_on(file.metadata()).unwrap();
+    assert_eq!(metadata.len(), SIZE);
+}
+
 #[test]
 fn fallocate() {
     let sq = test_queue();