aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'src/controller_zig.rs')
-rw-r--r--src/controller_zig.rs175+144 −31
1 files changed, 144 insertions, 31 deletions
diff --git a/src/controller_zig.rs b/src/controller_zig.rs
index 9e0b18c..41b8b0a 100644
--- a/src/controller_zig.rs
+++ b/src/controller_zig.rs
@@ -2,36 +2,174 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use axum::{Router, body, extract, http, response, routing};
-use std::sync::{Arc, OnceLock};
+use semver::Version;
+use std::sync::Arc;
+use thiserror::Error;
+use crate::service_config;
use crate::service_storage;
use crate::service_upstream;
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Archive {
+ Zip,
+ TarXz,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum TarballType<'a> {
+ Source,
+ Bootstrap,
+ Binary { os: &'a str, arch: &'a str },
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Error)]
+#[error("invalid tarball filename")]
+struct ParseError;
+
+/// 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)]
+struct Tarball<'a> {
+ filename: &'a str,
+ tarball_type: TarballType<'a>,
+ minisig: bool,
+ archive: Archive,
+ version: Version,
+ development: bool,
+}
+
+impl<'a> Tarball<'a> {
+ pub fn parse(filename: &'a str) -> Result<Self, ParseError> {
+ let mut buffer = filename;
+ let mut minisig = false;
+ let archive;
+ let tarball_type;
+
+ // (?:|-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(ParseError)?;
+
+ // (?:|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(ParseError);
+ }
+
+ if buffer.is_empty() {
+ return Err(ParseError);
+ }
+
+ let mut it = buffer.rsplit('-');
+ let last = it.next().ok_or(ParseError)?;
+
+ let development = last.starts_with("dev");
+
+ let version = if !development {
+ Version::parse(last).map_err(|_| ParseError)?
+ } else {
+ let semver = it.next().ok_or(ParseError)?;
+ let devver = last;
+ let version_str = format!("{}-{}", semver, devver);
+ Version::parse(&version_str).map_err(|_| ParseError)?
+ };
+
+ if let Some(payload) = it.next() {
+ if payload == "bootstrap" {
+ tarball_type = TarballType::Bootstrap;
+ } else {
+ // Version 0.14.0 is the last one to use the OS-ARCH format in names; newer versions use ARCH-OS.
+ let min_version = Version::new(0, 14, 0);
+ if version > min_version {
+ tarball_type = TarballType::Binary {
+ os: payload,
+ arch: it.next().ok_or(ParseError)?,
+ };
+ } else {
+ tarball_type = TarballType::Binary {
+ arch: payload,
+ os: it.next().ok_or(ParseError)?,
+ };
+ }
+ }
+ } else {
+ tarball_type = TarballType::Source;
+ }
+
+ if it.next().is_some() {
+ return Err(ParseError);
+ }
+
+ Ok(Tarball {
+ filename,
+ tarball_type,
+ minisig,
+ archive,
+ version,
+ development,
+ })
+ }
+
+ /// Builds the upstream URL for this tarball.
+ pub fn upstream_url(&self, source: &str) -> String {
+ if self.development {
+ format!(
+ "https://ziglang.org/builds/{}?source={}",
+ self.filename, source
+ )
+ } else {
+ format!(
+ "https://ziglang.org/download/{}/{}?source={}",
+ self.version, self.filename, source,
+ )
+ }
+ }
+}
+
pub struct ZigController {
+ config: Arc<service_config::ConfigService>,
storage: Arc<service_storage::StorageService>,
upstream: Arc<service_upstream::UpstreamService>,
}
impl ZigController {
pub fn new(
+ config: Arc<service_config::ConfigService>,
storage: Arc<service_storage::StorageService>,
upstream: Arc<service_upstream::UpstreamService>,
) -> Self {
- Self { storage, upstream }
+ Self {
+ config,
+ storage,
+ upstream,
+ }
}
pub fn router(self: Arc<Self>) -> Router {
Router::new()
- .route("/zig/{filename}", routing::get(Self::download))
+ .route("/zig/{filename}", routing::get(Self::handle))
.with_state(self)
}
- async fn download(
+ async fn handle(
extract::State(controller): extract::State<Arc<Self>>,
extract::Path(filename): extract::Path<String>,
) -> Result<response::Response, http::StatusCode> {
- let version = Self::parse_version(&filename).ok_or(http::StatusCode::NOT_FOUND)?;
- let url = Self::build_upstream_url(&filename, &version);
+ let tarball = Tarball::parse(&filename).map_err(|_| http::StatusCode::NOT_FOUND)?;
+ let url = tarball.upstream_url(controller.config.appname());
match controller.storage.get(&filename).await {
Ok(Some(entry)) => {
@@ -61,31 +199,6 @@ impl ZigController {
Ok(Self::build_response(http::StatusCode::OK, cache_entry))
}
- fn parse_version(filename: &str) -> Option<String> {
- let re = Self::filename_regex();
- re.captures(filename)
- .and_then(|captures| captures.get(1))
- .map(|match_| match_.as_str().to_string())
- }
-
- fn filename_regex() -> &'static regex::Regex {
- static REGEX: OnceLock<regex::Regex> = OnceLock::new();
- REGEX.get_or_init(|| {
- regex::Regex::new(
- r"^zig(?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)\.(?:tar\.xz|zip)(?:\.minisig)?$",
- )
- .unwrap()
- })
- }
-
- fn build_upstream_url(filename: &str, version: &str) -> String {
- if version.contains("-dev.") {
- format!("https://ziglang.org/builds/{filename}")
- } else {
- format!("https://ziglang.org/download/{version}/{filename}")
- }
- }
-
fn build_response(
status: http::StatusCode,
entry: service_storage::File,