io_test/
lib.rs

1use std::{
2    cell::RefCell,
3    collections::BTreeMap,
4    io::{self, Read, Seek, Write},
5    iter::once,
6    ops::Add,
7    rc::Rc,
8    str::from_utf8,
9    time::Duration,
10    vec,
11};
12
13use io_trait::{File, Io};
14
15#[derive(Debug, Clone)]
16pub struct Metadata {
17    len: u64,
18    is_dir: bool,
19}
20
21impl io_trait::Metadata for Metadata {
22    fn len(&self) -> u64 {
23        self.len
24    }
25    fn is_dir(&self) -> bool {
26        self.is_dir
27    }
28}
29
30#[derive(Debug, Default, Clone)]
31pub struct VecRef(Rc<RefCell<Vec<u8>>>);
32
33impl VecRef {
34    pub fn to_stdout(&self) -> String {
35        let mut result = Vec::default();
36        let mut i = 0;
37        for &c in self.0.borrow().iter() {
38            if c == 8 {
39                i -= 1;
40            } else {
41                if i < result.len() {
42                    result[i] = c;
43                } else {
44                    result.push(c);
45                }
46                i += 1;
47            }
48        }
49        from_utf8(&result).unwrap().to_string()
50    }
51    fn len(&self) -> u64 {
52        self.0.borrow().len() as u64
53    }
54    fn metadata(&self) -> Metadata {
55        Metadata {
56            len: self.len(),
57            is_dir: false,
58        }
59    }
60}
61
62impl Write for VecRef {
63    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
64        self.0.borrow_mut().extend_from_slice(buf);
65        Ok(buf.len())
66    }
67    fn flush(&mut self) -> io::Result<()> {
68        Ok(())
69    }
70}
71
72#[derive(Debug, Default)]
73enum Entity {
74    #[default]
75    Dir,
76    File(VecRef),
77}
78
79impl Entity {
80    fn metadata(&self) -> Metadata {
81        match self {
82            Entity::Dir => Metadata {
83                len: 0,
84                is_dir: true,
85            },
86            Entity::File(x) => x.metadata(),
87        }
88    }
89}
90
91#[derive(Debug, Default)]
92pub struct FileSystem {
93    entity_map: BTreeMap<String, Entity>,
94}
95
96impl FileSystem {
97    pub fn check_dir(&self, path: &str) -> io::Result<()> {
98        if let Some(Entity::Dir) = self.entity_map.get(path) {
99            Ok(())
100        } else {
101            Err(not_found())
102        }
103    }
104    pub fn check_parent(&self, path: &str) -> io::Result<()> {
105        if let Some(d) = path.rfind('/').map(|i| &path[..i]) {
106            self.check_dir(d)
107        } else {
108            Ok(())
109        }
110    }
111}
112
113pub struct DirEntry {
114    path: String,
115    metadata: Metadata,
116}
117
118impl io_trait::DirEntry for DirEntry {
119    type Metadata = Metadata;
120    fn path(&self) -> String {
121        self.path.clone()
122    }
123    fn metadata(&self) -> io::Result<Self::Metadata> {
124        Ok(self.metadata.clone())
125    }
126}
127
128pub struct VirtualIo {
129    pub args: Vec<String>,
130    pub fs: RefCell<FileSystem>,
131    pub stdout: VecRef,
132    pub duration: RefCell<Duration>,
133}
134
135impl VirtualIo {
136    pub fn new(args: &[&str]) -> Self {
137        Self {
138            args: once("blockset".to_string())
139                .chain(args.iter().map(|v| v.to_string()))
140                .collect(),
141            fs: Default::default(),
142            stdout: Default::default(),
143            duration: Default::default(),
144        }
145    }
146}
147
148#[derive(Debug)]
149pub struct MemFile {
150    vec_ref: VecRef,
151    pos: usize,
152}
153
154impl MemFile {
155    fn new(vec_ref: VecRef) -> Self {
156        Self { vec_ref, pos: 0 }
157    }
158}
159
160impl File for MemFile {
161    type Metadata = Metadata;
162    fn metadata(&self) -> io::Result<Self::Metadata> {
163        Ok(self.vec_ref.metadata())
164    }
165}
166
167impl Seek for MemFile {
168    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
169        self.pos = match pos {
170            io::SeekFrom::Start(x) => x as usize,
171            io::SeekFrom::End(x) => (self.vec_ref.len() as i64 + x) as usize,
172            io::SeekFrom::Current(x) => (self.pos as i64 + x) as usize,
173        };
174        Ok(self.pos as u64)
175    }
176}
177
178impl Read for MemFile {
179    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
180        let source = &self.vec_ref.0.borrow()[self.pos..];
181        let len = source.len().min(buf.len());
182        buf[..len].copy_from_slice(&source[..len]);
183        self.pos += len;
184        Ok(len)
185    }
186}
187
188impl Write for MemFile {
189    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
190        let pos = self.pos;
191        let buf_len = buf.len();
192        let end = pos + buf_len;
193        {
194            let mut v = self.vec_ref.0.borrow_mut();
195            if end > v.len() {
196                v.resize(end, 0);
197            }
198            v[pos..end].copy_from_slice(buf);
199        }
200        self.pos = end;
201        Ok(buf_len)
202    }
203    fn flush(&mut self) -> io::Result<()> {
204        self.vec_ref.flush()
205    }
206}
207
208fn not_found() -> io::Error {
209    io::Error::new(io::ErrorKind::NotFound, "file not found")
210}
211
212fn check_path(a: &str) -> io::Result<()> {
213    if a.chars()
214        .all(|c| c.is_ascii_alphanumeric() || "/_.-".contains(c))
215    {
216        Ok(())
217    } else {
218        Err(io::Error::new(
219            io::ErrorKind::InvalidInput,
220            "invalid file name",
221        ))
222    }
223}
224
225impl Io for VirtualIo {
226    type File = MemFile;
227    type Stdout = VecRef;
228    type Args = vec::IntoIter<String>;
229    type Metadata = Metadata;
230    type DirEntry = DirEntry;
231    type Instant = Duration;
232    fn args(&self) -> Self::Args {
233        self.args.clone().into_iter()
234    }
235    fn metadata(&self, path: &str) -> io::Result<Metadata> {
236        let fs = self.fs.borrow();
237        let dir_end = path.ends_with('/');
238        let path = if dir_end {
239            &path[..path.len() - 1]
240        } else {
241            path
242        };
243        let result = fs
244            .entity_map
245            .get(path)
246            .map(Entity::metadata)
247            .ok_or_else(not_found)?;
248        if !result.is_dir && dir_end {
249            return Err(not_found());
250        }
251        Ok(result)
252    }
253    fn create(&self, path: &str) -> io::Result<Self::File> {
254        let mut fs = self.fs.borrow_mut();
255        fs.check_parent(path)?;
256        let vec_ref = VecRef::default();
257        check_path(path)?;
258        fs.entity_map
259            .insert(path.to_string(), Entity::File(vec_ref.clone()));
260        Ok(MemFile::new(vec_ref))
261    }
262    fn create_dir(&self, path: &str) -> io::Result<()> {
263        let mut fs = self.fs.borrow_mut();
264        fs.entity_map.insert(path.to_string(), Entity::Dir);
265        Ok(())
266    }
267    fn open(&self, path: &str) -> io::Result<Self::File> {
268        let fs = self.fs.borrow();
269        fs.check_parent(path)?;
270        check_path(path)?;
271        fs.entity_map
272            .get(path)
273            .and_then(|v| {
274                if let Entity::File(x) = v {
275                    Some(MemFile::new(x.to_owned()))
276                } else {
277                    None
278                }
279            })
280            .ok_or_else(not_found)
281    }
282    fn stdout(&self) -> VecRef {
283        self.stdout.clone()
284    }
285
286    fn read_dir(&self, path: &str) -> io::Result<Vec<DirEntry>> {
287        let fs = self.fs.borrow();
288        fs.check_dir(path)?;
289        let i = fs.entity_map.iter().map(|(p, e)| DirEntry {
290            path: p.to_owned(),
291            metadata: e.metadata(),
292        });
293        let x = i
294            .filter(|p| {
295                if let Some((a, _)) = p.path.rsplit_once('/') {
296                    a == path
297                } else {
298                    false
299                }
300            })
301            .collect();
302        Ok(x)
303    }
304
305    fn now(&self) -> Self::Instant {
306        let mut d = self.duration.borrow_mut();
307        let result = *d;
308        *d = d.add(Duration::from_millis(1));
309        result
310    }
311
312    fn current_dir(&self) -> io::Result<String> {
313        Ok(String::default())
314    }
315    fn set_current_dir(&self, path: &str) -> io::Result<()> {
316        if path.is_empty() {
317            Ok(())
318        } else {
319            Err(io::Error::new(
320                io::ErrorKind::NotFound,
321                "directory not found",
322            ))
323        }
324    }
325}
326
327#[cfg(test)]
328mod test {
329    use std::io::{self, Seek, SeekFrom, Write};
330
331    use io_trait::{DirEntry, File, Io, Metadata};
332    use wasm_bindgen_test::wasm_bindgen_test;
333
334    use crate::VirtualIo;
335
336    #[wasm_bindgen_test]
337    #[test]
338    fn test() {
339        fn check_len(m: &super::Metadata, f: fn(m: &super::Metadata) -> u64, len: u64) {
340            assert_eq!(f(m), len);
341        }
342        fn check_current_dir(
343            io: &VirtualIo,
344            path: &str,
345            f: fn(x: &VirtualIo) -> io::Result<String>,
346        ) {
347            assert_eq!(f(io).unwrap(), path);
348        }
349        let io = VirtualIo::new(&[]);
350        io.write("test.txt", "Hello, world!".as_bytes()).unwrap();
351        let result = io.read_to_string("test.txt").unwrap();
352        assert_eq!(result, "Hello, world!");
353        check_len(&io.metadata("test.txt").unwrap(), Metadata::len, 13);
354        // assert_eq!(io.metadata("test.txt").unwrap().len(), 13);
355        check_current_dir(&io, "", VirtualIo::current_dir);
356        // assert_eq!(io.current_dir().unwrap(), "");
357    }
358
359    #[wasm_bindgen_test]
360    #[test]
361    fn test_args() {
362        let mut io = VirtualIo::new(&[]);
363        io.args = ["a".to_string(), "b".to_string()].to_vec();
364        let x = io.args().collect::<Vec<_>>();
365        assert_eq!(&x, &["a", "b"]);
366    }
367
368    #[wasm_bindgen_test]
369    #[test]
370    fn test_stdout() {
371        {
372            let io = VirtualIo::new(&[]);
373            let mut s = io.stdout();
374            s.write(b"Hello, world!\x08?").unwrap();
375            assert_eq!(s.to_stdout(), "Hello, world?");
376        }
377        {
378            let io = VirtualIo::new(&[]);
379            let mut s = io.stdout();
380            s.write(b"Hello, world!\x08\x08?").unwrap();
381            assert_eq!(s.to_stdout(), "Hello, worl?!");
382        }
383    }
384
385    #[wasm_bindgen_test]
386    #[test]
387    fn test_write() {
388        let io = VirtualIo::new(&[]);
389        io.write("test.txt", "Hello, world!".as_bytes()).unwrap();
390        let result = io.read("test.txt").unwrap();
391        assert_eq!(result, "Hello, world!".as_bytes());
392    }
393
394    #[wasm_bindgen_test]
395    #[test]
396    fn test_write_file() {
397        fn flush<W: Write>(w: &mut W, f: fn(&mut W) -> io::Result<()>) {
398            f(w).unwrap();
399        }
400        let i = VirtualIo::new(&[]);
401        {
402            let mut f = i.create("test.txt").unwrap();
403            f.write("Hello, world!".as_bytes()).unwrap();
404            f.write("?".as_bytes()).unwrap();
405            flush(&mut f, Write::flush);
406            let m = f.metadata().unwrap();
407            assert_eq!(m.len(), 14);
408            assert!(!m.is_dir());
409        }
410        let result = i.read("test.txt").unwrap();
411        assert_eq!(result, "Hello, world!?".as_bytes());
412    }
413
414    #[wasm_bindgen_test]
415    #[test]
416    fn test_write_seek() {
417        let io = VirtualIo::new(&[]);
418        {
419            let mut f = io.create("test.txt").unwrap();
420            f.write("Hello, world!".as_bytes()).unwrap();
421            f.seek(SeekFrom::Start(7)).unwrap();
422            f.write("there!".as_bytes()).unwrap();
423            f.flush().unwrap();
424            let m = f.metadata().unwrap();
425            assert_eq!(m.len(), 13);
426            assert!(!m.is_dir());
427        }
428        let result = io.read("test.txt").unwrap();
429        assert_eq!(result, "Hello, there!".as_bytes());
430    }
431
432    #[wasm_bindgen_test]
433    #[test]
434    fn test_write_seek_current() {
435        let io = VirtualIo::new(&[]);
436        {
437            let mut f = io.create("test.txt").unwrap();
438            f.write("Hello, world!".as_bytes()).unwrap();
439            f.seek(SeekFrom::Current(2)).unwrap();
440            f.write("there".as_bytes()).unwrap();
441            f.flush().unwrap();
442            let m = f.metadata().unwrap();
443            assert_eq!(m.len(), 20);
444            assert!(!m.is_dir());
445        }
446        let result = io.read("test.txt").unwrap();
447        assert_eq!(result, "Hello, world!\0\0there".as_bytes());
448    }
449
450    #[wasm_bindgen_test]
451    #[test]
452    fn test_write_seek_end() {
453        let io = VirtualIo::new(&[]);
454        {
455            let mut f = io.create("test.txt").unwrap();
456            f.write("Hello, world!".as_bytes()).unwrap();
457            f.seek(SeekFrom::End(-2)).unwrap();
458            f.write("there".as_bytes()).unwrap();
459            f.flush().unwrap();
460            let m = f.metadata().unwrap();
461            assert_eq!(m.len(), 16);
462            assert!(!m.is_dir());
463        }
464        let result = io.read("test.txt").unwrap();
465        assert_eq!(result, "Hello, worlthere".as_bytes());
466    }
467
468    #[wasm_bindgen_test]
469    #[test]
470    fn test_dir_fail() {
471        let io = VirtualIo::new(&[]);
472        assert!(io.write("a/test.txt", "Hello, world!".as_bytes()).is_err());
473        assert!(io.open("a").is_err());
474    }
475
476    #[wasm_bindgen_test]
477    #[test]
478    fn test_write_recursively() {
479        let io = VirtualIo::new(&[]);
480        assert!(io
481            .write_recursively("a/test.txt", "Hello, world!".as_bytes())
482            .is_ok());
483        assert!(io
484            .write_recursively("a/test2.txt", "Hello, world!".as_bytes())
485            .is_ok());
486        assert!(io.open("a").is_err());
487    }
488
489    #[wasm_bindgen_test]
490    #[test]
491    fn test_dir_rec() {
492        let io = VirtualIo::new(&[]);
493        assert!(io
494            .write_recursively("a/b/test.txt", "Hello, world!".as_bytes())
495            .is_ok());
496        let x = io
497            .read_dir_type("a", true)
498            .unwrap()
499            .iter()
500            .map(|v| v.path().to_owned())
501            .collect::<Vec<_>>();
502        assert_eq!(x, ["a/b"].to_vec());
503    }
504
505    #[wasm_bindgen_test]
506    #[test]
507    fn test_err() {
508        let io = VirtualIo::new(&[]);
509        assert!(io
510            .write_recursively("?", "Hello, world!".as_bytes())
511            .is_err());
512    }
513
514    #[wasm_bindgen_test]
515    #[test]
516    fn test_now() {
517        let io = VirtualIo::new(&[]);
518        assert_eq!(io.now().as_millis(), 0);
519        assert_eq!(io.now().as_millis(), 1);
520    }
521
522    fn check_len(m: &super::Metadata, f: fn(m: &super::Metadata) -> u64, len: u64) {
523        assert_eq!(m.len(), len);
524    }
525
526    #[wasm_bindgen_test]
527    #[test]
528    fn test_metadata() {
529        let io = VirtualIo::new(&[]);
530        io.write("test.txt", "Hello, world!".as_bytes()).unwrap();
531        io.create_dir("a").unwrap();
532        {
533            let m = io.metadata("test.txt").unwrap();
534            // assert_eq!(m.len(), 13);
535            check_len(&m, super::Metadata::len, 13);
536            assert!(!m.is_dir());
537        }
538        {
539            io.metadata("test.txt/").unwrap_err();
540        }
541        {
542            io.metadata("b").unwrap_err();
543        }
544        {
545            let m = io.metadata("a").unwrap();
546            assert!(m.is_dir());
547        }
548        {
549            let m = io.metadata("a/").unwrap();
550            assert!(m.is_dir());
551        }
552    }
553
554    #[wasm_bindgen_test]
555    #[test]
556    fn test_set_current_dir() {
557        let io = VirtualIo::new(&[]);
558        assert!(io.set_current_dir("").is_ok());
559        assert!(io.set_current_dir("a").is_err());
560    }
561}