loadsmith_registry/registries/
local.rs1use std::{fs::File, io::BufReader, path::Path, pin::Pin};
2
3use camino::Utf8PathBuf;
4use loadsmith_core::{Dependency, PackageId, PackageRef, Version, VersionReq};
5use serde::Deserialize;
6use thunderstore::VersionIdent;
7use tracing::debug;
8
9use crate::{Error, Registry, ResolvedVersion, Result, VersionInfo};
10
11#[derive(Debug)]
30pub struct LocalRegistry;
31
32impl Default for LocalRegistry {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl LocalRegistry {
39 pub fn new() -> Self {
41 Self
42 }
43
44 pub fn read(&self, metadata: Metadata) -> Result<(Option<PackageId>, VersionInfo)> {
45 let source = Source::read(metadata)?;
46
47 Ok((
48 source.package_id(),
49 VersionInfo {
50 version: source.version().clone(),
51 },
52 ))
53 }
54}
55
56impl Registry for LocalRegistry {
57 fn version_info<'a>(
58 &'a self,
59 id: &'a PackageId,
60 metadata: Option<&'a serde_json::Value>,
61 ) -> Pin<Box<dyn Future<Output = Result<Vec<VersionInfo>>> + 'a>> {
62 Box::pin(async move {
63 let metadata = crate::read_metadata(metadata)?;
64 let source = Source::read(metadata)?;
65
66 source.check_package_id(id)?;
67
68 Ok(vec![VersionInfo {
69 version: source.version().clone(),
70 }])
71 })
72 }
73
74 fn resolve<'a>(
75 &'a self,
76 ref_: &'a PackageRef,
77 metadata: Option<&'a serde_json::Value>,
78 ) -> Pin<Box<dyn Future<Output = Result<ResolvedVersion>> + 'a>> {
79 Box::pin(async move {
80 let metadata = crate::read_metadata(metadata)?;
81 let source = Source::read(metadata)?;
82 source.check_package_ref(ref_)?;
83
84 let full_path = source.path.canonicalize_utf8()?;
85
86 Ok(ResolvedVersion {
87 url: full_path.into(),
88 size: source.size()?,
89 deps: source.dependencies()?,
90 checksum: source.checksum().map(Some)?,
91 })
92 })
93 }
94
95 fn revalidate_checksum<'a>(
96 &'a self,
97 ref_: &'a PackageRef,
98 metadata: Option<&'a serde_json::Value>,
99 ) -> Result<Option<loadsmith_core::Checksum>> {
100 let metadata = crate::read_metadata(metadata)?;
101 let source = Source::read(metadata)?;
102 source.check_package_ref(ref_)?;
103
104 let checksum = source.checksum()?;
105
106 Ok(Some(checksum))
107 }
108}
109
110#[derive(Debug, Deserialize)]
111pub struct Metadata {
112 path: Utf8PathBuf,
113 #[serde(
114 default,
115 skip_serializing_if = "Option::is_none",
116 rename = "deps_source"
117 )]
118 dependency_registry: Option<String>,
119 #[serde(
120 default,
121 skip_serializing_if = "Option::is_none",
122 rename = "source_version"
123 )]
124 source_version: Option<Version>,
125}
126
127struct Source {
128 path: Utf8PathBuf,
129 kind: SourceKind,
130 dependency_registry: String,
131}
132
133enum SourceKind {
134 Zip(ThunderstoreManifest),
135 Directory(ThunderstoreManifest),
136 Dll(Version),
137 Other(Version),
138}
139
140impl Metadata {
141 pub fn new(path: impl Into<Utf8PathBuf>) -> Self {
142 Self {
143 path: path.into(),
144 dependency_registry: None,
145 source_version: None,
146 }
147 }
148
149 pub fn with_dependency_registry(mut self, registry: impl Into<String>) -> Self {
150 self.dependency_registry = Some(registry.into());
151 self
152 }
153
154 pub fn with_source_version(mut self, version: impl Into<Version>) -> Self {
155 self.source_version = Some(version.into());
156 self
157 }
158}
159
160impl Source {
161 const DEFAULT_DEPENDENCY_REGISTRY: &'static str = "thunderstore";
162
163 fn read(metadata: Metadata) -> Result<Self> {
164 if !metadata.path.exists() {
165 return Err(Error::FileNotFound(metadata.path));
166 }
167
168 let kind = match (metadata.path.is_file(), metadata.path.extension()) {
169 (true, Some("zip")) => {
170 let manifest = read_zip_manifest(&metadata.path)?;
171
172 Self::warn_if_source_version_set(&manifest.version_number, &metadata);
173
174 SourceKind::Zip(manifest)
175 }
176 (true, Some("dll")) => {
177 SourceKind::Dll(metadata.source_version.ok_or(Error::LocalVersionMissing)?)
178 }
179 (false, _) => {
180 let manifest_path = metadata.path.join("manifest.json");
181 if !manifest_path.exists() {
182 return Err(Error::LocalManifestMissing);
183 }
184
185 let manifest_str = std::fs::read_to_string(&manifest_path)?;
186 let manifest: ThunderstoreManifest = serde_json::from_str(&manifest_str)?;
187
188 Self::warn_if_source_version_set(&manifest.version_number, &metadata);
189
190 SourceKind::Directory(manifest)
191 }
192 _ => {
193 debug!(path = %metadata.path, "could not determine source kind");
194
195 SourceKind::Other(metadata.source_version.ok_or(Error::LocalVersionMissing)?)
196 }
197 };
198
199 let dependency_registry = metadata
200 .dependency_registry
201 .unwrap_or_else(|| Self::DEFAULT_DEPENDENCY_REGISTRY.to_string());
202
203 Ok(Source {
204 kind,
205 path: metadata.path,
206 dependency_registry,
207 })
208 }
209
210 fn warn_if_source_version_set(package_version: &Version, metadata: &Metadata) {
211 if let Some(source_version) = &metadata.source_version {
212 debug!(
213 path = %metadata.path,
214 %source_version,
215 %package_version,
216 "source_version is set, but is overriden by the local package's version"
217 );
218 }
219 }
220
221 fn check_package_id(&self, id: &PackageId) -> Result<()> {
222 match self.package_id() {
223 Some(package_id) if package_id == *id => Ok(()),
224 Some(_) => Err(Error::PackageNotFound),
225 None => Ok(()),
226 }
227 }
228
229 fn package_id(&self) -> Option<PackageId> {
230 match &self.kind {
231 SourceKind::Zip(manifest) | SourceKind::Directory(manifest) => {
232 let package_id = if let Some(namespace) = &manifest.namespace {
233 format!("{}-{}", namespace, manifest.name)
234 } else {
235 manifest.name.clone()
236 };
237
238 Some(PackageId::new(package_id))
239 }
240 SourceKind::Dll(_) | SourceKind::Other(_) => None,
241 }
242 }
243
244 fn check_package_ref(&self, ref_: &PackageRef) -> Result<()> {
245 self.check_package_id(ref_.id())?;
246
247 if self.version() != ref_.version() {
248 return Err(Error::VersionNotFound);
249 }
250
251 Ok(())
252 }
253
254 fn version(&self) -> &Version {
255 match &self.kind {
256 SourceKind::Zip(manifest) | SourceKind::Directory(manifest) => &manifest.version_number,
257 SourceKind::Dll(version) | SourceKind::Other(version) => version,
258 }
259 }
260
261 fn size(&self) -> Result<Option<u64>> {
262 match &self.kind {
263 SourceKind::Zip(_) | SourceKind::Dll(_) | SourceKind::Other(_) => {
264 let size = std::fs::metadata(&self.path)?.len();
265 Ok(Some(size))
266 }
267 SourceKind::Directory(_) => Ok(None),
268 }
269 }
270
271 fn dependencies(&self) -> Result<Vec<Dependency>> {
272 match &self.kind {
273 SourceKind::Zip(manifest) | SourceKind::Directory(manifest) => {
274 let deps = manifest
275 .dependencies
276 .iter()
277 .map(|ident| {
278 let package_id = PackageId::new(ident.package_id().into_string());
279
280 Dependency::new(
281 package_id,
282 VersionReq::STAR,
283 self.dependency_registry.clone(),
284 )
285 })
286 .collect();
287
288 Ok(deps)
289 }
290 SourceKind::Dll(_) | SourceKind::Other(_) => Ok(Vec::new()),
291 }
292 }
293
294 fn checksum(&self) -> Result<loadsmith_core::Checksum> {
295 loadsmith_core::Checksum::compute_from_path(
296 &self.path,
297 loadsmith_core::ChecksumAlgorithm::Blake3,
298 )
299 .map_err(Into::into)
300 }
301}
302
303#[derive(Debug, Deserialize)]
304struct ThunderstoreManifest {
305 #[serde(default)]
306 namespace: Option<String>,
307 name: String,
308 #[allow(unused)]
309 description: String,
310 version_number: Version,
311 dependencies: Vec<VersionIdent>,
312 #[allow(unused)]
313 website_url: Option<String>,
314}
315
316fn read_zip_manifest(path: impl AsRef<Path>) -> Result<ThunderstoreManifest> {
317 let file = File::open(path.as_ref()).map(BufReader::new)?;
318 let mut archive = zip::ZipArchive::new(file)?;
319
320 let mut manifest_file = archive.by_name("manifest.json").map_err(|err| match err {
321 zip::result::ZipError::FileNotFound => Error::LocalManifestMissing,
322 other => Error::Zip(other),
323 })?;
324 let manifest: ThunderstoreManifest = serde_json::from_reader(&mut manifest_file)?;
325
326 Ok(manifest)
327}