diff options
Diffstat (limited to 'crates/repos/src/zig.rs')
| -rw-r--r-- | crates/repos/src/zig.rs | 519 | +0 −519 |
1 files changed, 0 insertions, 519 deletions
diff --git a/crates/repos/src/zig.rs b/crates/repos/src/zig.rs deleted file mode 100644 --- a/crates/repos/src/zig.rs +++ /dev/null @@ -1,519 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov -// SPDX-License-Identifier: AGPL-3.0-or-later - -use std::collections::HashMap; -use std::time::Duration; - -use semver::Version as SemVersion; -use serde::de::{self, Visitor}; -use serde::{Deserialize, Serialize}; -use url::Url; - -use bytes::Bytes; - -use super::*; - -#[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct ZigConfig { - pub enabled: bool, - pub upstream: Url, - pub refresh_interval: u64, -} - -impl Default for ZigConfig { - fn default() -> Self { - Self { - enabled: true, - upstream: Url::parse("https://ziglang.org").unwrap(), - refresh_interval: 60 * 60, - } - } -} - -impl BackendConfig for ZigConfig { - fn enabled(&self) -> bool { - self.enabled - } - fn refresh_interval(&self) -> Duration { - Duration::from_secs(self.refresh_interval) - } -} - -/// Deserializes a u64 from either a number or a string. -fn deserialize_size<'de, D>(deserializer: D) -> Result<u64, D::Error> -where - D: serde::Deserializer<'de>, -{ - struct SizeVisitor; - impl<'de> Visitor<'de> for SizeVisitor { - type Value = u64; - - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("a number or string") - } - - fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> { - Ok(v) - } - - fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> { - v.parse().map_err(de::Error::custom) - } - } - - deserializer.deserialize_any(SizeVisitor) -} - -/// Typed metadata for Zig files -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ZigFileMeta { - pub target: String, - - #[serde(default, skip_serializing_if = "Option::is_none")] - pub minisig: Option<String>, -} - -/// Typed metadata for Zig releases (stored in DB) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ZigReleaseMeta { - pub date: Option<String>, - pub docs: Option<String>, - #[serde(alias = "stdDocs")] - pub std_docs: Option<String>, - pub notes: Option<String>, -} - -/// Upstream API tarball format (only used for parsing JSON from ziglang.org) -#[derive(Deserialize)] -struct UpstreamFile { - #[serde(alias = "tarball")] - filename: String, - shasum: String, - #[serde(deserialize_with = "deserialize_size")] - size: u64, -} - -/// Upstream API release format (only used for parsing JSON from ziglang.org) -#[derive(Deserialize)] -#[allow(dead_code)] -struct UpstreamRelease { - #[serde(flatten)] - meta: ZigReleaseMeta, - /// Present only in the "master" entry - version: Option<String>, - src: Option<UpstreamFile>, - bootstrap: Option<UpstreamFile>, - #[serde(flatten)] - targets: HashMap<String, UpstreamFile>, -} - -/// Zig file with metadata -pub type ZigFile = ReleaseFile<ZigFileMeta>; - -impl ZigFile { - fn from_upstream( - version: &str, - target: &str, - tarball: &UpstreamFile, - ) -> Option<RawReleaseFile> { - let url = Url::parse(&tarball.filename).ok()?; - let filename = url - .path_segments() - .and_then(|mut s| s.next_back()) - .filter(|s| !s.is_empty())?; - - let parsed = ZigFilename::parse(filename).ok()?; - - let file: ReleaseFile<ZigFileMeta> = ReleaseFile { - backend: ZigSpec::ID.to_string(), - version: version.to_string(), - filename: filename.to_string(), - checksum: tarball.shasum.clone(), - size: tarball.size as i64, - os: parsed.os.and_then(|s| s.parse().ok()), - arch: parsed.arch.and_then(|s| s.parse().ok()), - meta: ZigFileMeta { - minisig: None, - target: target.to_string(), - }, - }; - Some(file.to_raw()) - } -} - -/// Zig backend specification -pub struct ZigSpec; - -#[async_trait::async_trait] -impl BackendSpec for ZigSpec { - const ID: &'static str = "zig"; - const SIGNATURE_META_FIELD: Option<&'static str> = Some("minisig"); - - type Config = ZigConfig; - type ReleaseMeta = ZigReleaseMeta; - type FileMeta = ZigFileMeta; - type Filename<'a> = ZigFilename<'a>; - - async fn fetch_index( - config: &Self::Config, - network: &dyn BackendNetwork, - ) -> Result<Vec<(RawRelease, Vec<RawReleaseFile>)>, BackendError> { - let mut url = config.upstream.clone(); - url.path_segments_mut() - .map_err(|_| BackendError::Internal("cannot-be-a-base URL".into()))? - .pop_if_empty() - .extend(["download", "index.json"]); - - let bytes = network.http_get(&url).await?; - let map: HashMap<String, UpstreamRelease> = - serde_json::from_slice(&bytes).map_err(|e| BackendError::Upstream(e.to_string()))?; - - let mut releases = Vec::with_capacity(map.len()); - for (version_str, upstream) in map { - let sort_key = match ZigVersion::parse(&version_str) { - Ok(v) => v.sort_key(), - Err(e) => { - tracing::error!(version = version_str, "invalid zig version, skipping: {e}"); - continue; - } - }; - - let release: Release<ZigReleaseMeta> = Release { - backend: Self::ID.to_string(), - version: version_str.to_string(), - sort_key, - meta: upstream.meta.clone(), - }; - - let mut files = Vec::new(); - - if let Some(ref tarball) = upstream.src - && let Some(f) = ZigFile::from_upstream(&version_str, "src", tarball) - { - files.push(f); - } - if let Some(ref tarball) = upstream.bootstrap - && let Some(f) = ZigFile::from_upstream(&version_str, "bootstrap", tarball) - { - files.push(f); - } - for (target, tarball) in &upstream.targets { - if let Some(f) = ZigFile::from_upstream(&version_str, target, tarball) { - files.push(f); - } - } - - releases.push((release.to_raw(), files)); - } - - Ok(releases) - } - - async fn fetch_signature( - file: &RawReleaseFile, - config: &Self::Config, - source: &str, - network: &dyn BackendNetwork, - ) -> Result<RawReleaseFile, BackendError> { - let minisig_filename = format!("{}.minisig", file.filename); - let parsed = ZigFilename::parse(&minisig_filename) - .map_err(|_| BackendError::Internal(format!("invalid filename: {}", file.filename)))?; - - let url = parsed - .upstream_url(config, source) - .map_err(|_| BackendError::Internal("cannot build URL".into()))?; - - let bytes = network.http_get(&url).await?; - let minisig = - String::from_utf8(bytes.to_vec()).map_err(|e| BackendError::Upstream(e.to_string()))?; - - let mut typed: ReleaseFile<ZigFileMeta> = file.clone().try_into_typed()?; - typed.meta.minisig = Some(minisig); - - tracing::debug!(filename = file.filename, "cached minisig"); - Ok(typed.to_raw()) - } -} - -/// Type alias for Zig backend -pub type ZigBackend = Backend<ZigSpec>; - -/// Wrapper for sort key computation. -pub enum ZigVersion { - Master, - Semver(SemVersion), -} - -impl ZigVersion { - fn parse(s: &str) -> Result<Self, BackendError> { - if s == "master" { - return Ok(Self::Master); - } - SemVersion::parse(s) - .map(Self::Semver) - .map_err(|_| BackendError::Upstream(format!("invalid zig version: {s}"))) - } - - fn sort_key(&self) -> i64 { - match self { - Self::Master => i64::MAX, - Self::Semver(v) => { - let vtype = if v.pre.is_empty() { - VersionType::Stable - } else if v.pre.as_str().starts_with("dev.") { - let num = v.pre.as_str()[4..].parse().unwrap_or(0); - VersionType::Dev(num) - } else { - VersionType::Stable - }; - - stable_version(v.major, v.minor, v.patch, vtype) - } - } - } -} - -/// Describes a single file stored at `ziglang.org/download/`. -/// -/// The tarball naming has changed several times. When parsing, -/// we standardize the files, but for the reverse operation -/// (getting a string from a tarball), we preserve the original path. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ZigFilename<'a> { - filename: &'a str, - os: Option<&'a str>, - arch: Option<&'a str>, - kind: FileKind, - minisig: bool, - archive: Archive, - version: SemVersion, - development: bool, -} - -impl<'a> BackendFilename<'a, ZigConfig> for ZigFilename<'a> { - const SIGNATURE_SUFFIX: Option<&'static str> = Some(".minisig"); - - /// Builds the upstream URL for this tarball. - fn parse(filename: &'a str) -> Result<Self, BackendError> { - let mut buffer = filename; - let mut minisig = false; - let archive; - - // (?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( - // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? - // )\.(?:tar\.xz|zip)(?:\.minisig)? - buffer = buffer.strip_prefix("zig-").ok_or(BackendError::NotFound)?; - - // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( - // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? - // )\.(?:tar\.xz|zip) - if let Some(it) = buffer.strip_suffix(".minisig") { - buffer = it; - minisig = true; - } - - // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-( - // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)? - // ) - if let Some(it) = buffer.strip_suffix(".zip") { - buffer = it; - archive = Archive::Zip; - } else if let Some(it) = buffer.strip_suffix(".tar.xz") { - buffer = it; - archive = Archive::TarXz; - } else { - return Err(BackendError::NotFound); - } - - if buffer.is_empty() { - return Err(BackendError::NotFound); - } - - let mut it = buffer.rsplit('-'); - let last = it.next().ok_or(BackendError::NotFound)?; - - let development = last.starts_with("dev"); - - let version = if !development { - SemVersion::parse(last).map_err(|_| BackendError::NotFound)? - } else { - let semver = it.next().ok_or(BackendError::NotFound)?; - let devver = last; - let version_str = format!("{}-{}", semver, devver); - SemVersion::parse(&version_str).map_err(|_| BackendError::NotFound)? - }; - - let (os, arch, kind) = if let Some(payload) = it.next() { - if payload == "bootstrap" { - (None, None, FileKind::Bootstrap) - } else { - // Filename format changed over time: - // - <= 0.2.0: used zig-win64 for windows and zig-linux-x86_64 for linux ¯\_(ツ)_/¯ - // - 0.2.0 to 0.14.0: zig-OS-ARCH-VERSION (e.g. zig-linux-x86_64-0.13.0) - // - > 0.14.0: zig-ARCH-OS-VERSION (e.g. zig-x86_64-linux-0.15.0) - let (os, arch) = if version <= SemVersion::new(0, 2, 0) && payload == "win64" { - ("windows", "x86_64") - } else if version <= SemVersion::new(0, 14, 0) { - (it.next().ok_or(BackendError::NotFound)?, payload) - } else { - (payload, it.next().ok_or(BackendError::NotFound)?) - }; - (Some(os), Some(arch), FileKind::Archive) - } - } else { - (None, None, FileKind::Source) - }; - - if it.next().is_some() { - return Err(BackendError::NotFound); - } - - Ok(ZigFilename { - filename, - os, - arch, - kind, - minisig, - archive, - version, - development, - }) - } - - fn upstream_url(&self, config: &ZigConfig, source: &str) -> Result<Url, BackendError> { - let mut url = config.upstream.clone(); - - { - let mut segments = url - .path_segments_mut() - .map_err(|()| BackendError::Internal("cannot build upstream URL".into()))?; - segments.pop_if_empty(); - if self.development { - segments.push("builds"); - } else { - segments.push("download").push(&self.version.to_string()); - } - segments.push(self.filename); - } - - url.query_pairs_mut().append_pair("source", source); - Ok(url) - } - - fn is_sha256(&self) -> bool { - false // Zig uses minisig, not sha256 - } - - fn is_signature(&self) -> bool { - self.minisig - } - - fn can_bypass_index(&self) -> bool { - self.development - } - - fn signature_content(&self, file: &RawReleaseFile) -> Option<Bytes> { - if !self.minisig { - return None; - } - let typed: ReleaseFile<ZigFileMeta> = file.clone().try_into_typed().ok()?; - typed.meta.minisig.map(|s| s.into_bytes().into()) - } -} - -#[cfg(test)] -mod tests_zig_filename { - use super::*; - - #[test] - fn parse_old_combined_platform() { - // <= 0.2.0: zig-PLATFORM-VERSION (only Windows used this format) - let file = ZigFilename::parse("zig-win64-0.1.1.zip").unwrap(); - assert_eq!(file.os, Some("windows")); - assert_eq!(file.arch, Some("x86_64")); - assert_eq!(file.version, SemVersion::new(0, 1, 1)); - assert_eq!(file.archive, Archive::Zip); - assert_eq!(file.kind, FileKind::Archive); - - let file = ZigFilename::parse("zig-win64-0.2.0.zip").unwrap(); - assert_eq!(file.os, Some("windows")); - assert_eq!(file.arch, Some("x86_64")); - assert_eq!(file.version, SemVersion::new(0, 2, 0)); - } - - #[test] - fn parse_middle_os_arch_format() { - // 0.2.0 to 0.14.0: zig-OS-ARCH-VERSION - let file = ZigFilename::parse("zig-linux-x86_64-0.13.0.tar.xz").unwrap(); - assert_eq!(file.os, Some("linux")); - assert_eq!(file.arch, Some("x86_64")); - assert_eq!(file.version, SemVersion::new(0, 13, 0)); - assert_eq!(file.archive, Archive::TarXz); - - let file = ZigFilename::parse("zig-windows-x86_64-0.10.0.zip").unwrap(); - assert_eq!(file.os, Some("windows")); - assert_eq!(file.arch, Some("x86_64")); - } - - #[test] - fn parse_new_arch_os_format() { - // > 0.14.0: zig-ARCH-OS-VERSION - let file = ZigFilename::parse("zig-x86_64-linux-0.15.0.tar.xz").unwrap(); - assert_eq!(file.os, Some("linux")); - assert_eq!(file.arch, Some("x86_64")); - assert_eq!(file.version, SemVersion::new(0, 15, 0)); - - let file = ZigFilename::parse("zig-aarch64-macos-0.15.0.tar.xz").unwrap(); - assert_eq!(file.os, Some("macos")); - assert_eq!(file.arch, Some("aarch64")); - } - - #[test] - fn parse_dev_version() { - let file = ZigFilename::parse("zig-x86_64-linux-0.14.0-dev.123+abc123.tar.xz").unwrap(); - assert!(file.development); - assert_eq!(file.version.major, 0); - assert_eq!(file.version.minor, 14); - assert_eq!(file.version.patch, 0); - } - - #[test] - fn parse_source_tarball() { - let file = ZigFilename::parse("zig-0.13.0.tar.xz").unwrap(); - assert_eq!(file.os, None); - assert_eq!(file.arch, None); - assert_eq!(file.kind, FileKind::Source); - } - - #[test] - fn parse_bootstrap() { - let file = ZigFilename::parse("zig-bootstrap-0.13.0.tar.xz").unwrap(); - assert_eq!(file.kind, FileKind::Bootstrap); - } - - #[test] - fn parse_minisig() { - let file = ZigFilename::parse("zig-win64-0.1.1.zip.minisig").unwrap(); - assert!(file.minisig); - assert_eq!(file.os, Some("windows")); - assert_eq!(file.arch, Some("x86_64")); - - let file = ZigFilename::parse("zig-x86_64-linux-0.15.0.tar.xz.minisig").unwrap(); - assert!(file.minisig); - assert_eq!(file.os, Some("linux")); - } - - #[test] - fn parse_boundary_version() { - // 0.14.0 should use OS-ARCH format - let file = ZigFilename::parse("zig-linux-x86_64-0.14.0.tar.xz").unwrap(); - assert_eq!(file.os, Some("linux")); - assert_eq!(file.arch, Some("x86_64")); - - // 0.14.1 should use ARCH-OS format - let file = ZigFilename::parse("zig-x86_64-linux-0.14.1.tar.xz").unwrap(); - assert_eq!(file.os, Some("linux")); - assert_eq!(file.arch, Some("x86_64")); - } -} |
