1use 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#[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 pub fn get(&self, filename: &str) -> Option<String> {
38 get_file_contents(filename).ok()
39 }
40
41 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 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 pub fn keys(&self) -> io::Result<Vec<String>> {
65 scan_directory(".")
66 }
67
68 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 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#[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#[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
198pub 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
218pub fn file_exists(filename: &str) -> bool {
220 Path::new(filename).exists()
221}
222
223pub 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}