Skip to main content

loadsmith_registry/registries/
local.rs

1use 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}