Skip to main content

endbasic_repl/
demos.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Exposes EndBASIC demos as a read-only drive.
17
18use async_trait::async_trait;
19use endbasic_std::storage::{DiskSpace, Drive, DriveFactory, DriveFiles, Metadata};
20use std::collections::{BTreeMap, HashMap};
21use std::io;
22use std::str;
23
24/// A read-only drive that exposes a bunch of read-only demo files.
25pub struct DemosDrive {
26    /// The demos to expose, expressed as a mapping of names to (metadata, content) pairs.
27    demos: HashMap<&'static str, (Metadata, String)>,
28}
29
30/// Converts the raw bytes of a demo file into the program string to expose.
31///
32/// The input `bytes` must be valid UTF-8, which we can guarantee because these bytes come from
33/// files that we own in the source tree.
34///
35/// On Windows, the output string has all CRLF pairs converted to LF to ensure that the reported
36/// demo file sizes are consistent across platforms.
37fn process_demo(bytes: &[u8]) -> String {
38    let raw_content = str::from_utf8(bytes).expect("Malformed demo file");
39    if cfg!(target_os = "windows") {
40        raw_content.replace("\r\n", "\n")
41    } else {
42        raw_content.to_owned()
43    }
44}
45
46impl Default for DemosDrive {
47    /// Creates a new demo drive.
48    fn default() -> Self {
49        let mut demos = HashMap::default();
50        {
51            let content = process_demo(include_bytes!("../examples/alarm.bas"));
52            let metadata = Metadata {
53                date: time::OffsetDateTime::from_unix_timestamp(1781913600).unwrap(),
54                length: content.len() as u64,
55            };
56            demos.insert("ALARM.BAS", (metadata, content));
57        }
58        {
59            let content = process_demo(include_bytes!("../examples/bounce.bas"));
60            let metadata = Metadata {
61                date: time::OffsetDateTime::from_unix_timestamp(1780142400).unwrap(),
62                length: content.len() as u64,
63            };
64            demos.insert("BOUNCE.BAS", (metadata, content));
65        }
66        {
67            let content = process_demo(include_bytes!("../examples/fibonacci.bas"));
68            let metadata = Metadata {
69                date: time::OffsetDateTime::from_unix_timestamp(1719672741).unwrap(),
70                length: content.len() as u64,
71            };
72            demos.insert("FIBONACCI.BAS", (metadata, content));
73        }
74        {
75            let content = process_demo(include_bytes!("../examples/guess.bas"));
76            let metadata = Metadata {
77                date: time::OffsetDateTime::from_unix_timestamp(1608693152).unwrap(),
78                length: content.len() as u64,
79            };
80            demos.insert("GUESS.BAS", (metadata, content));
81        }
82        {
83            let content = process_demo(include_bytes!("../examples/gpio.bas"));
84            let metadata = Metadata {
85                date: time::OffsetDateTime::from_unix_timestamp(1613316558).unwrap(),
86                length: content.len() as u64,
87            };
88            demos.insert("GPIO.BAS", (metadata, content));
89        }
90        {
91            let content = process_demo(include_bytes!("../examples/hello.bas"));
92            let metadata = Metadata {
93                date: time::OffsetDateTime::from_unix_timestamp(1608646800).unwrap(),
94                length: content.len() as u64,
95            };
96            demos.insert("HELLO.BAS", (metadata, content));
97        }
98        {
99            let content = process_demo(include_bytes!("../examples/palette.bas"));
100            let metadata = Metadata {
101                date: time::OffsetDateTime::from_unix_timestamp(1671243940).unwrap(),
102                length: content.len() as u64,
103            };
104            demos.insert("PALETTE.BAS", (metadata, content));
105        }
106        {
107            let content = process_demo(include_bytes!("../examples/tour.bas"));
108            let metadata = Metadata {
109                date: time::OffsetDateTime::from_unix_timestamp(1608774770).unwrap(),
110                length: content.len() as u64,
111            };
112            demos.insert("TOUR.BAS", (metadata, content));
113        }
114        Self { demos }
115    }
116}
117
118#[async_trait(?Send)]
119impl Drive for DemosDrive {
120    async fn delete(&mut self, _name: &str) -> io::Result<()> {
121        Err(io::Error::new(io::ErrorKind::PermissionDenied, "The demos drive is read-only"))
122    }
123
124    async fn enumerate(&self) -> io::Result<DriveFiles> {
125        let mut entries = BTreeMap::new();
126        let mut bytes = 0;
127        for (name, (metadata, content)) in self.demos.iter() {
128            entries.insert(name.to_string(), metadata.clone());
129            bytes += content.len();
130        }
131        let files = self.demos.len();
132
133        let disk_quota = if bytes <= u64::MAX as usize && files <= u64::MAX as usize {
134            Some(DiskSpace::new(bytes as u64, files as u64))
135        } else {
136            // Cannot represent the amount of disk within a DiskSpace.
137            None
138        };
139        let disk_free = Some(DiskSpace::new(0, 0));
140
141        Ok(DriveFiles::new(entries, disk_quota, disk_free))
142    }
143
144    async fn get(&self, name: &str) -> io::Result<Vec<u8>> {
145        let uc_name = name.to_ascii_uppercase();
146        match self.demos.get(&uc_name.as_ref()) {
147            Some(value) => {
148                let (_metadata, content) = value;
149                Ok(content.as_bytes().to_owned())
150            }
151            None => Err(io::Error::new(io::ErrorKind::NotFound, "Demo not found")),
152        }
153    }
154
155    async fn put(&mut self, _name: &str, _content: &[u8]) -> io::Result<()> {
156        Err(io::Error::new(io::ErrorKind::PermissionDenied, "The demos drive is read-only"))
157    }
158}
159
160/// Factory for demo drives.
161#[derive(Default)]
162pub struct DemoDriveFactory {}
163
164impl DriveFactory for DemoDriveFactory {
165    fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
166        if target.is_empty() {
167            Ok(Box::from(DemosDrive::default()))
168        } else {
169            Err(io::Error::new(
170                io::ErrorKind::InvalidInput,
171                "Cannot specify a path to mount a demos drive",
172            ))
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use futures_lite::future::block_on;
181
182    #[test]
183    fn test_demos_drive_delete() {
184        let mut drive = DemosDrive::default();
185
186        assert_eq!(
187            io::ErrorKind::PermissionDenied,
188            block_on(drive.delete("hello.bas")).unwrap_err().kind()
189        );
190        assert_eq!(
191            io::ErrorKind::PermissionDenied,
192            block_on(drive.delete("Hello.BAS")).unwrap_err().kind()
193        );
194
195        assert_eq!(
196            io::ErrorKind::PermissionDenied,
197            block_on(drive.delete("unknown.bas")).unwrap_err().kind()
198        );
199    }
200
201    #[test]
202    fn test_demos_drive_enumerate() {
203        let drive = DemosDrive::default();
204
205        let files = block_on(drive.enumerate()).unwrap();
206        assert!(files.dirents().contains_key("ALARM.BAS"));
207        assert!(files.dirents().contains_key("BOUNCE.BAS"));
208        assert!(files.dirents().contains_key("FIBONACCI.BAS"));
209        assert!(files.dirents().contains_key("GPIO.BAS"));
210        assert!(files.dirents().contains_key("GUESS.BAS"));
211        assert!(files.dirents().contains_key("HELLO.BAS"));
212        assert!(files.dirents().contains_key("PALETTE.BAS"));
213        assert!(files.dirents().contains_key("TOUR.BAS"));
214
215        assert!(files.disk_quota().unwrap().bytes() > 0);
216        assert_eq!(8, files.disk_quota().unwrap().files());
217        assert_eq!(DiskSpace::new(0, 0), files.disk_free().unwrap());
218    }
219
220    #[test]
221    fn test_demos_drive_get() {
222        let drive = DemosDrive::default();
223
224        assert_eq!(io::ErrorKind::NotFound, block_on(drive.get("unknown.bas")).unwrap_err().kind());
225
226        assert_eq!(
227            process_demo(include_bytes!("../examples/hello.bas")).as_bytes(),
228            block_on(drive.get("hello.bas")).unwrap().as_slice()
229        );
230        assert_eq!(
231            process_demo(include_bytes!("../examples/hello.bas")).as_bytes(),
232            block_on(drive.get("Hello.Bas")).unwrap().as_slice()
233        );
234    }
235
236    #[test]
237    fn test_demos_drive_put() {
238        let mut drive = DemosDrive::default();
239
240        assert_eq!(
241            io::ErrorKind::PermissionDenied,
242            block_on(drive.put("hello.bas", b"")).unwrap_err().kind()
243        );
244        assert_eq!(
245            io::ErrorKind::PermissionDenied,
246            block_on(drive.put("Hello.BAS", b"")).unwrap_err().kind()
247        );
248
249        assert_eq!(
250            io::ErrorKind::PermissionDenied,
251            block_on(drive.put("unknown.bas", b"")).unwrap_err().kind()
252        );
253    }
254
255    #[test]
256    fn test_demos_drive_system_path() {
257        let drive = DemosDrive::default();
258        assert!(drive.system_path("foo").is_none());
259    }
260}