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)]
12pub struct LocalRegistry;
13
14impl Default for LocalRegistry {
15 fn default() -> Self {
16 Self::new()
17 }
18}
19
20impl LocalRegistry {
21 pub fn new() -> Self {
22 Self
23 }
24}
25
26impl Registry for LocalRegistry {
27 fn version_info<'a>(
28 &'a self,
29 id: &'a PackageId,
30 metadata: Option<&'a serde_json::Value>,
31 ) -> Pin<Box<dyn Future<Output = Result<Vec<VersionInfo>>> + 'a>> {
32 Box::pin(async move {
33 let metadata = crate::read_metadata(metadata)?;
34 let source = Source::read(id, metadata)?;
35
36 Ok(vec![VersionInfo {
37 version: source.version().clone(),
38 }])
39 })
40 }
41
42 fn resolve<'a>(
43 &'a self,
44 ref_: &'a PackageRef,
45 metadata: Option<&'a serde_json::Value>,
46 ) -> Pin<Box<dyn Future<Output = Result<ResolvedVersion>> + 'a>> {
47 Box::pin(async move {
48 let metadata = crate::read_metadata(metadata)?;
49
50 let source = Source::read_ref(ref_, metadata)?;
51
52 let full_path = source.path.canonicalize_utf8()?;
53
54 Ok(ResolvedVersion {
55 url: full_path.into(),
56 size: source.size()?,
57 deps: source.dependencies()?,
58 checksum: source.checksum()?,
59 })
60 })
61 }
62
63 fn revalidate_checksum<'a>(
64 &'a self,
65 ref_: &'a PackageRef,
66 metadata: Option<&'a serde_json::Value>,
67 ) -> Result<Option<loadsmith_core::Checksum>> {
68 let metadata = crate::read_metadata(metadata)?;
69 let source = Source::read_ref(ref_, metadata)?;
70 let checksum = source.checksum()?;
71
72 Ok(checksum)
73 }
74}
75
76#[derive(Debug, Deserialize)]
77struct Metadata {
78 path: Utf8PathBuf,
79 #[serde(
80 default,
81 skip_serializing_if = "Option::is_none",
82 rename = "deps_source"
83 )]
84 dependency_registry: Option<String>,
85 #[serde(
86 default,
87 skip_serializing_if = "Option::is_none",
88 rename = "source_version"
89 )]
90 source_version: Option<Version>,
91}
92
93struct Source {
94 path: Utf8PathBuf,
95 kind: SourceKind,
96 dependency_registry: String,
97}
98
99enum SourceKind {
100 Zip(ThunderstoreManifest),
101 Directory(ThunderstoreManifest),
102 Dll(Version),
103 Other(Version),
104}
105
106impl Source {
107 const DEFAULT_DEPENDENCY_REGISTRY: &'static str = "thunderstore";
108
109 fn read(id: &PackageId, metadata: Metadata) -> Result<Self> {
110 if !metadata.path.exists() {
111 return Err(Error::FileNotFound(metadata.path));
112 }
113
114 let kind = match (metadata.path.is_file(), metadata.path.extension()) {
115 (true, Some("zip")) => {
116 let manifest = read_zip_manifest(&metadata.path)?;
117
118 if let Some(namespace) = &manifest.namespace {
119 let ident = format!("{}-{}", namespace, manifest.name);
120 if ident != id.as_str() {
121 return Err(Error::PackageNotFound);
122 }
123 } else if manifest.name != id.as_str() {
124 return Err(Error::PackageNotFound);
125 }
126
127 Self::warn_if_source_version_set(&manifest.version_number, &metadata);
128
129 SourceKind::Zip(manifest)
130 }
131 (true, Some("dll")) => {
132 SourceKind::Dll(metadata.source_version.ok_or(Error::LocalVersionMissing)?)
133 }
134 (false, _) => {
135 let manifest_path = metadata.path.join("manifest.json");
136 if !manifest_path.exists() {
137 return Err(Error::LocalManifestMissing);
138 }
139
140 let manifest_str = std::fs::read_to_string(&manifest_path)?;
141 let manifest: ThunderstoreManifest = serde_json::from_str(&manifest_str)?;
142
143 Self::warn_if_source_version_set(&manifest.version_number, &metadata);
144
145 SourceKind::Directory(manifest)
146 }
147 _ => {
148 debug!(path = %metadata.path, "could not determine source kind");
149
150 SourceKind::Other(metadata.source_version.ok_or(Error::LocalVersionMissing)?)
151 }
152 };
153
154 let dependency_registry = metadata
155 .dependency_registry
156 .unwrap_or_else(|| Self::DEFAULT_DEPENDENCY_REGISTRY.to_string());
157
158 Ok(Source {
159 kind,
160 path: metadata.path,
161 dependency_registry,
162 })
163 }
164
165 fn warn_if_source_version_set(package_version: &Version, metadata: &Metadata) {
166 if let Some(source_version) = &metadata.source_version {
167 debug!(
168 path = %metadata.path,
169 %source_version,
170 %package_version,
171 "source_version is set, but is overriden by the local package's version"
172 );
173 }
174 }
175
176 fn read_ref(ref_: &PackageRef, metadata: Metadata) -> Result<Self> {
177 let source = Self::read(ref_.id(), metadata)?;
178
179 if source.version() != ref_.version() {
180 return Err(Error::VersionNotFound);
181 }
182
183 Ok(source)
184 }
185
186 fn version(&self) -> &Version {
187 match &self.kind {
188 SourceKind::Zip(manifest) | SourceKind::Directory(manifest) => &manifest.version_number,
189 SourceKind::Dll(version) | SourceKind::Other(version) => version,
190 }
191 }
192
193 fn size(&self) -> Result<Option<u64>> {
194 match &self.kind {
195 SourceKind::Zip(_) | SourceKind::Dll(_) | SourceKind::Other(_) => {
196 let size = std::fs::metadata(&self.path)?.len();
197 Ok(Some(size))
198 }
199 SourceKind::Directory(_) => Ok(None),
200 }
201 }
202
203 fn dependencies(&self) -> Result<Vec<Dependency>> {
204 match &self.kind {
205 SourceKind::Zip(manifest) | SourceKind::Directory(manifest) => {
206 let deps = manifest
207 .dependencies
208 .iter()
209 .map(|ident| {
210 let package_id = PackageId::new(ident.package_id().into_string());
211
212 Dependency::new(
213 package_id,
214 VersionReq::STAR,
215 self.dependency_registry.clone(),
216 )
217 })
218 .collect();
219
220 Ok(deps)
221 }
222 SourceKind::Dll(_) | SourceKind::Other(_) => Ok(Vec::new()),
223 }
224 }
225
226 fn checksum(&self) -> Result<Option<loadsmith_core::Checksum>> {
227 match self.kind {
228 SourceKind::Zip(_) | SourceKind::Dll(_) | SourceKind::Other(_) => {
229 let hash = loadsmith_util::hash_file(&self.path)?;
230 Ok(Some(hash.into()))
231 }
232 SourceKind::Directory(_) => Ok(None),
233 }
234 }
235}
236
237#[derive(Debug, Deserialize)]
238struct ThunderstoreManifest {
239 #[serde(default)]
240 namespace: Option<String>,
241 name: String,
242 #[allow(unused)]
243 description: String,
244 version_number: Version,
245 dependencies: Vec<VersionIdent>,
246 #[allow(unused)]
247 website_url: Option<String>,
248}
249
250fn read_zip_manifest(path: impl AsRef<Path>) -> Result<ThunderstoreManifest> {
251 let file = File::open(path.as_ref()).map(BufReader::new)?;
252 let mut archive = zip::ZipArchive::new(file)?;
253
254 let mut manifest_file = archive.by_name("manifest.json").map_err(|err| match err {
255 zip::result::ZipError::FileNotFound => Error::LocalManifestMissing,
256 other => Error::Zip(other),
257 })?;
258 let manifest: ThunderstoreManifest = serde_json::from_reader(&mut manifest_file)?;
259
260 Ok(manifest)
261}