1mod model;
4mod repository;
5mod sandbox;
6
7use std::fmt;
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15pub use model::{
16 CheckResult, CheckStage, CheckStageResult, Error, Result, RustLibFile, RustLibPath,
17};
18use repository::{RepositorySnapshot, read_repository, validate_name, write_repository};
19
20static WORK_COUNTER: AtomicU64 = AtomicU64::new(0);
21
22#[derive(Clone)]
24pub struct KcodeRustLibs {
25 rust_libs_root: PathBuf,
26 shared: Arc<Shared>,
27}
28
29impl KcodeRustLibs {
30 pub fn new(
32 rust_libs_root: impl AsRef<Path>,
33 crates_io_registry_token: impl AsRef<str>,
34 ) -> Result<Self> {
35 let token = RegistryToken::new(crates_io_registry_token.as_ref())?;
36 let requested_root = absolute_path(rust_libs_root.as_ref())?;
37 fs::create_dir_all(&requested_root)
38 .map_err(|source| Error::io("create Rust libraries root", &requested_root, source))?;
39 let rust_libs_root = fs::canonicalize(&requested_root).map_err(|source| {
40 Error::io("canonicalize Rust libraries root", &requested_root, source)
41 })?;
42 if !fs::metadata(&rust_libs_root)
43 .map_err(|source| Error::io("inspect Rust libraries root", &rust_libs_root, source))?
44 .is_dir()
45 {
46 return Err(Error::RustLibIsNotDirectory(
47 rust_libs_root.display().to_string(),
48 ));
49 }
50
51 let work_root = WorkRoot::new()?;
52 if paths_overlap(&rust_libs_root, work_root.path()) {
53 return Err(Error::RootsOverlap {
54 rust_libs_root,
55 work_root: work_root.path().to_path_buf(),
56 });
57 }
58 Ok(Self {
59 rust_libs_root,
60 shared: Arc::new(Shared { token, work_root }),
61 })
62 }
63
64 pub fn create_rust_lib(&self, name: &str) -> Result<OpenedRustLib> {
66 validate_name(name)?;
67 let root = self.rust_libs_root.join(name);
68 match fs::symlink_metadata(&root) {
69 Ok(_) => return Err(Error::RustLibAlreadyExists(name.to_owned())),
70 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
71 Err(source) => {
72 return Err(Error::io("inspect Rust library destination", &root, source));
73 }
74 }
75 fs::create_dir(&root).map_err(|source| Error::io("create Rust library", &root, source))?;
76 let creation = (|| {
77 let source = root.join("src");
78 fs::create_dir(&source)
79 .map_err(|error| Error::io("create source directory", &source, error))?;
80 let manifest = format!(
81 "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n"
82 );
83 fs::write(root.join("Cargo.toml"), manifest).map_err(|error| {
84 Error::io("write initial manifest", root.join("Cargo.toml"), error)
85 })?;
86 fs::write(root.join("Documentation.md"), "").map_err(|error| {
87 Error::io(
88 "write initial documentation",
89 root.join("Documentation.md"),
90 error,
91 )
92 })?;
93 fs::write(source.join("lib.rs"), "")
94 .map_err(|error| Error::io("write initial source", source.join("lib.rs"), error))?;
95 self.open_rust_lib(name)
96 })();
97 if creation.is_err() {
98 let _ = fs::remove_dir_all(&root);
99 }
100 creation
101 }
102
103 pub fn open_rust_lib(&self, name: &str) -> Result<OpenedRustLib> {
105 validate_name(name)?;
106 let root = self.rust_libs_root.join(name);
107 let metadata = match fs::symlink_metadata(&root) {
108 Ok(metadata) => metadata,
109 Err(source) if source.kind() == io::ErrorKind::NotFound => {
110 return Err(Error::RustLibNotFound(name.to_owned()));
111 }
112 Err(source) => return Err(Error::io("inspect Rust library", &root, source)),
113 };
114 if metadata.file_type().is_symlink() {
115 return Err(Error::SymlinkNotAllowed(root));
116 }
117 if !metadata.is_dir() {
118 return Err(Error::RustLibIsNotDirectory(name.to_owned()));
119 }
120 let snapshot = read_repository(&root)?;
121 Ok(OpenedRustLib {
122 name: name.to_owned(),
123 root,
124 snapshot,
125 shared: Arc::clone(&self.shared),
126 })
127 }
128}
129
130impl fmt::Debug for KcodeRustLibs {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.debug_struct("KcodeRustLibs")
133 .field("rust_libs_root", &self.rust_libs_root)
134 .field("registry_token", &"[REDACTED]")
135 .finish_non_exhaustive()
136 }
137}
138
139pub struct OpenedRustLib {
141 name: String,
142 root: PathBuf,
143 snapshot: RepositorySnapshot,
144 shared: Arc<Shared>,
145}
146
147impl OpenedRustLib {
148 pub fn name(&self) -> &str {
149 &self.name
150 }
151
152 pub fn version(&self) -> &str {
154 &self.snapshot.version
155 }
156
157 pub fn files(&self) -> &[RustLibFile] {
159 &self.snapshot.files
160 }
161
162 pub fn write(&mut self, files: &[RustLibFile]) -> Result<()> {
164 self.snapshot = write_repository(&self.root, &self.snapshot.files, files)?;
165 Ok(())
166 }
167
168 pub fn check(&self) -> Result<CheckResult> {
170 sandbox::check_repository(&self.root, self.shared.work_root.path())
171 }
172
173 pub fn publish(&self) -> Result<()> {
175 let check = self.check()?;
176 if !check.passed() {
177 return Err(Error::CheckFailed(check));
178 }
179 sandbox::publish_repository(
180 &self.root,
181 self.shared.work_root.path(),
182 self.shared.token.expose(),
183 )
184 }
185}
186
187struct Shared {
188 token: RegistryToken,
189 work_root: WorkRoot,
190}
191
192struct RegistryToken(Arc<str>);
193
194impl RegistryToken {
195 fn new(value: &str) -> Result<Self> {
196 let value = value.trim();
197 if value.is_empty() {
198 return Err(Error::InvalidRegistryToken);
199 }
200 Ok(Self(Arc::from(value)))
201 }
202
203 fn expose(&self) -> &str {
204 &self.0
205 }
206}
207
208impl fmt::Debug for RegistryToken {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str("[REDACTED]")
211 }
212}
213
214struct WorkRoot(PathBuf);
215
216impl WorkRoot {
217 fn new() -> Result<Self> {
218 let parent = std::env::temp_dir().join("kcode-rust-libs-v2");
219 fs::create_dir_all(&parent)
220 .map_err(|source| Error::io("create temporary work parent", &parent, source))?;
221 for _ in 0..100 {
222 let timestamp = SystemTime::now()
223 .duration_since(UNIX_EPOCH)
224 .unwrap_or_default()
225 .as_nanos();
226 let counter = WORK_COUNTER.fetch_add(1, Ordering::Relaxed);
227 let path = parent.join(format!(
228 "process-{}-{timestamp}-{counter}",
229 std::process::id()
230 ));
231 match fs::create_dir(&path) {
232 Ok(()) => {
233 let canonical = fs::canonicalize(&path).map_err(|source| {
234 Error::io("canonicalize temporary work root", &path, source)
235 })?;
236 return Ok(Self(canonical));
237 }
238 Err(source) if source.kind() == io::ErrorKind::AlreadyExists => continue,
239 Err(source) => {
240 return Err(Error::io("create temporary work root", path, source));
241 }
242 }
243 }
244 Err(Error::Sandbox {
245 stage: "prepare-work-root".to_owned(),
246 message: "could not allocate a unique process work root".to_owned(),
247 })
248 }
249
250 fn path(&self) -> &Path {
251 &self.0
252 }
253}
254
255impl Drop for WorkRoot {
256 fn drop(&mut self) {
257 let _ = fs::remove_dir_all(&self.0);
258 }
259}
260
261fn absolute_path(path: &Path) -> Result<PathBuf> {
262 if path.is_absolute() {
263 Ok(path.to_path_buf())
264 } else {
265 let current = std::env::current_dir()
266 .map_err(|source| Error::io("read current directory", ".", source))?;
267 Ok(current.join(path))
268 }
269}
270
271fn paths_overlap(left: &Path, right: &Path) -> bool {
272 left.starts_with(right) || right.starts_with(left)
273}