Skip to main content

zsh/
mapfile.rs

1//! Mapfile module - port of Modules/mapfile.c
2//!
3//! Provides associative array interface to external files.
4//! The mapfile hash allows reading and writing files through hash syntax:
5//! - Reading: `$mapfile[filename]` returns file contents
6//! - Writing: `mapfile[filename]=content` writes to file
7//! - Unsetting: `unset 'mapfile[filename]'` deletes the file
8
9use std::collections::HashMap;
10use std::fs::{self, File, OpenOptions};
11use std::io::{self, Read, Write};
12use std::path::Path;
13
14#[cfg(unix)]
15use std::os::unix::io::AsRawFd;
16
17/// Mapfile associative array emulation
18#[derive(Debug, Default)]
19pub struct Mapfile {
20    readonly: bool,
21}
22
23impl Mapfile {
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    pub fn set_readonly(&mut self, readonly: bool) {
29        self.readonly = readonly;
30    }
31
32    pub fn is_readonly(&self) -> bool {
33        self.readonly
34    }
35
36    /// Get file contents by filename (key)
37    pub fn get(&self, filename: &str) -> Option<String> {
38        get_file_contents(filename).ok()
39    }
40
41    /// Set file contents by filename (key)
42    pub fn set(&self, filename: &str, contents: &str) -> io::Result<()> {
43        if self.readonly {
44            return Err(io::Error::new(
45                io::ErrorKind::PermissionDenied,
46                "mapfile is read-only",
47            ));
48        }
49        set_file_contents(filename, contents)
50    }
51
52    /// Unset (delete) a file by filename (key)
53    pub fn unset(&self, filename: &str) -> io::Result<()> {
54        if self.readonly {
55            return Err(io::Error::new(
56                io::ErrorKind::PermissionDenied,
57                "mapfile is read-only",
58            ));
59        }
60        fs::remove_file(filename)
61    }
62
63    /// Scan current directory for files
64    pub fn keys(&self) -> io::Result<Vec<String>> {
65        scan_directory(".")
66    }
67
68    /// Get all files in current directory as hash
69    pub fn to_hash(&self) -> io::Result<HashMap<String, String>> {
70        let mut result = HashMap::new();
71        for filename in self.keys()? {
72            if let Ok(contents) = get_file_contents(&filename) {
73                result.insert(filename, contents);
74            }
75        }
76        Ok(result)
77    }
78
79    /// Set multiple files from a hash
80    pub fn from_hash(&self, files: &HashMap<String, String>) -> io::Result<()> {
81        if self.readonly {
82            return Err(io::Error::new(
83                io::ErrorKind::PermissionDenied,
84                "mapfile is read-only",
85            ));
86        }
87        for (filename, contents) in files {
88            set_file_contents(filename, contents)?;
89        }
90        Ok(())
91    }
92}
93
94/// Read file contents using mmap when available
95#[cfg(unix)]
96pub fn get_file_contents(filename: &str) -> io::Result<String> {
97    use std::os::unix::fs::MetadataExt;
98
99    let file = File::open(filename)?;
100    let metadata = file.metadata()?;
101    let size = metadata.size() as usize;
102
103    if size == 0 {
104        return Ok(String::new());
105    }
106
107    let fd = file.as_raw_fd();
108
109    let ptr = unsafe {
110        libc::mmap(
111            std::ptr::null_mut(),
112            size,
113            libc::PROT_READ,
114            libc::MAP_PRIVATE,
115            fd,
116            0,
117        )
118    };
119
120    if ptr == libc::MAP_FAILED {
121        let mut contents = String::new();
122        let mut file = file;
123        file.read_to_string(&mut contents)?;
124        return Ok(contents);
125    }
126
127    let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) };
128    let contents = String::from_utf8_lossy(slice).into_owned();
129
130    unsafe {
131        libc::munmap(ptr, size);
132    }
133
134    Ok(contents)
135}
136
137#[cfg(not(unix))]
138pub fn get_file_contents(filename: &str) -> io::Result<String> {
139    fs::read_to_string(filename)
140}
141
142/// Write file contents using mmap when available
143#[cfg(unix)]
144pub fn set_file_contents(filename: &str, contents: &str) -> io::Result<()> {
145    let file = OpenOptions::new()
146        .read(true)
147        .write(true)
148        .create(true)
149        .truncate(false)
150        .open(filename)?;
151
152    let fd = file.as_raw_fd();
153    let len = contents.len();
154
155    if len == 0 {
156        file.set_len(0)?;
157        return Ok(());
158    }
159
160    unsafe {
161        if libc::ftruncate(fd, len as libc::off_t) < 0 {
162            return Err(io::Error::last_os_error());
163        }
164    }
165
166    let ptr = unsafe {
167        libc::mmap(
168            std::ptr::null_mut(),
169            len,
170            libc::PROT_READ | libc::PROT_WRITE,
171            libc::MAP_SHARED,
172            fd,
173            0,
174        )
175    };
176
177    if ptr == libc::MAP_FAILED {
178        let mut file = file;
179        file.set_len(0)?;
180        file.write_all(contents.as_bytes())?;
181        return Ok(());
182    }
183
184    unsafe {
185        std::ptr::copy_nonoverlapping(contents.as_ptr(), ptr as *mut u8, len);
186        libc::msync(ptr, len, libc::MS_SYNC);
187        libc::munmap(ptr, len);
188    }
189
190    Ok(())
191}
192
193#[cfg(not(unix))]
194pub fn set_file_contents(filename: &str, contents: &str) -> io::Result<()> {
195    fs::write(filename, contents)
196}
197
198/// Scan directory for regular files
199pub fn scan_directory(dir: &str) -> io::Result<Vec<String>> {
200    let mut files = Vec::new();
201
202    for entry in fs::read_dir(dir)? {
203        let entry = entry?;
204        let path = entry.path();
205
206        if path.is_file() {
207            if let Some(name) = path.file_name() {
208                if let Some(name_str) = name.to_str() {
209                    files.push(name_str.to_string());
210                }
211            }
212        }
213    }
214
215    Ok(files)
216}
217
218/// Check if a file exists
219pub fn file_exists(filename: &str) -> bool {
220    Path::new(filename).exists()
221}
222
223/// Get file size
224pub fn file_size(filename: &str) -> io::Result<u64> {
225    Ok(fs::metadata(filename)?.len())
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use std::fs;
232
233    #[test]
234    fn test_mapfile_new() {
235        let mf = Mapfile::new();
236        assert!(!mf.is_readonly());
237    }
238
239    #[test]
240    fn test_mapfile_readonly() {
241        let mut mf = Mapfile::new();
242        mf.set_readonly(true);
243        assert!(mf.is_readonly());
244
245        let result = mf.set("test.txt", "content");
246        assert!(result.is_err());
247    }
248
249    #[test]
250    fn test_get_nonexistent_file() {
251        let mf = Mapfile::new();
252        assert!(mf.get("/nonexistent/file/path").is_none());
253    }
254
255    #[test]
256    fn test_file_roundtrip() {
257        let test_file = "/tmp/zsh_mapfile_test.txt";
258        let content = "Hello, mapfile!";
259
260        let result = set_file_contents(test_file, content);
261        assert!(result.is_ok());
262
263        let read_content = get_file_contents(test_file).unwrap();
264        assert_eq!(read_content, content);
265
266        let _ = fs::remove_file(test_file);
267    }
268
269    #[test]
270    fn test_empty_file() {
271        let test_file = "/tmp/zsh_mapfile_empty.txt";
272
273        let result = set_file_contents(test_file, "");
274        assert!(result.is_ok());
275
276        let read_content = get_file_contents(test_file).unwrap();
277        assert!(read_content.is_empty());
278
279        let _ = fs::remove_file(test_file);
280    }
281
282    #[test]
283    fn test_scan_directory() {
284        let files = scan_directory(".");
285        assert!(files.is_ok());
286    }
287
288    #[test]
289    fn test_file_exists() {
290        assert!(file_exists("."));
291        assert!(!file_exists("/nonexistent/path/to/file"));
292    }
293
294    #[test]
295    fn test_mapfile_unset() {
296        let test_file = "/tmp/zsh_mapfile_unset.txt";
297        let _ = fs::write(test_file, "content");
298
299        let mf = Mapfile::new();
300        let result = mf.unset(test_file);
301        assert!(result.is_ok());
302        assert!(!file_exists(test_file));
303    }
304}