From 7af767898eb08333bfa42b4d2832d84f8228d28d Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 18 Sep 2026 13:27:20 +0200 Subject: [PATCH] launchpad: drive the endpoints from data/launchpad.yml --- data/launchpad.yml | 26 +++++++++++++ src/apt/release.rs | 4 +- src/data.rs | 5 ++- src/launchpad.rs | 92 +++++++++++++++++++++++++++++++++++++++++---- src/package_info.rs | 4 +- 5 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 data/launchpad.yml diff --git a/data/launchpad.yml b/data/launchpad.yml new file mode 100644 index 0000000..cbdda76 --- /dev/null +++ b/data/launchpad.yml @@ -0,0 +1,26 @@ +## Launchpad service endpoints: the REST API, the PPA SFTP upload queue, +## the PPA package-content host and the Ubuntu source-package git web UI. +## Like host_keys.yml, this file exists so that static endpoints are data, +## updatable in one reviewable place, instead of hardcoded in the source — +## the API and content URLs were previously triplicated across modules. +## +## Templates carry their variable parts as {name} placeholders ({owner}, +## {ppa}, {package}), substituted by the accessors of src/launchpad.rs +## with plain string replacement. +## +## Where the values come from: +## api_base: the Launchpad REST API root (https://launchpad.net/docs/api/) +## ssh_*: the PPA upload queue, as expanded by dput-ng's +## ppa:user/ppa profile (ppa.launchpad.net:22, incoming +## ~/) +## content_host_template: ppa.launchpadcontent.net serves PPA apt +## repositories since the 2022 move off ppa.launchpad.net +## git_web_template: Launchpad's CGit mirrors of Ubuntu source packages +## (git.launchpad.net/ubuntu/+source/) + +api_base: https://api.launchpad.net/1.0 +ssh_host: ppa.launchpad.net +ssh_port: 22 +incoming_template: "~{owner}/{ppa}" +content_host_template: https://ppa.launchpadcontent.net/{owner}/{ppa}/ubuntu +git_web_template: https://git.launchpad.net/ubuntu/+source/{package} diff --git a/src/apt/release.rs b/src/apt/release.rs index bf071f2..8f53b63 100644 --- a/src/apt/release.rs +++ b/src/apt/release.rs @@ -851,7 +851,7 @@ fn parse_ppa_url(ppa_base_url: &str) -> Option<(String, String)> { .strip_prefix("https://") .or_else(|| ppa_base_url.strip_prefix("http://"))?; let (host, path) = rest.split_once('/')?; - if host != "ppa.launchpadcontent.net" { + if host != crate::launchpad::ppa_content_host() { return None; } @@ -886,7 +886,7 @@ pub async fn ppa_keyring_bytes( return Ok(cached.clone()); } - let api_url = format!("https://api.launchpad.net/1.0/~{owner}/+archive/ubuntu/{name}"); + let api_url = crate::launchpad::archive_url(&owner, &name); let response = crate::distro_info::http_get_retried(&api_url).await?; if !response.status().is_success() { return Err(format!( diff --git a/src/data.rs b/src/data.rs index 7e36a3d..4d31c02 100644 --- a/src/data.rs +++ b/src/data.rs @@ -7,8 +7,9 @@ //! //! This module is deliberately not a central registry: each file is //! embedded by the module that owns it (distro_info.rs owns -//! data/distro_info.yml, put/ssh.rs owns data/host_keys.yml, quirks.rs -//! owns data/quirks.yml) through the [`embed_data!`] macro below, so data +//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml, put/ssh.rs +//! owns data/host_keys.yml, quirks.rs owns data/quirks.yml) through the +//! [`embed_data!`] macro below, so data //! and its accessors stay together and a diff touching one domain cannot //! half-touch another. The macro embeds the file at compile time and //! parses it once into a `lazy_static` on first use; since the data ships diff --git a/src/launchpad.rs b/src/launchpad.rs index d5af061..f53f2dd 100644 --- a/src/launchpad.rs +++ b/src/launchpad.rs @@ -23,13 +23,69 @@ use std::path::Path; use serde::Deserialize; +use crate::data::embed_data; use crate::put::target::UploadTarget; /// Git configuration key holding the Launchpad account name const LP_USER_KEY: &str = "lp.user"; +/// Launchpad service endpoints, loaded from the bundled `launchpad.yml` +/// data file (same pattern as `distro_info.yml`): static endpoints that +/// change with Launchpad, not with the code, are data — several of them +/// were previously duplicated across three modules. +#[derive(Debug, Deserialize)] +struct LaunchpadData { + /// Base URL of the Launchpad REST API + api_base: String, + /// Host of the PPA SFTP upload server + ssh_host: String, + /// Port of the PPA SFTP upload server + ssh_port: u16, + /// Upload queue incoming directory template (`{owner}`/`{ppa}`) + incoming_template: String, + /// PPA package-content (apt repository) URL template + content_host_template: String, + /// Ubuntu source-package git web URL template (`{package}`) + git_web_template: String, +} + +embed_data! { + static ref LAUNCHPAD_DATA: LaunchpadData = "../data/launchpad.yml" +} + /// Base URL of the Launchpad REST API -const API_BASE: &str = "https://api.launchpad.net/1.0"; +fn api_base() -> &'static str { + &LAUNCHPAD_DATA.api_base +} + +/// Host serving PPA package content, derived from the content-host +/// template so the URL builders and the URL parsers of PPA addresses +/// cannot drift apart +pub(crate) fn ppa_content_host() -> &'static str { + let template = LAUNCHPAD_DATA.content_host_template.as_str(); + let after_scheme = template + .split_once("://") + .map_or(template, |(_, rest)| rest); + after_scheme.split('/').next().unwrap_or(after_scheme) +} + +/// Base URL of the apt repository serving a PPA's packages +/// (e.g. `https://ppa.launchpadcontent.net/user/ppa/ubuntu`) +pub(crate) fn ppa_content_url(owner: &str, ppa: &str) -> String { + LAUNCHPAD_DATA + .content_host_template + .replace("{owner}", owner) + .replace("{ppa}", ppa) +} + +/// URL of the Launchpad git repository of an Ubuntu source package +/// (`git.launchpad.net/ubuntu/+source/`), the preferred VCS of +/// Ubuntu packages +pub(crate) fn ubuntu_source_git_url(package: &str) -> String { + LAUNCHPAD_DATA + .git_web_template + .replace("{package}", package) +} /// Page size (`ws.size`) asked from Launchpad collections. Launchpad /// truncates collection answers at 75 entries by default and rejects @@ -88,27 +144,30 @@ fn split_ppa(ppa: &str) -> Result<(String, String), String> { /// URL of the Launchpad API resource of a Launchpad account fn person_url(user: &str) -> String { - format!("{API_BASE}/~{user}") + format!("{}/~{user}", api_base()) } /// URL of the Launchpad API resource of a PPA (`~user/+archive/ubuntu/name` /// covers the default `ppa` archive and named archives alike); shared by the /// put-side pre-flight checks and the apt keyring's fingerprint lookup pub(crate) fn archive_url(user: &str, ppa: &str) -> String { - format!("{API_BASE}/~{user}/+archive/ubuntu/{ppa}") + format!("{}/~{user}/+archive/ubuntu/{ppa}", api_base()) } -/// Resolve a `user/ppa_name` PPA argument into its upload target -/// (`ppa.launchpad.net`, incoming `~user/ppa_name`), like dput-ng's +/// Resolve a `user/ppa_name` PPA argument into its upload target (the +/// SFTP host and incoming template of `launchpad.yml`), like dput-ng's /// `ppa:user/ppa` profile expansion. pub fn ppa_target(ppa: &str) -> Result { let (user, name) = split_ppa(ppa)?; Ok(UploadTarget { - fqdn: "ppa.launchpad.net".to_string(), - port: 22, + fqdn: LAUNCHPAD_DATA.ssh_host.clone(), + port: LAUNCHPAD_DATA.ssh_port, login: None, - incoming: format!("~{user}/{name}"), + incoming: LAUNCHPAD_DATA + .incoming_template + .replace("{owner}", &user) + .replace("{ppa}", &name), label: format!("ppa:{ppa}"), }) } @@ -425,6 +484,23 @@ mod tests { ); } + /// The data-driven endpoint accessors build the same addresses the + /// former hardcoded constants carried (each verified against the live + /// service), and the content host is derived from the same template + /// the content URLs are built from + #[test] + fn data_driven_endpoints_match_the_service() { + assert_eq!( + ppa_content_url("vhaudiquet", "noctalia"), + "https://ppa.launchpadcontent.net/vhaudiquet/noctalia/ubuntu" + ); + assert_eq!(ppa_content_host(), "ppa.launchpadcontent.net"); + assert_eq!( + ubuntu_source_git_url("hello"), + "https://git.launchpad.net/ubuntu/+source/hello" + ); + } + /// The API answer carries many unrelated fields; deserialization must /// pick the relevant ones and tolerate a null `enabled` #[test] diff --git a/src/package_info.rs b/src/package_info.rs index c44f92d..205d05e 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -18,11 +18,11 @@ use log::{debug, warn}; /// # Returns /// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/") pub fn ppa_to_base_url(user: &str, name: &str) -> String { - format!("https://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name) + crate::launchpad::ppa_content_url(user, name) } fn check_launchpad_repo_sync(package: &str) -> Result, String> { - let url = format!("https://git.launchpad.net/ubuntu/+source/{}", package); + let url = crate::launchpad::ubuntu_source_git_url(package); // Use libgit2 to check if the remote repository exists // This is more reliable than HTTP HEAD requests when CGIt is disabled