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/fibonacci.bas"));
52            let metadata = Metadata {
53                date: time::OffsetDateTime::from_unix_timestamp(1719672741).unwrap(),
54                length: content.len() as u64,
55            };
56            demos.insert("FIBONACCI.BAS", (metadata, content));
57        }
58        {
59            let content = process_demo(include_bytes!("../examples/guess.bas"));
60            let metadata = Metadata {
61                date: time::OffsetDateTime::from_unix_timestamp(1608693152).unwrap(),
62                length: content.len() as u64,
63            };
64            demos.insert("GUESS.BAS", (metadata, content));
65        }
66        {
67            let content = process_demo(include_bytes!("../examples/gpio.bas"));
68            let metadata = Metadata {
69                date: time::OffsetDateTime::from_unix_timestamp(1613316558).unwrap(),
70                length: content.len() as u64,
71            };
72            demos.insert("GPIO.BAS", (metadata, content));
73        }
74        {
75            let content = process_demo(include_bytes!("../examples/hello.bas"));
76            let metadata = Metadata {
77                date: time::OffsetDateTime::from_unix_timestamp(1608646800).unwrap(),
78                length: content.len() as u64,
79            };
80            demos.insert("HELLO.BAS", (metadata, content));
81        }
82        {
83            let content = process_demo(include_bytes!("../examples/palette.bas"));
84            let metadata = Metadata {
85                date: time::OffsetDateTime::from_unix_timestamp(1671243940).unwrap(),
86                length: content.len() as u64,
87            };
88            demos.insert("PALETTE.BAS", (metadata, content));
89        }
90        {
91            let content = process_demo(include_bytes!("../examples/tour.bas"));
92            let metadata = Metadata {
93                date: time::OffsetDateTime::from_unix_timestamp(1608774770).unwrap(),
94                length: content.len() as u64,
95            };
96            demos.insert("TOUR.BAS", (metadata, content));
97        }
98        Self { demos }
99    }
100}
101
102#[async_trait(?Send)]
103impl Drive for DemosDrive {
104    async fn delete(&mut self, _name: &str) -> io::Result<()> {
105        Err(io::Error::new(io::ErrorKind::PermissionDenied, "The demos drive is read-only"))
106    }
107
108    async fn enumerate(&self) -> io::Result<DriveFiles> {
109        let mut entries = BTreeMap::new();
110        let mut bytes = 0;
111        for (name, (metadata, content)) in self.demos.iter() {
112            entries.insert(name.to_string(), metadata.clone());
113            bytes += content.len();
114        }
115        let files = self.demos.len();
116
117        let disk_quota = if bytes <= u64::MAX as usize && files <= u64::MAX as usize {
118            Some(DiskSpace::new(bytes as u64, files as u64))
119        } else {
120            // Cannot represent the amount of disk within a DiskSpace.
121            None
122        };
123        let disk_free = Some(DiskSpace::new(0, 0));
124
125        Ok(DriveFiles::new(entries, disk_quota, disk_free))
126    }
127
128    async fn get(&self, name: &str) -> io::Result<String> {
129        let uc_name = name.to_ascii_uppercase();
130        match self.demos.get(&uc_name.as_ref()) {
131            Some(value) => {
132                let (_metadata, content) = value;
133                Ok(content.to_string())
134            }
135            None => Err(io::Error::new(io::ErrorKind::NotFound, "Demo not found")),
136        }
137    }
138
139    async fn put(&mut self, _name: &str, _content: &str) -> io::Result<()> {
140        Err(io::Error::new(io::ErrorKind::PermissionDenied, "The demos drive is read-only"))
141    }
142}
143
144/// Factory for demo drives.
145#[derive(Default)]
146pub struct DemoDriveFactory {}
147
148impl DriveFactory for DemoDriveFactory {
149    fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
150        if target.is_empty() {
151            Ok(Box::from(DemosDrive::default()))
152        } else {
153            Err(io::Error::new(
154                io::ErrorKind::InvalidInput,
155                "Cannot specify a path to mount a demos drive",
156            ))
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use futures_lite::future::block_on;
165
166    #[test]
167    fn test_demos_drive_delete() {
168        let mut drive = DemosDrive::default();
169
170        assert_eq!(
171            io::ErrorKind::PermissionDenied,
172            block_on(drive.delete("hello.bas")).unwrap_err().kind()
173        );
174        assert_eq!(
175            io::ErrorKind::PermissionDenied,
176            block_on(drive.delete("Hello.BAS")).unwrap_err().kind()
177        );
178
179        assert_eq!(
180            io::ErrorKind::PermissionDenied,
181            block_on(drive.delete("unknown.bas")).unwrap_err().kind()
182        );
183    }
184
185    #[test]
186    fn test_demos_drive_enumerate() {
187        let drive = DemosDrive::default();
188
189        let files = block_on(drive.enumerate()).unwrap();
190        assert!(files.dirents().contains_key("FIBONACCI.BAS"));
191        assert!(files.dirents().contains_key("GPIO.BAS"));
192        assert!(files.dirents().contains_key("GUESS.BAS"));
193        assert!(files.dirents().contains_key("HELLO.BAS"));
194        assert!(files.dirents().contains_key("PALETTE.BAS"));
195        assert!(files.dirents().contains_key("TOUR.BAS"));
196
197        assert!(files.disk_quota().unwrap().bytes() > 0);
198        assert_eq!(6, files.disk_quota().unwrap().files());
199        assert_eq!(DiskSpace::new(0, 0), files.disk_free().unwrap());
200    }
201
202    #[test]
203    fn test_demos_drive_get() {
204        let drive = DemosDrive::default();
205
206        assert_eq!(io::ErrorKind::NotFound, block_on(drive.get("unknown.bas")).unwrap_err().kind());
207
208        assert_eq!(
209            process_demo(include_bytes!("../examples/hello.bas")),
210            block_on(drive.get("hello.bas")).unwrap()
211        );
212        assert_eq!(
213            process_demo(include_bytes!("../examples/hello.bas")),
214            block_on(drive.get("Hello.Bas")).unwrap()
215        );
216    }
217
218    #[test]
219    fn test_demos_drive_put() {
220        let mut drive = DemosDrive::default();
221
222        assert_eq!(
223            io::ErrorKind::PermissionDenied,
224            block_on(drive.put("hello.bas", "")).unwrap_err().kind()
225        );
226        assert_eq!(
227            io::ErrorKind::PermissionDenied,
228            block_on(drive.put("Hello.BAS", "")).unwrap_err().kind()
229        );
230
231        assert_eq!(
232            io::ErrorKind::PermissionDenied,
233            block_on(drive.put("unknown.bas", "")).unwrap_err().kind()
234        );
235    }
236
237    #[test]
238    fn test_demos_drive_system_path() {
239        let drive = DemosDrive::default();
240        assert!(drive.system_path("foo").is_none());
241    }
242}