starnix_c_file_buffer/lib.rs
1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! C-compatible memory-backed file buffer for FFI.
6
7use std::ffi::CStr;
8use std::marker::PhantomData;
9
10unsafe extern "C" {
11 fn fmemopen(
12 buf: *mut libc::c_void,
13 size: libc::size_t,
14 mode: *const libc::c_char,
15 ) -> *mut libc::FILE;
16}
17
18/// A wrapper around the raw `FILE*` that borrows `CFileBuffer` mutably.
19/// This ensures exclusive access to the file descriptor.
20pub struct CFilePtr<'a> {
21 ptr: *mut libc::FILE,
22 _marker: PhantomData<&'a mut CFileBuffer>,
23}
24
25impl<'a> CFilePtr<'a> {
26 /// Get the raw `FILE*` pointer.
27 ///
28 /// # Safety Constraints
29 /// The returned raw pointer must not be stored or used after this `CFilePtr`
30 /// (or the parent `CFileBuffer`) is dropped. The caller must ensure that the
31 /// parent `CFileBuffer` outlives any use of the raw pointer.
32 pub fn as_raw(&self) -> *mut libc::FILE {
33 self.ptr
34 }
35}
36
37/// A C-compatible memory-backed file buffer.
38///
39/// This struct wraps a `FILE*` created via `fmemopen`, allowing FFI functions
40/// to write to a memory buffer that can then be read from Rust.
41pub struct CFileBuffer {
42 file: *mut libc::FILE,
43 buffer: Vec<u8>,
44}
45
46// SAFETY: CFileBuffer owns the FILE* and the buffer, and does not share them concurrently.
47// It is safe to transfer ownership of CFileBuffer to another thread.
48unsafe impl Send for CFileBuffer {}
49
50impl CFileBuffer {
51 /// Create a new CFileBuffer object with a buffer of `max_size`.
52 pub fn new(max_size: usize) -> Result<Self, &'static str> {
53 let mut buffer = vec![0u8; max_size];
54
55 // Open the file in "w+" mode. We use "w+" instead of "w" because in "w"
56 // mode, musl's fmemopen will overwrite the last byte with a null character
57 // when the buffer is full, corrupting the data if we write exactly `max_size`
58 // bytes. "w+" mode avoids this behavior.
59 let mode = CStr::from_bytes_with_nul(b"w+\0").unwrap();
60
61 // SAFETY: The buffer is owned by the CFileBuffer struct and will remain
62 // valid for the lifetime of the returned FILE*. The mode string is null-terminated.
63 let file =
64 unsafe { fmemopen(buffer.as_mut_ptr() as *mut libc::c_void, max_size, mode.as_ptr()) };
65
66 if file.is_null() {
67 return Err("fmemopen failed");
68 }
69
70 Ok(Self { file, buffer })
71 }
72
73 /// Get the raw FILE pointer wrapper.
74 /// This mutably borrows `self`, ensuring no other borrows (like `data()`)
75 /// can exist while this wrapper is alive.
76 pub fn file(&mut self) -> CFilePtr<'_> {
77 CFilePtr { ptr: self.file, _marker: PhantomData }
78 }
79
80 /// Flush the stream and return a slice over the data that has been written.
81 pub fn data(&self) -> &[u8] {
82 // Flush to ensure all data is written to the buffer.
83 // SAFETY: self.file is a valid FILE pointer created during construction.
84 unsafe {
85 libc::fflush(self.file);
86 }
87
88 // Find the current position in the file.
89 // SAFETY: self.file is a valid FILE pointer created during construction.
90 let pos = unsafe { libc::ftell(self.file) };
91 if pos < 0 {
92 return &[];
93 }
94
95 let len = pos as usize;
96 let len = std::cmp::min(len, self.buffer.len());
97
98 &self.buffer[..len]
99 }
100
101 /// Reset the position of the FILE* to the start of the file.
102 pub fn reset(&self) -> Result<(), &'static str> {
103 // SAFETY: self.file is a valid FILE pointer created during construction.
104 let ret = unsafe { libc::fseek(self.file, 0, libc::SEEK_SET) };
105 if ret != 0 {
106 return Err("fseek failed");
107 }
108 Ok(())
109 }
110}
111
112impl Drop for CFileBuffer {
113 fn drop(&mut self) {
114 // SAFETY: self.file is a valid FILE pointer that has not been closed yet.
115 unsafe {
116 libc::fclose(self.file);
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_write_and_read() {
127 let mut fmem = CFileBuffer::new(1024).unwrap();
128
129 // Write some data using libc
130 let data_to_write = b"hello world";
131 // SAFETY: The file pointer is valid, and the data buffer is valid for the duration of the write.
132 unsafe {
133 let file_ptr = fmem.file();
134 assert!(!file_ptr.as_raw().is_null());
135 libc::fwrite(
136 data_to_write.as_ptr() as *const libc::c_void,
137 1,
138 data_to_write.len(),
139 file_ptr.as_raw(),
140 );
141 }
142
143 // Read data back
144 let written = fmem.data();
145 assert_eq!(written, data_to_write);
146 }
147
148 #[test]
149 fn test_reset() {
150 let mut fmem = CFileBuffer::new(1024).unwrap();
151
152 let data1 = b"abc";
153 // SAFETY: The file pointer is valid, and the data buffer is valid for the duration of the write.
154 unsafe {
155 let file_ptr = fmem.file();
156 libc::fwrite(data1.as_ptr() as *const libc::c_void, 1, data1.len(), file_ptr.as_raw());
157 }
158 assert_eq!(fmem.data(), data1);
159
160 fmem.reset().unwrap();
161 assert_eq!(fmem.data(), b"");
162
163 let data2 = b"defgh";
164 // SAFETY: The file pointer is valid, and the data buffer is valid for the duration of the write.
165 unsafe {
166 let file_ptr = fmem.file();
167 libc::fwrite(data2.as_ptr() as *const libc::c_void, 1, data2.len(), file_ptr.as_raw());
168 }
169 assert_eq!(fmem.data(), data2);
170 }
171
172 #[test]
173 fn test_overflow() {
174 let mut fmem = CFileBuffer::new(5).unwrap();
175
176 let data = b"abcdefg";
177 // SAFETY: The file pointer is valid, and the data buffer is valid for the duration of the write.
178 unsafe {
179 let file_ptr = fmem.file();
180 libc::fwrite(data.as_ptr() as *const libc::c_void, 1, data.len(), file_ptr.as_raw());
181 }
182 // It should be capped at 5
183 assert_eq!(fmem.data(), b"abcde");
184 }
185
186 #[test]
187 fn test_exact_size() {
188 let mut fmem = CFileBuffer::new(5).unwrap();
189
190 let data = b"12345";
191 // SAFETY: The file pointer is valid, and the data buffer is valid for the duration of the write.
192 unsafe {
193 let file_ptr = fmem.file();
194 libc::fwrite(data.as_ptr() as *const libc::c_void, 1, data.len(), file_ptr.as_raw());
195 }
196 assert_eq!(fmem.data(), b"12345");
197 }
198}