build,debian: extract reusable Debian format primitives from build/
Move the generic components out of src/build/ into a new src/debian/ module so they can be reused independently of the build pipeline: deb822 control parsing (plus the debian/control model), file checksum registry, debian/files registry, Debian version handling and changelog entry parsing. Merge OpenPGP clearsigning into utils/gpg.rs next to the existing key discovery helper, making signing available outside of builds. Delegate changelog.rs header/footer parsing to the new debian::changelog parser, removing the duplicate regex implementation. src/build/ keeps only build-specific logic: the pipeline driver, build types, environment setup and the .buildinfo/.changes writers.
This commit is contained in:
@@ -5,8 +5,8 @@
|
|||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::checksums::FileChecksums;
|
use crate::debian::checksums::FileChecksums;
|
||||||
use super::control::{parse_paragraphs, write_paragraph, Paragraph};
|
use crate::debian::control::{parse_paragraphs, write_paragraph, Paragraph};
|
||||||
|
|
||||||
/// One installed package relevant for dependency resolution.
|
/// One installed package relevant for dependency resolution.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::checksums::FileChecksums;
|
use crate::debian::checksums::FileChecksums;
|
||||||
use super::control::{write_paragraph, Paragraph};
|
use crate::debian::control::{write_paragraph, Paragraph};
|
||||||
use super::files::FilesList;
|
use crate::debian::files::FilesList;
|
||||||
|
|
||||||
/// Everything needed to render a `.changes` file.
|
/// Everything needed to render a `.changes` file.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -178,7 +178,7 @@ mod tests {
|
|||||||
checksums.add_file(&dsc_path).unwrap();
|
checksums.add_file(&dsc_path).unwrap();
|
||||||
|
|
||||||
let mut files_list = FilesList::new();
|
let mut files_list = FilesList::new();
|
||||||
files_list.add(super::super::files::FilesEntry::new(
|
files_list.add(crate::debian::FilesEntry::new(
|
||||||
"pkg_1.0.dsc",
|
"pkg_1.0.dsc",
|
||||||
"utils",
|
"utils",
|
||||||
"optional",
|
"optional",
|
||||||
|
|||||||
+10
-27
@@ -9,22 +9,16 @@
|
|||||||
pub mod buildinfo;
|
pub mod buildinfo;
|
||||||
pub mod buildtype;
|
pub mod buildtype;
|
||||||
pub mod changes;
|
pub mod changes;
|
||||||
pub mod checksums;
|
|
||||||
pub mod control;
|
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod files;
|
|
||||||
pub mod metadata;
|
|
||||||
pub mod sign;
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use checksums::{Entry as ChecksumEntry, FileChecksums};
|
use crate::debian::{
|
||||||
use control::parse_paragraphs;
|
parse_paragraphs, ChecksumEntry, ControlInfo, FileChecksums, FilesEntry, FilesList,
|
||||||
use files::{FilesEntry, FilesList};
|
};
|
||||||
use metadata::ControlInfo;
|
|
||||||
|
|
||||||
/// Options for a native source-package build.
|
/// Options for a native source-package build.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -114,7 +108,7 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 2. Metadata resolution
|
// 2. Metadata resolution
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
let entry = metadata::parse_changelog_entry(&changelog_path)?;
|
let entry = crate::debian::parse_changelog_entry(&changelog_path)?;
|
||||||
let ctrl = ControlInfo::parse(&control_path)?;
|
let ctrl = ControlInfo::parse(&control_path)?;
|
||||||
|
|
||||||
log::info!("source package {}", entry.source);
|
log::info!("source package {}", entry.source);
|
||||||
@@ -346,22 +340,22 @@ pub fn run_source_build(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
let mut signed = false;
|
let mut signed = false;
|
||||||
if let Some(keyid) = signing_key.filter(|_| do_sign) {
|
if let Some(keyid) = signing_key.filter(|_| do_sign) {
|
||||||
sign::validate_key_id(&keyid)?;
|
crate::utils::gpg::validate_key_id(&keyid)?;
|
||||||
|
|
||||||
println!("signfile {}", dsc_name);
|
println!("signfile {}", dsc_name);
|
||||||
sign::clearsign_file(&dsc_path, &keyid)?;
|
crate::utils::gpg::clearsign_file(&dsc_path, &keyid)?;
|
||||||
// The .dsc changed: refresh its checksums inside the .buildinfo.
|
// The .dsc changed: refresh its checksums inside the .buildinfo.
|
||||||
checksums.add_file_as(&dsc_path, &dsc_name)?;
|
checksums.add_file_as(&dsc_path, &dsc_name)?;
|
||||||
buildinfo::save_buildinfo(&buildinfo_path, &render_buildinfo_doc(&checksums))?;
|
buildinfo::save_buildinfo(&buildinfo_path, &render_buildinfo_doc(&checksums))?;
|
||||||
|
|
||||||
println!("signfile {}", buildinfo_name);
|
println!("signfile {}", buildinfo_name);
|
||||||
sign::clearsign_file(&buildinfo_path, &keyid)?;
|
crate::utils::gpg::clearsign_file(&buildinfo_path, &keyid)?;
|
||||||
// Both .dsc and .buildinfo changed: refresh the .changes.
|
// Both .dsc and .buildinfo changed: refresh the .changes.
|
||||||
checksums.add_file_as(&buildinfo_path, &buildinfo_name)?;
|
checksums.add_file_as(&buildinfo_path, &buildinfo_name)?;
|
||||||
changes::save_changes(&changes_path, &render_changes_doc(&checksums))?;
|
changes::save_changes(&changes_path, &render_changes_doc(&checksums))?;
|
||||||
|
|
||||||
println!("signfile {}", changes_name);
|
println!("signfile {}", changes_name);
|
||||||
sign::clearsign_file(&changes_path, &keyid)?;
|
crate::utils::gpg::clearsign_file(&changes_path, &keyid)?;
|
||||||
|
|
||||||
signed = true;
|
signed = true;
|
||||||
}
|
}
|
||||||
@@ -418,13 +412,10 @@ fn run_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export commonly used types at the module root.
|
|
||||||
pub use metadata::{ChangelogEntry, DebianVersion};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use buildtype::BuildType;
|
use crate::debian::DebianVersion;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn partial_checksum_defaults() {
|
fn partial_checksum_defaults() {
|
||||||
@@ -434,16 +425,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn debian_version_reexport_usable() {
|
fn debian_version_from_debian_module_usable() {
|
||||||
let v = DebianVersion::parse("1.0-2").unwrap();
|
let v = DebianVersion::parse("1.0-2").unwrap();
|
||||||
assert_eq!(v.no_epoch(), "1.0-2");
|
assert_eq!(v.no_epoch(), "1.0-2");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The full pipeline is exercised end-to-end by running pkh against a
|
|
||||||
// fixture package; unit tests cover the individual stages above.
|
|
||||||
#[test]
|
|
||||||
fn build_type_source_only_pipeline_mapping() {
|
|
||||||
// Source-only builds always map to the 'source' artifact suffix.
|
|
||||||
assert_eq!(buildtype::SOURCE.arch_suffix("amd64"), "source");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
//! OpenPGP inline (clear) signing of `.dsc`, `.buildinfo` and `.changes`
|
|
||||||
//! files through `gpgme`, mirroring what `dpkg-buildpackage` does via its
|
|
||||||
//! OpenPGP backends.
|
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use gpgme::{Context, Data, Protocol};
|
|
||||||
|
|
||||||
/// Validate an OpenPGP key id / fingerprint like dpkg does.
|
|
||||||
///
|
|
||||||
/// Short (<= 8 hex chars) key IDs are rejected outright, long (16 hex chars)
|
|
||||||
/// key IDs produce a warning; anything else must be a v4 (40) or v6 (64)
|
|
||||||
/// fingerprint length.
|
|
||||||
pub fn validate_key_id(keyid: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let len = keyid.len();
|
|
||||||
if len <= 8 {
|
|
||||||
return Err(
|
|
||||||
"short OpenPGP key IDs are broken; use a key fingerprint instead".into(),
|
|
||||||
);
|
|
||||||
} else if len <= 16 {
|
|
||||||
log::warn!(
|
|
||||||
"long OpenPGP key IDs are strongly discouraged; \
|
|
||||||
use a key fingerprint instead"
|
|
||||||
);
|
|
||||||
} else if len != 40 && len != 64 {
|
|
||||||
log::warn!("OpenPGP key ID has unknown v4 or v6 fingerprint length");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a secret key whose fingerprint matches `keyid` (suffix matching
|
|
||||||
/// allows passing a long key id instead of the full fingerprint).
|
|
||||||
fn find_secret_key(ctx: &mut Context, keyid: &str) -> Result<Option<gpgme::Key>, Box<dyn Error>> {
|
|
||||||
for key_result in ctx.secret_keys()? {
|
|
||||||
let key = key_result?;
|
|
||||||
if let Ok(fingerprint) = key.fingerprint() {
|
|
||||||
if fingerprint.ends_with(keyid) {
|
|
||||||
return Ok(Some(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clearsign a file in place: the original content becomes the payload of an
|
|
||||||
/// armored inline-signed document which atomically replaces the file.
|
|
||||||
///
|
|
||||||
/// This is the same operation as dpkg's `inline_sign` + rename sequence.
|
|
||||||
pub fn clearsign_file(path: &Path, keyid: &str) -> Result<(), Box<dyn Error>> {
|
|
||||||
let content = std::fs::read(path)
|
|
||||||
.map_err(|e| format!("cannot read '{}' for signing: {}", path.display(), e))?;
|
|
||||||
|
|
||||||
let mut ctx = Context::from_protocol(Protocol::OpenPgp)
|
|
||||||
.map_err(|e| format!("cannot initialize GPGME: {}", e))?;
|
|
||||||
ctx.set_armor(true);
|
|
||||||
|
|
||||||
let key = find_secret_key(&mut ctx, keyid)?
|
|
||||||
.ok_or_else(|| format!("no secret key matching '{}' found", keyid))?;
|
|
||||||
|
|
||||||
ctx.add_signer(&key)
|
|
||||||
.map_err(|e| format!("cannot add signer '{}': {}", keyid, e))?;
|
|
||||||
|
|
||||||
let input = Data::from_bytes(&content)?;
|
|
||||||
let mut output = Data::new()?;
|
|
||||||
ctx.sign_clear(input, &mut output)
|
|
||||||
.map_err(|e| format!("clear-signing '{}' failed: {}", path.display(), e))?;
|
|
||||||
|
|
||||||
// gpgme leaves the output buffer cursor at the end after writing.
|
|
||||||
output.seek(SeekFrom::Start(0))?;
|
|
||||||
let mut signed = Vec::new();
|
|
||||||
output.read_to_end(&mut signed)?;
|
|
||||||
|
|
||||||
// Atomic replace, like dpkg's signfile (write .asc then move).
|
|
||||||
let tmp = path.with_extension("asc.tmp");
|
|
||||||
std::fs::write(&tmp, &signed)
|
|
||||||
.map_err(|e| format!("cannot write '{}': {}", tmp.display(), e))?;
|
|
||||||
std::fs::rename(&tmp, path)
|
|
||||||
.map_err(|e| format!("cannot install signed '{}': {}", path.display(), e).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn key_id_validation() {
|
|
||||||
assert!(validate_key_id("12345678").is_err()); // short: rejected
|
|
||||||
assert!(validate_key_id("1234567890ABCDEF").is_ok()); // long: warns
|
|
||||||
assert!(validate_key_id(&"a".repeat(40)).is_ok());
|
|
||||||
assert!(validate_key_id(&"b".repeat(64)).is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-74
@@ -2,7 +2,7 @@ use chrono::Local;
|
|||||||
use git2::{Oid, Repository, Sort};
|
use git2::{Oid, Repository, Sort};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, BufRead, Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Automatically generate a changelog entry from a commit history and previous changelog
|
/// Automatically generate a changelog entry from a commit history and previous changelog
|
||||||
@@ -121,84 +121,15 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
|
|||||||
pub fn parse_changelog_header(
|
pub fn parse_changelog_header(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<(String, String, String), Box<dyn std::error::Error>> {
|
) -> Result<(String, String, String), Box<dyn std::error::Error>> {
|
||||||
let file = File::open(path).map_err(|e| {
|
let entry = crate::debian::parse_changelog_entry(path)?;
|
||||||
format!(
|
Ok((entry.source, entry.version.full(), entry.distribution))
|
||||||
"Failed to read changelog '{}': {}. \
|
|
||||||
Make sure you are running this command from the root of a source package \
|
|
||||||
(a directory containing a 'debian/' subdirectory with a 'changelog' file).",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut reader = io::BufReader::new(file);
|
|
||||||
let mut first_line = String::new();
|
|
||||||
reader.read_line(&mut first_line).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"Failed to read first line of changelog '{}': {}",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Format: package (version) series; urgency=urgency
|
|
||||||
let re = Regex::new(r"^(\S+) \(([^)]+)\) (.*); .*")?;
|
|
||||||
if let Some(caps) = re.captures(&first_line) {
|
|
||||||
let package = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
|
||||||
let version = caps.get(2).map_or("", |m| m.as_str()).to_string();
|
|
||||||
let series = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
|
||||||
Ok((package, version, series))
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"Invalid changelog header format in '{}'. \
|
|
||||||
The first line must look like: `package (version) series; urgency=...`, \
|
|
||||||
but got: {:?}",
|
|
||||||
path.display(),
|
|
||||||
first_line.trim_end()
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a changelog file footer to extract maintainer information
|
/// Parse a changelog file footer to extract maintainer information
|
||||||
/// Returns (name, email) tuple from the last modification entry
|
/// Returns (name, email) tuple from the last modification entry
|
||||||
pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box<dyn std::error::Error>> {
|
pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box<dyn std::error::Error>> {
|
||||||
let mut file = File::open(path).map_err(|e| {
|
let entry = crate::debian::parse_changelog_entry(path)?;
|
||||||
format!(
|
Ok((entry.maintainer_name, entry.maintainer_email))
|
||||||
"Failed to read changelog '{}': {}. \
|
|
||||||
Make sure you are running this command from the root of a source package \
|
|
||||||
(a directory containing a 'debian/' subdirectory with a 'changelog' file).",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut content = String::new();
|
|
||||||
file.read_to_string(&mut content)
|
|
||||||
.map_err(|e| format!("Failed to read changelog '{}': {}", path.display(), e))?;
|
|
||||||
|
|
||||||
// Find the last maintainer line (format: -- Name <email> Date)
|
|
||||||
let re = Regex::new(r"--\s*([^<]+?)\s*<([^>]+)>\s*")?;
|
|
||||||
|
|
||||||
if let Some(first_match) = re.captures_iter(&content).next() {
|
|
||||||
let name = first_match
|
|
||||||
.get(1)
|
|
||||||
.map_or("", |m| m.as_str())
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
let email = first_match
|
|
||||||
.get(2)
|
|
||||||
.map_or("", |m| m.as_str())
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
Ok((name, email))
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"No maintainer information found in '{}'. \
|
|
||||||
The changelog must contain a line of the form '-- Name <email> Date', \
|
|
||||||
but none was found.",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,95 +1,10 @@
|
|||||||
//! Source package metadata: Debian version splitting, changelog entry
|
//! Debian changelog entry parsing (`debian/changelog`).
|
||||||
//! parsing and `debian/control` parsing.
|
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
|
||||||
use super::control::{parse_paragraphs, Paragraph};
|
use super::version::DebianVersion;
|
||||||
|
|
||||||
/// A Debian version, split into its `[epoch:]upstream[-revision]` parts.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct DebianVersion {
|
|
||||||
/// Optional numeric epoch (part before the first `:`).
|
|
||||||
pub epoch: Option<u32>,
|
|
||||||
/// Upstream version (may itself contain `-` when there is no revision).
|
|
||||||
pub upstream: String,
|
|
||||||
/// Optional Debian revision (part after the last `-`).
|
|
||||||
pub debian_revision: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DebianVersion {
|
|
||||||
/// Parse and validate a Debian version string.
|
|
||||||
pub fn parse(raw: &str) -> Result<DebianVersion, String> {
|
|
||||||
let raw = raw.trim();
|
|
||||||
if raw.is_empty() {
|
|
||||||
return Err("empty version string".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (epoch, rest) = match raw.split_once(':') {
|
|
||||||
Some((e, r)) => {
|
|
||||||
let epoch: u32 = e
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| format!("invalid epoch '{}' in version '{}'", e, raw))?;
|
|
||||||
(Some(epoch), r)
|
|
||||||
}
|
|
||||||
None => (None, raw),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The revision is everything after the last hyphen.
|
|
||||||
let (upstream, debian_revision) = match rest.rsplit_once('-') {
|
|
||||||
Some((u, r)) => (u.to_string(), Some(r.to_string())),
|
|
||||||
None => (rest.to_string(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
if upstream.is_empty() {
|
|
||||||
return Err(format!("missing upstream version in '{}'", raw));
|
|
||||||
}
|
|
||||||
for c in upstream.chars() {
|
|
||||||
if !(c.is_ascii_alphanumeric()
|
|
||||||
|| matches!(c, '.' | '+' | '-' | '~' | ':')
|
|
||||||
|| !c.is_ascii())
|
|
||||||
{
|
|
||||||
return Err(format!("invalid character '{}' in version '{}'", c, raw));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(rev) = &debian_revision {
|
|
||||||
for c in rev.chars() {
|
|
||||||
if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) {
|
|
||||||
return Err(format!(
|
|
||||||
"invalid character '{}' in revision of version '{}'",
|
|
||||||
c, raw
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DebianVersion {
|
|
||||||
epoch,
|
|
||||||
upstream,
|
|
||||||
debian_revision,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full version string, including the epoch (`[epoch:]upstream[-rev]`).
|
|
||||||
pub fn full(&self) -> String {
|
|
||||||
match (&self.epoch, &self.debian_revision) {
|
|
||||||
(Some(e), Some(r)) => format!("{}:{}-{}", e, self.upstream, r),
|
|
||||||
(Some(e), None) => format!("{}:{}", e, self.upstream),
|
|
||||||
(None, Some(r)) => format!("{}-{}", self.upstream, r),
|
|
||||||
(None, None) => self.upstream.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Version string without the epoch (`upstream[-rev]`), used in artifact
|
|
||||||
/// file names.
|
|
||||||
pub fn no_epoch(&self) -> String {
|
|
||||||
match &self.debian_revision {
|
|
||||||
Some(r) => format!("{}-{}", self.upstream, r),
|
|
||||||
None => self.upstream.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A parsed `debian/changelog` entry (the most recent one).
|
/// A parsed `debian/changelog` entry (the most recent one).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -274,92 +189,10 @@ fn looks_like_header(line: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed `debian/control`: the source stanza plus all binary stanzas.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ControlInfo {
|
|
||||||
/// First paragraph (source package stanza).
|
|
||||||
pub source: Paragraph,
|
|
||||||
/// Remaining paragraphs (binary package stanzas).
|
|
||||||
pub binaries: Vec<Paragraph>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ControlInfo {
|
|
||||||
/// Parse a `debian/control` file.
|
|
||||||
pub fn parse(path: &Path) -> Result<ControlInfo, Box<dyn std::error::Error>> {
|
|
||||||
let content = std::fs::read_to_string(path)
|
|
||||||
.map_err(|e| format!("failed to read control file '{}': {}", path.display(), e))?;
|
|
||||||
Self::from_str(&content)
|
|
||||||
.map_err(|e| format!("invalid control file '{}': {}", path.display(), e).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse control content from a string.
|
|
||||||
pub fn from_str(content: &str) -> Result<ControlInfo, String> {
|
|
||||||
let paragraphs = parse_paragraphs(content);
|
|
||||||
let mut iter = paragraphs.into_iter();
|
|
||||||
let source = iter
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| "control file has no paragraphs".to_string())?;
|
|
||||||
if source.get("Source").is_none() {
|
|
||||||
return Err("first control paragraph has no 'Source' field".to_string());
|
|
||||||
}
|
|
||||||
let binaries: Vec<Paragraph> = iter.collect();
|
|
||||||
for bin in &binaries {
|
|
||||||
if bin.get("Package").is_none() {
|
|
||||||
return Err("binary control paragraph has no 'Package' field".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ControlInfo { source, binaries })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The source package name.
|
|
||||||
pub fn source_name(&self) -> &str {
|
|
||||||
self.source.get("Source").expect("checked at parse")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Section from the source stanza, or `'-'`.
|
|
||||||
pub fn section(&self) -> &str {
|
|
||||||
self.source.get("Section").unwrap_or("-")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Priority from the source stanza, or `'-'`.
|
|
||||||
pub fn priority(&self) -> &str {
|
|
||||||
self.source.get("Priority").unwrap_or("-")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn version_splitting() {
|
|
||||||
let v = DebianVersion::parse("1.2.3-4ubuntu5").unwrap();
|
|
||||||
assert_eq!(v.epoch, None);
|
|
||||||
assert_eq!(v.upstream, "1.2.3");
|
|
||||||
assert_eq!(v.debian_revision.as_deref(), Some("4ubuntu5"));
|
|
||||||
assert_eq!(v.full(), "1.2.3-4ubuntu5");
|
|
||||||
assert_eq!(v.no_epoch(), "1.2.3-4ubuntu5");
|
|
||||||
|
|
||||||
let v = DebianVersion::parse("3:2.10-3").unwrap();
|
|
||||||
assert_eq!(v.epoch, Some(3));
|
|
||||||
assert_eq!(v.upstream, "2.10");
|
|
||||||
assert_eq!(v.no_epoch(), "2.10-3");
|
|
||||||
assert_eq!(v.full(), "3:2.10-3");
|
|
||||||
|
|
||||||
let v = DebianVersion::parse("1.0").unwrap();
|
|
||||||
assert_eq!(v.debian_revision, None);
|
|
||||||
assert_eq!(v.no_epoch(), "1.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn version_validation() {
|
|
||||||
assert!(DebianVersion::parse("").is_err());
|
|
||||||
assert!(DebianVersion::parse(":1.0").is_err());
|
|
||||||
assert!(DebianVersion::parse("a:_b").is_err());
|
|
||||||
assert!(DebianVersion::parse("1.0").is_ok());
|
|
||||||
assert!(DebianVersion::parse("1.0~rc1-2").is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn changelog_parsing() {
|
fn changelog_parsing() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -405,24 +238,4 @@ pkg (1.0-1+b1) unstable; urgency=medium, binary-only=yes
|
|||||||
assert!(entry.binary_only);
|
assert!(entry.binary_only);
|
||||||
assert_eq!(entry.version.full(), "1.0-1+b1");
|
assert_eq!(entry.version.full(), "1.0-1+b1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn control_parsing() {
|
|
||||||
let ci = ControlInfo::from_str(
|
|
||||||
"Source: hello\nSection: utils\nPriority: optional\nMaintainer: A B <a@b.c>\nBuild-Depends: debhelper\n\nPackage: hello\nArchitecture: any\nDescription: test\n long\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(ci.source_name(), "hello");
|
|
||||||
assert_eq!(ci.section(), "utils");
|
|
||||||
assert_eq!(ci.priority(), "optional");
|
|
||||||
assert_eq!(ci.binaries.len(), 1);
|
|
||||||
assert_eq!(ci.binaries[0].get("Package"), Some("hello"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn control_defaults() {
|
|
||||||
let ci = ControlInfo::from_str("Source: x\n\nPackage: x\nDescription: d\n").unwrap();
|
|
||||||
assert_eq!(ci.section(), "-");
|
|
||||||
assert_eq!(ci.priority(), "-");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
//! Minimal Debian control-file (deb822) paragraph parser and writer.
|
//! Debian control-file handling: a minimal deb822 paragraph parser/writer
|
||||||
|
//! plus a `debian/control` model.
|
||||||
//!
|
//!
|
||||||
//! Implements the subset of RFC822-ish parsing needed for `debian/control`,
|
//! Implements the subset of RFC822-ish parsing needed for `debian/control`,
|
||||||
//! `debian/files`, `.dsc`, `.changes` and `.buildinfo` files: paragraphs
|
//! `debian/files`, `.dsc`, `.changes` and `.buildinfo` files: paragraphs
|
||||||
//! separated by blank lines, `Field: value` entries with continuation lines
|
//! separated by blank lines, `Field: value` entries with continuation lines
|
||||||
//! starting by a single space or tab, and `#` comments.
|
//! starting by a single space or tab, and `#` comments.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// A single deb822 paragraph: an ordered list of `(field, value)` pairs.
|
/// A single deb822 paragraph: an ordered list of `(field, value)` pairs.
|
||||||
///
|
///
|
||||||
/// Values are stored with continuation-line breaks as `\n` and without the
|
/// Values are stored with continuation-line breaks as `\n` and without the
|
||||||
@@ -210,3 +213,81 @@ mod tests {
|
|||||||
assert!(p.is_empty());
|
assert!(p.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parsed `debian/control`: the source stanza plus all binary stanzas.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ControlInfo {
|
||||||
|
/// First paragraph (source package stanza).
|
||||||
|
pub source: Paragraph,
|
||||||
|
/// Remaining paragraphs (binary package stanzas).
|
||||||
|
pub binaries: Vec<Paragraph>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlInfo {
|
||||||
|
/// Parse a `debian/control` file.
|
||||||
|
pub fn parse(path: &Path) -> Result<ControlInfo, Box<dyn std::error::Error>> {
|
||||||
|
let content = std::fs::read_to_string(path)
|
||||||
|
.map_err(|e| format!("failed to read control file '{}': {}", path.display(), e))?;
|
||||||
|
Self::from_str(&content)
|
||||||
|
.map_err(|e| format!("invalid control file '{}': {}", path.display(), e).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse control content from a string.
|
||||||
|
pub fn from_str(content: &str) -> Result<ControlInfo, String> {
|
||||||
|
let paragraphs = parse_paragraphs(content);
|
||||||
|
let mut iter = paragraphs.into_iter();
|
||||||
|
let source = iter
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "control file has no paragraphs".to_string())?;
|
||||||
|
if source.get("Source").is_none() {
|
||||||
|
return Err("first control paragraph has no 'Source' field".to_string());
|
||||||
|
}
|
||||||
|
let binaries: Vec<Paragraph> = iter.collect();
|
||||||
|
for bin in &binaries {
|
||||||
|
if bin.get("Package").is_none() {
|
||||||
|
return Err("binary control paragraph has no 'Package' field".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ControlInfo { source, binaries })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source package name.
|
||||||
|
pub fn source_name(&self) -> &str {
|
||||||
|
self.source.get("Source").expect("checked at parse")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Section from the source stanza, or `'-'`.
|
||||||
|
pub fn section(&self) -> &str {
|
||||||
|
self.source.get("Section").unwrap_or("-")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Priority from the source stanza, or `'-'`.
|
||||||
|
pub fn priority(&self) -> &str {
|
||||||
|
self.source.get("Priority").unwrap_or("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod control_info_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_parsing() {
|
||||||
|
let ci = ControlInfo::from_str(
|
||||||
|
"Source: hello\nSection: utils\nPriority: optional\nMaintainer: A B <a@b.c>\nBuild-Depends: debhelper\n\nPackage: hello\nArchitecture: any\nDescription: test\n long\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(ci.source_name(), "hello");
|
||||||
|
assert_eq!(ci.section(), "utils");
|
||||||
|
assert_eq!(ci.priority(), "optional");
|
||||||
|
assert_eq!(ci.binaries.len(), 1);
|
||||||
|
assert_eq!(ci.binaries[0].get("Package"), Some("hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_defaults() {
|
||||||
|
let ci = ControlInfo::from_str("Source: x\n\nPackage: x\nDescription: d\n").unwrap();
|
||||||
|
assert_eq!(ci.section(), "-");
|
||||||
|
assert_eq!(ci.priority(), "-");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//! Reusable Debian format primitives.
|
||||||
|
//!
|
||||||
|
//! These components are independent from any build orchestration and can be
|
||||||
|
//! used by any pkh submodule (or external consumers of the library):
|
||||||
|
//!
|
||||||
|
//! - [`control`]: deb822 paragraph parsing/writing and `debian/control`
|
||||||
|
//! - [`checksums`]: file checksum registry (`Dpkg::Checksums` equivalent)
|
||||||
|
//! - [`files`]: `debian/files` artifact registry (`Dpkg::Dist::Files`)
|
||||||
|
//! - [`version`]: Debian version splitting/validation
|
||||||
|
//! - [`changelog`]: `debian/changelog` entry parsing
|
||||||
|
|
||||||
|
pub mod changelog;
|
||||||
|
pub mod checksums;
|
||||||
|
pub mod control;
|
||||||
|
pub mod files;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
pub use changelog::{parse_changelog_entry, ChangelogEntry};
|
||||||
|
pub use checksums::{Entry as ChecksumEntry, FileChecksums};
|
||||||
|
pub use control::{parse_paragraphs, write_paragraph, ControlInfo, Paragraph};
|
||||||
|
pub use files::{FilesEntry, FilesList};
|
||||||
|
pub use version::DebianVersion;
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
//! Debian version handling: splitting and validation of
|
||||||
|
//! `[epoch:]upstream[-revision]` version strings.
|
||||||
|
|
||||||
|
/// A Debian version, split into its `[epoch:]upstream[-revision]` parts.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DebianVersion {
|
||||||
|
/// Optional numeric epoch (part before the first `:`).
|
||||||
|
pub epoch: Option<u32>,
|
||||||
|
/// Upstream version (may itself contain `-` when there is no revision).
|
||||||
|
pub upstream: String,
|
||||||
|
/// Optional Debian revision (part after the last `-`).
|
||||||
|
pub debian_revision: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebianVersion {
|
||||||
|
/// Parse and validate a Debian version string.
|
||||||
|
pub fn parse(raw: &str) -> Result<DebianVersion, String> {
|
||||||
|
let raw = raw.trim();
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Err("empty version string".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (epoch, rest) = match raw.split_once(':') {
|
||||||
|
Some((e, r)) => {
|
||||||
|
let epoch: u32 = e
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("invalid epoch '{}' in version '{}'", e, raw))?;
|
||||||
|
(Some(epoch), r)
|
||||||
|
}
|
||||||
|
None => (None, raw),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The revision is everything after the last hyphen.
|
||||||
|
let (upstream, debian_revision) = match rest.rsplit_once('-') {
|
||||||
|
Some((u, r)) => (u.to_string(), Some(r.to_string())),
|
||||||
|
None => (rest.to_string(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if upstream.is_empty() {
|
||||||
|
return Err(format!("missing upstream version in '{}'", raw));
|
||||||
|
}
|
||||||
|
for c in upstream.chars() {
|
||||||
|
if !(c.is_ascii_alphanumeric()
|
||||||
|
|| matches!(c, '.' | '+' | '-' | '~' | ':')
|
||||||
|
|| !c.is_ascii())
|
||||||
|
{
|
||||||
|
return Err(format!("invalid character '{}' in version '{}'", c, raw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(rev) = &debian_revision {
|
||||||
|
for c in rev.chars() {
|
||||||
|
if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid character '{}' in revision of version '{}'",
|
||||||
|
c, raw
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DebianVersion {
|
||||||
|
epoch,
|
||||||
|
upstream,
|
||||||
|
debian_revision,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full version string, including the epoch (`[epoch:]upstream[-rev]`).
|
||||||
|
pub fn full(&self) -> String {
|
||||||
|
match (&self.epoch, &self.debian_revision) {
|
||||||
|
(Some(e), Some(r)) => format!("{}:{}-{}", e, self.upstream, r),
|
||||||
|
(Some(e), None) => format!("{}:{}", e, self.upstream),
|
||||||
|
(None, Some(r)) => format!("{}-{}", self.upstream, r),
|
||||||
|
(None, None) => self.upstream.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version string without the epoch (`upstream[-rev]`), used in artifact
|
||||||
|
/// file names.
|
||||||
|
pub fn no_epoch(&self) -> String {
|
||||||
|
match &self.debian_revision {
|
||||||
|
Some(r) => format!("{}-{}", self.upstream, r),
|
||||||
|
None => self.upstream.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_splitting() {
|
||||||
|
let v = DebianVersion::parse("1.2.3-4ubuntu5").unwrap();
|
||||||
|
assert_eq!(v.epoch, None);
|
||||||
|
assert_eq!(v.upstream, "1.2.3");
|
||||||
|
assert_eq!(v.debian_revision.as_deref(), Some("4ubuntu5"));
|
||||||
|
assert_eq!(v.full(), "1.2.3-4ubuntu5");
|
||||||
|
assert_eq!(v.no_epoch(), "1.2.3-4ubuntu5");
|
||||||
|
|
||||||
|
let v = DebianVersion::parse("3:2.10-3").unwrap();
|
||||||
|
assert_eq!(v.epoch, Some(3));
|
||||||
|
assert_eq!(v.upstream, "2.10");
|
||||||
|
assert_eq!(v.no_epoch(), "2.10-3");
|
||||||
|
assert_eq!(v.full(), "3:2.10-3");
|
||||||
|
|
||||||
|
let v = DebianVersion::parse("1.0").unwrap();
|
||||||
|
assert_eq!(v.debian_revision, None);
|
||||||
|
assert_eq!(v.no_epoch(), "1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_validation() {
|
||||||
|
assert!(DebianVersion::parse("").is_err());
|
||||||
|
assert!(DebianVersion::parse(":1.0").is_err());
|
||||||
|
assert!(DebianVersion::parse("a:_b").is_err());
|
||||||
|
assert!(DebianVersion::parse("1.0").is_ok());
|
||||||
|
assert!(DebianVersion::parse("1.0~rc1-2").is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ pub mod apt;
|
|||||||
pub mod build;
|
pub mod build;
|
||||||
/// Parse or edit a Debian changelog of a source package
|
/// Parse or edit a Debian changelog of a source package
|
||||||
pub mod changelog;
|
pub mod changelog;
|
||||||
|
/// Reusable Debian format primitives (control/deb822, checksums, versions,
|
||||||
|
/// changelog entries, artifact registries)
|
||||||
|
pub mod debian;
|
||||||
/// Build a Debian package into a binary (.deb)
|
/// Build a Debian package into a binary (.deb)
|
||||||
pub mod deb;
|
pub mod deb;
|
||||||
/// Obtain general information about distribution, series, etc
|
/// Obtain general information about distribution, series, etc
|
||||||
|
|||||||
+96
-1
@@ -1,4 +1,11 @@
|
|||||||
use gpgme::{Context, Protocol};
|
//! GPG / OpenPGP helpers: secret key discovery and inline (clear) signing
|
||||||
|
//! of Debian artifacts such as `.dsc`, `.buildinfo` and `.changes` files.
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use gpgme::{Context, Data, Protocol};
|
||||||
|
|
||||||
/// Check if a GPG key matching 'email' exists
|
/// Check if a GPG key matching 'email' exists
|
||||||
/// Returns the key ID if found, None otherwise
|
/// Returns the key ID if found, None otherwise
|
||||||
@@ -30,3 +37,91 @@ pub fn find_signing_key_for_email(
|
|||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate an OpenPGP key id / fingerprint like dpkg does.
|
||||||
|
///
|
||||||
|
/// Short (<= 8 hex chars) key IDs are rejected outright, long (16 hex chars)
|
||||||
|
/// key IDs produce a warning; anything else must be a v4 (40) or v6 (64)
|
||||||
|
/// fingerprint length.
|
||||||
|
pub fn validate_key_id(keyid: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let len = keyid.len();
|
||||||
|
if len <= 8 {
|
||||||
|
return Err(
|
||||||
|
"short OpenPGP key IDs are broken; use a key fingerprint instead".into(),
|
||||||
|
);
|
||||||
|
} else if len <= 16 {
|
||||||
|
log::warn!(
|
||||||
|
"long OpenPGP key IDs are strongly discouraged; \
|
||||||
|
use a key fingerprint instead"
|
||||||
|
);
|
||||||
|
} else if len != 40 && len != 64 {
|
||||||
|
log::warn!("OpenPGP key ID has unknown v4 or v6 fingerprint length");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a secret key whose fingerprint matches `keyid` (suffix matching
|
||||||
|
/// allows passing a long key id instead of the full fingerprint).
|
||||||
|
fn find_secret_key(ctx: &mut Context, keyid: &str) -> Result<Option<gpgme::Key>, Box<dyn Error>> {
|
||||||
|
for key_result in ctx.secret_keys()? {
|
||||||
|
let key = key_result?;
|
||||||
|
if let Ok(fingerprint) = key.fingerprint() {
|
||||||
|
if fingerprint.ends_with(keyid) {
|
||||||
|
return Ok(Some(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clearsign a file in place: the original content becomes the payload of an
|
||||||
|
/// armored inline-signed document which atomically replaces the file.
|
||||||
|
///
|
||||||
|
/// This is the same operation as dpkg's `inline_sign` + rename sequence used
|
||||||
|
/// when signing `.dsc`, `.buildinfo` or `.changes` files.
|
||||||
|
pub fn clearsign_file(path: &Path, keyid: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let content = std::fs::read(path)
|
||||||
|
.map_err(|e| format!("cannot read '{}' for signing: {}", path.display(), e))?;
|
||||||
|
|
||||||
|
let mut ctx = Context::from_protocol(Protocol::OpenPgp)
|
||||||
|
.map_err(|e| format!("cannot initialize GPGME: {}", e))?;
|
||||||
|
ctx.set_armor(true);
|
||||||
|
|
||||||
|
let key = find_secret_key(&mut ctx, keyid)?
|
||||||
|
.ok_or_else(|| format!("no secret key matching '{}' found", keyid))?;
|
||||||
|
|
||||||
|
ctx.add_signer(&key)
|
||||||
|
.map_err(|e| format!("cannot add signer '{}': {}", keyid, e))?;
|
||||||
|
|
||||||
|
let input = Data::from_bytes(&content)?;
|
||||||
|
let mut output = Data::new()?;
|
||||||
|
ctx.sign_clear(input, &mut output)
|
||||||
|
.map_err(|e| format!("clear-signing '{}' failed: {}", path.display(), e))?;
|
||||||
|
|
||||||
|
// gpgme leaves the output buffer cursor at the end after writing.
|
||||||
|
use std::io::Seek;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
output.seek(SeekFrom::Start(0))?;
|
||||||
|
let mut signed = Vec::new();
|
||||||
|
output.read_to_end(&mut signed)?;
|
||||||
|
|
||||||
|
// Atomic replace, like dpkg's signfile (write .asc then move).
|
||||||
|
let tmp = path.with_extension("asc.tmp");
|
||||||
|
std::fs::write(&tmp, &signed)
|
||||||
|
.map_err(|e| format!("cannot write '{}': {}", tmp.display(), e))?;
|
||||||
|
std::fs::rename(&tmp, path)
|
||||||
|
.map_err(|e| format!("cannot install signed '{}': {}", path.display(), e).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_id_validation() {
|
||||||
|
assert!(validate_key_id("12345678").is_err()); // short: rejected
|
||||||
|
assert!(validate_key_id("1234567890ABCDEF").is_ok()); // long: warns
|
||||||
|
assert!(validate_key_id(&"a".repeat(40)).is_ok());
|
||||||
|
assert!(validate_key_id(&"b".repeat(64)).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user