kcode_k1_web_code_runtime/
lib.rs1use 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}