diff options
Diffstat (limited to 'crates/repos/src/lib.rs')
| -rw-r--r-- | crates/repos/src/lib.rs | 679 | +0 −679 |
1 files changed, 0 insertions, 679 deletions
diff --git a/crates/repos/src/lib.rs b/crates/repos/src/lib.rs deleted file mode 100644 --- a/crates/repos/src/lib.rs +++ /dev/null @@ -1,679 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov -// SPDX-License-Identifier: AGPL-3.0-or-later - -pub mod go; -pub mod zig; - -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; - -use async_trait::async_trait; -use bytes::Bytes; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use thiserror::Error; -use url::Url; - -pub use base::ContentType; -pub use go::{GoBackend, GoConfig}; -pub use zig::{ZigBackend, ZigConfig}; - -/// Backend operation error. -#[derive(Debug, Clone, Error)] -pub enum BackendError { - /// Resource not found - #[error("not found")] - NotFound, - - /// Upstream network error - #[error("network: {0}")] - Network(String), - - /// Internal storage error - #[error("storage: {0}")] - Storage(String), - - /// Malformed data from upstream - #[error("upstream: {0}")] - Upstream(String), - - /// Internal logic error - #[error("internal: {0}")] - Internal(String), -} - -/// Operating system -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - strum::Display, - strum::EnumString, - strum::AsRefStr, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum Os { - Linux, - Windows, - Darwin, - #[serde(alias = "macos")] - Macos, - Freebsd, - Netbsd, - Openbsd, - Illumos, - Plan9, - Aix, - Solaris, - Dragonfly, - Android, - Ios, - Js, - Wasip1, - Wasi, -} - -/// CPU architecture -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - strum::Display, - strum::EnumString, - strum::AsRefStr, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum Arch { - Amd64, - #[serde(alias = "x86_64")] - X86_64, - Arm64, - #[serde(alias = "aarch64")] - Aarch64, - #[serde(rename = "386")] - #[strum(serialize = "386")] - I386, - Arm, - Armv6l, - Armv7a, - Loong64, - Mips, - Mips64, - Mips64le, - Mipsle, - Ppc64, - Ppc64le, - Riscv64, - S390x, - Wasm32, - Powerpc, - Powerpc64, - Powerpc64le, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Archive { - TarGz, - TarXz, - Zip, - Msi, - Pkg, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum FileKind { - Source, - Bootstrap, - Archive, - Installer, -} - -/// Version type for ordering. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VersionType { - Stable, - Rc(u64), - Beta(u64), - Dev(u64), -} - -/// Result of resolving a file request. -pub enum ResolvedFile { - /// Proxy request to upstream URL - Upstream { mime: ContentType, url: Url }, - /// Return content directly - Content { - mime: ContentType, - data: bytes::Bytes, - }, -} - -/// Release entry with typed metadata -#[derive(Debug, Clone)] -pub struct Release<M> { - pub backend: String, - pub version: String, - pub sort_key: i64, - pub meta: M, -} - -/// Type alias for storage (untyped meta) -pub type RawRelease = Release<Option<serde_json::Value>>; - -impl<M: Serialize> Release<M> { - pub fn to_raw(&self) -> RawRelease { - Release { - backend: self.backend.clone(), - version: self.version.clone(), - sort_key: self.sort_key, - meta: Some(serde_json::to_value(&self.meta).expect("meta serialization")), - } - } -} - -impl RawRelease { - pub fn try_into_typed<M: DeserializeOwned>(self) -> Result<Release<M>, BackendError> { - let meta = self - .meta - .ok_or_else(|| BackendError::Internal("missing release meta".into()))?; - let meta = serde_json::from_value(meta) - .map_err(|e| BackendError::Internal(format!("release meta: {e}")))?; - Ok(Release { - backend: self.backend, - version: self.version, - sort_key: self.sort_key, - meta, - }) - } -} - -/// Release file entry with typed metadata -#[derive(Debug, Clone)] -pub struct ReleaseFile<M> { - pub backend: String, - pub version: String, - pub filename: String, - pub checksum: String, - pub size: i64, - pub os: Option<Os>, - pub arch: Option<Arch>, - pub meta: M, -} - -/// Type alias for storage (untyped meta) -pub type RawReleaseFile = ReleaseFile<Option<serde_json::Value>>; - -impl<M: Serialize> ReleaseFile<M> { - pub fn to_raw(&self) -> RawReleaseFile { - ReleaseFile { - backend: self.backend.clone(), - version: self.version.clone(), - filename: self.filename.clone(), - checksum: self.checksum.clone(), - size: self.size, - os: self.os, - arch: self.arch, - meta: Some(serde_json::to_value(&self.meta).expect("meta serialization")), - } - } -} - -impl RawReleaseFile { - pub fn try_into_typed<M: DeserializeOwned>(self) -> Result<ReleaseFile<M>, BackendError> { - let meta = self - .meta - .ok_or_else(|| BackendError::Internal("missing file meta".into()))?; - let meta = serde_json::from_value(meta) - .map_err(|e| BackendError::Internal(format!("file meta: {e}")))?; - Ok(ReleaseFile { - backend: self.backend, - version: self.version, - filename: self.filename, - checksum: self.checksum, - size: self.size, - os: self.os, - arch: self.arch, - meta, - }) - } -} - -/// Release view for display (version + metadata + files) -#[derive(Debug, Clone)] -pub struct ReleaseView<M, F> { - pub version: String, - pub meta: M, - pub files: Vec<F>, -} - -/// Trait for backend configuration with common fields. -pub trait BackendConfig { - fn enabled(&self) -> bool; - fn refresh_interval(&self) -> Duration; -} - -/// Trait for parsing filenames and handling checksums/signatures. -pub trait BackendFilename<'a, C>: Sized { - /// Backend-specific signature suffix (e.g., ".minisig"). - /// SHA256 is handled automatically by the base. - const SIGNATURE_SUFFIX: Option<&'static str> = None; - - fn parse(filename: &'a str) -> Result<Self, BackendError>; - - fn upstream_url(&self, config: &C, source: &str) -> Result<Url, BackendError>; - - /// Is this a .sha256 checksum file? - fn is_sha256(&self) -> bool; - - /// Is this a signature file (minisig, gpg)? - fn is_signature(&self) -> bool { - false - } - - /// Can bypass index check? (for dev builds) - fn can_bypass_index(&self) -> bool { - false - } - - /// Get signature content from file meta (not sha256) - fn signature_content(&self, _file: &RawReleaseFile) -> Option<Bytes> { - None - } -} - -/// Storage interface for backend version/file index -#[async_trait] -pub trait BackendStorage: Send + Sync { - /// Query versions by backend, ordered by sort_key. - async fn query_releases(&self, backend: &str) -> Result<Vec<RawRelease>, BackendError>; - - /// Insert or update releases with their files in a single transaction. - async fn insert_releases( - &self, - releases: &[(RawRelease, Vec<RawReleaseFile>)], - ) -> Result<(), BackendError>; - - /// Query files by backend with optional filters. - async fn query_files( - &self, - backend: &str, - version: Option<&str>, - filename: Option<&str>, - meta_null_field: Option<&str>, - ) -> Result<Vec<RawReleaseFile>, BackendError>; - - /// Update file metadata (e.g. after fetching a signature). - async fn update_file_meta(&self, file: &RawReleaseFile) -> Result<bool, BackendError>; -} - -/// Delegate provides I/O primitives to backends. -#[async_trait] -pub trait BackendNetwork: Send + Sync { - /// HTTP GET request - async fn http_get(&self, url: &Url) -> Result<Bytes, BackendError>; -} - -/// Trait for backend-specific types and methods. -#[async_trait] -pub trait BackendSpec: Send + Sync + 'static { - /// Fixed unique identifier for storage - const ID: &'static str; - - /// Meta field where signature is stored (for query_files with meta_null_field) - /// None = signatures not supported by this backend - const SIGNATURE_META_FIELD: Option<&'static str> = None; - - type Config: BackendConfig + Send + Sync; - type Filename<'a>: BackendFilename<'a, Self::Config> + Send; - - type FileMeta: DeserializeOwned; - type ReleaseMeta: DeserializeOwned; - - /// Fetch release index from upstream and convert to storage format - async fn fetch_index( - config: &Self::Config, - network: &dyn BackendNetwork, - ) -> Result<Vec<(RawRelease, Vec<RawReleaseFile>)>, BackendError>; - - /// Fetch signature for a file and return updated file - async fn fetch_signature( - _file: &RawReleaseFile, - _config: &Self::Config, - _source: &str, - _network: &dyn BackendNetwork, - ) -> Result<RawReleaseFile, BackendError> { - unimplemented!("fetch_signature unimplemented"); // default: not supported - } -} - -/// Generic backend implementation -pub struct Backend<S: BackendSpec> { - pub config: S::Config, - pub source: String, - pub storage: Arc<dyn BackendStorage>, - pub network: Arc<dyn BackendNetwork>, -} - -impl<S: BackendSpec> Backend<S> { - pub fn new( - config: S::Config, - source: String, - storage: Arc<dyn BackendStorage>, - network: Arc<dyn BackendNetwork>, - ) -> Self { - Self { - config, - source, - storage, - network, - } - } - - pub fn enabled(&self) -> bool { - self.config.enabled() - } - - pub fn refresh_interval(&self) -> Duration { - self.config.refresh_interval() - } - - /// Fetch index from upstream and store in DB - pub async fn refresh(&self) -> Result<(), BackendError> { - let releases = S::fetch_index(&self.config, &*self.network).await?; - - self.storage.insert_releases(&releases).await?; - - // Fetch signatures if supported - if let Some(field) = S::SIGNATURE_META_FIELD { - let files = self - .storage - .query_files(S::ID, None, None, Some(field)) - .await?; - - for file in files { - match S::fetch_signature(&file, &self.config, &self.source, &*self.network).await { - Ok(updated) => { - if let Err(e) = self.storage.update_file_meta(&updated).await { - tracing::debug!( - filename = file.filename, - "failed to store signature: {e}" - ); - } - } - Err(e) => { - tracing::debug!(filename = file.filename, "failed to fetch signature: {e}"); - } - } - } - } - - Ok(()) - } - - /// Load releases from DB - pub async fn get_releases( - &self, - ) -> Result<Vec<ReleaseView<S::ReleaseMeta, ReleaseFile<S::FileMeta>>>, BackendError> { - let raw_releases = self.storage.query_releases(S::ID).await?; - let raw_files = self.storage.query_files(S::ID, None, None, None).await?; - - let mut files_by_version: HashMap<String, Vec<ReleaseFile<S::FileMeta>>> = HashMap::new(); - for raw_file in raw_files { - if let Ok(file) = raw_file.try_into_typed::<S::FileMeta>() { - files_by_version - .entry(file.version.clone()) - .or_default() - .push(file); - } - } - - let releases = raw_releases - .into_iter() - .filter_map(|raw| { - let typed: Release<S::ReleaseMeta> = raw.try_into_typed().ok()?; - let files = files_by_version.remove(&typed.version).unwrap_or_default(); - Some(ReleaseView { - version: typed.version, - meta: typed.meta, - files, - }) - }) - .collect(); - - Ok(releases) - } - - /// Resolves filename to upstream URL or direct content - pub async fn resolve_file(&self, filename: &str) -> Result<ResolvedFile, BackendError> { - let parsed = S::Filename::parse(filename)?; - - let is_checksum_or_sig = parsed.is_sha256() || parsed.is_signature(); - let mime = if is_checksum_or_sig { - ContentType::TextPlain - } else { - ContentType::OctetStream - }; - - let url = parsed.upstream_url(&self.config, &self.source)?; - - // Strip suffix to get base filename - let base = filename - .strip_suffix(".sha256") - .or_else(|| { - <S::Filename<'_> as BackendFilename<'_, S::Config>>::SIGNATURE_SUFFIX - .and_then(|s| filename.strip_suffix(s)) - }) - .unwrap_or(filename); - - let files = self - .storage - .query_files(S::ID, None, Some(base), None) - .await?; - - let Some(file) = files.into_iter().next() else { - return if parsed.can_bypass_index() { - Ok(ResolvedFile::Upstream { url, mime }) - } else { - Err(BackendError::NotFound) - }; - }; - - // SHA256 — universal, always in file.checksum - if parsed.is_sha256() { - return Ok(ResolvedFile::Content { - mime, - data: file.checksum.clone().into(), - }); - } - - // Signature — backend supports it, may or may not be indexed - if parsed.is_signature() { - if let Some(data) = parsed.signature_content(&file) { - return Ok(ResolvedFile::Content { mime, data }); - } - // Not indexed yet, proxy to upstream - return Ok(ResolvedFile::Upstream { url, mime }); - } - - Ok(ResolvedFile::Upstream { url, mime }) - } -} - -/// Computes numeric sort key for correct version ordering. -/// -/// Formula: major × 10^12 + minor × 10^9 + patch × 10^6 + type × 10^4 + num -/// Where type: dev=10, beta=25, rc=50, stable=99 -/// -/// Examples: -/// - 1.21.3 stable → 1_021_003_990_000 -/// - 1.22.0 beta(1) → 1_022_000_250_001 -/// - 1.22.0 rc(2) → 1_022_000_500_002 -/// - 0.13.0 dev(1234) → 0_013_000_101_234 -pub fn stable_version(major: u64, minor: u64, patch: u64, vtype: VersionType) -> i64 { - let (type_val, num) = match vtype { - VersionType::Stable => (99, 0), - VersionType::Rc(n) => (50, n), - VersionType::Beta(n) => (25, n), - VersionType::Dev(n) => (10, n), - }; - - (major as i64) * 1_000_000_000_000 - + (minor as i64) * 1_000_000_000 - + (patch as i64) * 1_000_000 - + (type_val as i64) * 10_000 - + (num as i64) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::HashSet; - - use proptest::prelude::*; - - #[test] - fn test_stable_version_ordering() { - // dev < beta < rc < stable - let dev = stable_version(1, 0, 0, VersionType::Dev(1)); - let beta = stable_version(1, 0, 0, VersionType::Beta(1)); - let rc = stable_version(1, 0, 0, VersionType::Rc(1)); - let stable = stable_version(1, 0, 0, VersionType::Stable); - - assert!(dev < beta); - assert!(beta < rc); - assert!(rc < stable); - } - - #[test] - fn test_stable_version_patch_ordering() { - let v1_0_0 = stable_version(1, 0, 0, VersionType::Stable); - let v1_0_1 = stable_version(1, 0, 1, VersionType::Stable); - let v1_1_0 = stable_version(1, 1, 0, VersionType::Stable); - let v2_0_0 = stable_version(2, 0, 0, VersionType::Stable); - - assert!(v1_0_0 < v1_0_1); - assert!(v1_0_1 < v1_1_0); - assert!(v1_1_0 < v2_0_0); - } - - #[test] - fn test_stable_version_rc_before_stable() { - // 1.22.0-rc1 < 1.22.0-rc2 < 1.22.0 - let rc1 = stable_version(1, 22, 0, VersionType::Rc(1)); - let rc2 = stable_version(1, 22, 0, VersionType::Rc(2)); - let stable = stable_version(1, 22, 0, VersionType::Stable); - - assert!(rc1 < rc2); - assert!(rc2 < stable); - } - - #[test] - fn test_stable_version_no_collisions() { - let mut keys = HashSet::new(); - - // Test a range of realistic versions - for major in 0..3 { - for minor in 0..30 { - for patch in 0..20 { - let key = stable_version(major, minor, patch, VersionType::Stable); - assert!(keys.insert(key), "collision at {major}.{minor}.{patch}"); - } - // Also test pre-release versions - for n in 1..10 { - let rc = stable_version(major, minor, 0, VersionType::Rc(n)); - assert!(keys.insert(rc), "collision at {major}.{minor}.0-rc{n}"); - - let beta = stable_version(major, minor, 0, VersionType::Beta(n)); - assert!(keys.insert(beta), "collision at {major}.{minor}.0-beta{n}"); - - let dev = stable_version(major, minor, 0, VersionType::Dev(n)); - assert!(keys.insert(dev), "collision at {major}.{minor}.0-dev{n}"); - } - } - } - } - - #[test] - fn test_stable_version_expected_values() { - // Verify the documented examples - assert_eq!( - stable_version(1, 21, 3, VersionType::Stable), - 1_021_003_990_000 - ); - assert_eq!( - stable_version(1, 22, 0, VersionType::Beta(1)), - 1_022_000_250_001 - ); - assert_eq!( - stable_version(1, 22, 0, VersionType::Rc(2)), - 1_022_000_500_002 - ); - assert_eq!( - stable_version(0, 13, 0, VersionType::Dev(1234)), - 13_000_101_234 - ); - } - - fn version_type_strategy() -> impl Strategy<Value = VersionType> { - prop_oneof![ - Just(VersionType::Stable), - (1..100u64).prop_map(VersionType::Rc), - (1..100u64).prop_map(VersionType::Beta), - (1..9999u64).prop_map(VersionType::Dev), - ] - } - - fn to_semver(major: u64, minor: u64, patch: u64, vtype: VersionType) -> semver::Version { - let pre = match vtype { - VersionType::Stable => semver::Prerelease::EMPTY, - VersionType::Rc(n) => semver::Prerelease::new(&format!("rc.{n}")).unwrap(), - VersionType::Beta(n) => semver::Prerelease::new(&format!("beta.{n}")).unwrap(), - VersionType::Dev(n) => semver::Prerelease::new(&format!("dev.{n}")).unwrap(), - }; - semver::Version { - major, - minor, - patch, - pre, - build: semver::BuildMetadata::EMPTY, - } - } - - proptest! { - #[test] - fn fuzz_stable_version_matches_semver( - major1 in 0..100u64, - minor1 in 0..1000u64, - patch1 in 0..1000u64, - vtype1 in version_type_strategy(), - major2 in 0..100u64, - minor2 in 0..1000u64, - patch2 in 0..1000u64, - vtype2 in version_type_strategy(), - ) { - let key1 = stable_version(major1, minor1, patch1, vtype1); - let key2 = stable_version(major2, minor2, patch2, vtype2); - - let sem1 = to_semver(major1, minor1, patch1, vtype1); - let sem2 = to_semver(major2, minor2, patch2, vtype2); - - let key_cmp = key1.cmp(&key2); - let sem_cmp = sem1.cmp(&sem2); - - prop_assert_eq!(key_cmp, sem_cmp, - "Mismatch: {} vs {}: stable_version gives {:?}, semver gives {:?}", - sem1, sem2, key_cmp, sem_cmp); - } - } -} |
