Skip to main content

kcode_k1_web_code_runtime/
lib.rs

1use kcode_k1_transaction_id::TxId;
2use kcode_k1_web_coding::{K1WebCoding, WebCodingConfig, WebCodingError, WebCodingRevisions};
3use kcode_k1_web_podman::WebPodmanConfig;
4use kcode_k1_web_projection::K1WebProjection;
5use std::collections::HashMap;
6use std::fmt::{Display, Formatter, Write};
7use std::fs;
8use std::io::ErrorKind;
9use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11
12type UserSlot = Arc<Mutex<Option<K1WebCoding>>>;
13
14#[derive(Clone)]
15pub struct RuntimeRevisions {
16    pub boot: String,
17    pub schema: String,
18    pub route: String,
19    pub harness: String,
20    pub check_policy: String,
21}
22
23#[derive(Clone)]
24pub struct RuntimeConfig {
25    cache_root: PathBuf,
26    projection_root: PathBuf,
27    revisions: RuntimeRevisions,
28    podman: WebPodmanConfig,
29}
30
31impl RuntimeConfig {
32    pub fn new(
33        cache_root: PathBuf,
34        projection_root: PathBuf,
35        revisions: RuntimeRevisions,
36        podman: WebPodmanConfig,
37    ) -> Self {
38        Self {
39            cache_root,
40            projection_root,
41            revisions,
42            podman,
43        }
44    }
45}
46
47#[derive(Debug)]
48pub enum RuntimeError {
49    Coding(WebCodingError),
50    LockPoisoned(String),
51}
52
53impl Display for RuntimeError {
54    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Coding(error) => Display::fmt(error, formatter),
57            Self::LockPoisoned(message) => formatter.write_str(message),
58        }
59    }
60}
61
62impl std::error::Error for RuntimeError {
63    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
64        match self {
65            Self::Coding(error) => Some(error),
66            Self::LockPoisoned(_) => None,
67        }
68    }
69}
70
71impl From<WebCodingError> for RuntimeError {
72    fn from(error: WebCodingError) -> Self {
73        Self::Coding(error)
74    }
75}
76
77struct Inner {
78    config: RuntimeConfig,
79    projection: Arc<K1WebProjection>,
80    slots: Mutex<HashMap<[u8; 12], UserSlot>>,
81}
82
83#[derive(Clone)]
84pub struct K1WebCodeRuntime {
85    inner: Arc<Inner>,
86}
87
88impl K1WebCodeRuntime {
89    pub fn new(config: RuntimeConfig, projection: Arc<K1WebProjection>) -> Self {
90        Self {
91            inner: Arc::new(Inner {
92                config,
93                projection,
94                slots: Mutex::new(HashMap::new()),
95            }),
96        }
97    }
98
99    pub fn with_user<T>(
100        &self,
101        user: [u8; 12],
102        operation: impl FnOnce(&mut K1WebCoding) -> Result<T, WebCodingError>,
103    ) -> Result<T, RuntimeError> {
104        let slot = {
105            let mut slots = self.inner.slots.lock().map_err(|_| {
106                RuntimeError::LockPoisoned("runtime slot map mutex poisoned".to_owned())
107            })?;
108            slots
109                .entry(user)
110                .or_insert_with(|| Arc::new(Mutex::new(None)))
111                .clone()
112        };
113
114        let mut coding = slot
115            .lock()
116            .map_err(|_| RuntimeError::LockPoisoned("user coding mutex poisoned".to_owned()))?;
117        if coding.is_none() {
118            *coding = Some(self.open_user(user)?);
119        }
120        operation(coding.as_mut().expect("coding was initialized")).map_err(RuntimeError::Coding)
121    }
122
123    fn open_user(&self, user: [u8; 12]) -> Result<K1WebCoding, WebCodingError> {
124        let revisions = &self.inner.config.revisions;
125        let config = WebCodingConfig::new(
126            WebCodingRevisions {
127                boot: revisions.boot.clone(),
128                schema: revisions.schema.clone(),
129                route: revisions.route.clone(),
130                harness: revisions.harness.clone(),
131                check_policy: revisions.check_policy.clone(),
132            },
133            self.inner.config.projection_root.clone(),
134            self.inner.config.podman.clone(),
135        )?;
136        let cache_root = self.inner.config.cache_root.join(cache_directory(user));
137        ensure_user_cache_directory(&cache_root)?;
138        K1WebCoding::open(
139            cache_root,
140            TxId::from_bytes(user),
141            config,
142            self.inner.projection.clone(),
143        )
144    }
145}
146
147fn ensure_user_cache_directory(path: &Path) -> Result<(), WebCodingError> {
148    match fs::symlink_metadata(path) {
149        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
150        Ok(_) => Err(WebCodingError::State(format!(
151            "user cache path is not a real directory: {}",
152            path.display()
153        ))),
154        Err(cause) if cause.kind() == ErrorKind::NotFound => {
155            fs::create_dir(path).map_err(|cause| {
156                WebCodingError::State(format!(
157                    "create user cache directory {}: {cause}",
158                    path.display()
159                ))
160            })?;
161            Ok(())
162        }
163        Err(cause) => Err(WebCodingError::State(format!(
164            "inspect user cache path {}: {cause}",
165            path.display()
166        ))),
167    }
168}
169
170fn cache_directory(user: [u8; 12]) -> String {
171    let mut directory = String::with_capacity(24);
172    for byte in user {
173        write!(&mut directory, "{byte:02x}").expect("writing to String cannot fail");
174    }
175    directory
176}
177
178#[cfg(test)]
179mod tests {
180    use super::{cache_directory, ensure_user_cache_directory};
181    use kcode_k1_web_coding::WebCodingError;
182    use std::fs;
183
184    #[test]
185    fn cache_directory_is_exact_lowercase_user_hex() {
186        assert_eq!(
187            cache_directory([
188                0x00, 0x01, 0x0a, 0x10, 0x2f, 0x7f, 0x80, 0xab, 0xcd, 0xef, 0xf0, 0xff,
189            ]),
190            "00010a102f7f80abcdeff0ff"
191        );
192    }
193
194    #[test]
195    fn missing_user_cache_directory_is_created() {
196        let root = tempfile::tempdir().unwrap();
197        let user = root.path().join("00112233445566778899aabb");
198
199        ensure_user_cache_directory(&user).unwrap();
200
201        let metadata = fs::symlink_metadata(user).unwrap();
202        assert!(metadata.is_dir());
203        assert!(!metadata.file_type().is_symlink());
204    }
205
206    #[test]
207    fn user_cache_file_is_rejected() {
208        let root = tempfile::tempdir().unwrap();
209        let user = root.path().join("00112233445566778899aabb");
210        fs::write(&user, b"not a directory").unwrap();
211
212        let error = ensure_user_cache_directory(&user).unwrap_err();
213
214        assert!(
215            matches!(error, WebCodingError::State(message) if message.contains("not a real directory"))
216        );
217    }
218
219    #[cfg(unix)]
220    #[test]
221    fn user_cache_symlink_is_rejected() {
222        let root = tempfile::tempdir().unwrap();
223        let target = root.path().join("target");
224        let user = root.path().join("00112233445566778899aabb");
225        fs::create_dir(&target).unwrap();
226        std::os::unix::fs::symlink(target, &user).unwrap();
227
228        let error = ensure_user_cache_directory(&user).unwrap_err();
229
230        assert!(
231            matches!(error, WebCodingError::State(message) if message.contains("not a real directory"))
232        );
233    }
234}