pub use object_store;
pub mod boot;
pub mod disk;
pub mod lease;
pub mod manifest;
pub mod restore;
pub mod segment;
pub mod store;
pub mod worker;
pub use boot::{restore_on_boot, BootError, BootLease, BootRestore};
pub use lease::{BucketLease, Exclusion, LeaseError};
pub use manifest::{FileEntry, Manifest, SegmentEntry, MANIFEST_VERSION};
pub use restore::{
restore_from_s3, restore_from_store, RestoreS3Error, RestoreS3Outcome, RestoreTarget,
};
pub use store::{build_store, BackupConfig, S3Location};
pub use worker::{
BackupCounters, BackupHandle, BackupTrigger, BackupWorker, WalSource, SIDE_FILES,
};
#[derive(Debug)]
pub enum BackupError {
Config(String),
Store(object_store::Error),
Io(std::io::Error),
Manifest(String),
Inconsistent(String),
Timeout(String),
Lease(String),
Lineage(String),
Conflict(String),
}
impl std::fmt::Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BackupError::Config(m) => write!(f, "backup configuration: {m}"),
BackupError::Store(e) => write!(f, "object store: {}", redact(&e.to_string())),
BackupError::Timeout(what) => write!(f, "{what} did not finish within the deadline"),
BackupError::Io(e) => write!(f, "reading the store: {e}"),
BackupError::Manifest(m) => write!(f, "manifest: {m}"),
BackupError::Inconsistent(m) => write!(f, "{m}"),
BackupError::Lease(m) => write!(f, "bucket lease: {m}"),
BackupError::Lineage(m) | BackupError::Conflict(m) => write!(f, "{m}"),
}
}
}
impl std::error::Error for BackupError {}
impl From<object_store::Error> for BackupError {
fn from(e: object_store::Error) -> Self {
BackupError::Store(e)
}
}
impl From<std::io::Error> for BackupError {
fn from(e: std::io::Error) -> Self {
BackupError::Io(e)
}
}
const REDACTED_ELEMENTS: [&str; 7] = [
"AWSAccessKeyId",
"AccessKeyId",
"SignatureProvided",
"StringToSign",
"StringToSignBytes",
"CanonicalRequest",
"CanonicalRequestBytes",
];
const REDACTED_FIELDS: [&str; 6] = [
"X-Amz-Credential=",
"X-Amz-Signature=",
"X-Amz-Security-Token=",
"Credential=",
"Signature=",
"x-amz-security-token:",
];
pub fn redact(text: &str) -> String {
let mut out = text.to_string();
for el in REDACTED_ELEMENTS {
let (open, close) = (format!("<{el}>"), format!("</{el}>"));
let mut from = 0;
while let Some(i) = out[from..].find(&open).map(|i| i + from + open.len()) {
let Some(j) = out[i..].find(&close).map(|j| j + i) else {
break;
};
out.replace_range(i..j, "[redacted]");
from = i + "[redacted]".len() + close.len();
}
}
for field in REDACTED_FIELDS {
let mut from = 0;
while let Some(i) = out[from..].find(field).map(|i| i + from + field.len()) {
let end = out[i..]
.find(|c: char| c == '&' || c == ',' || c == '"' || c == '<' || c.is_whitespace())
.map_or(out.len(), |j| j + i);
out.replace_range(i..end, "[redacted]");
from = i + "[redacted]".len();
}
}
redact_key_ids(&out)
}
fn redact_key_ids(text: &str) -> String {
const PREFIXES: [&str; 6] = ["AKIA", "ASIA", "AIDA", "AROA", "AGPA", "ANPA"];
let bytes = text.as_bytes();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < bytes.len() {
let boundary_before = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
let candidate = bytes.get(i..i + 20).filter(|w| {
PREFIXES.iter().any(|p| w.starts_with(p.as_bytes()))
&& w.iter()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
});
let boundary_after = bytes.get(i + 20).is_none_or(|b| !b.is_ascii_alphanumeric());
if boundary_before && boundary_after && candidate.is_some() {
out.push_str("[redacted]");
i += 20;
} else {
let ch = text[i..].chars().next().expect("in bounds");
out.push(ch);
i += ch.len_utf8();
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const BODY: &str = "<?xml version=\"1.0\"?><Error><Code>SignatureDoesNotMatch</Code>\
<AWSAccessKeyId>AKIAIOSFODNN7EXAMPLE</AWSAccessKeyId>\
<StringToSign>AWS4-HMAC-SHA256\n20261020T000000Z</StringToSign>\
<SignatureProvided>abc123</SignatureProvided><RequestId>R1</RequestId></Error>";
#[test]
fn an_s3_refusal_body_loses_its_credentials() {
let r = redact(BODY);
for secret in ["AKIAIOSFODNN7EXAMPLE", "abc123", "AWS4-HMAC-SHA256"] {
assert!(!r.contains(secret), "{secret} survived: {r}");
}
assert!(
r.contains("SignatureDoesNotMatch"),
"the useful part stays: {r}"
);
assert!(r.contains("<RequestId>R1</RequestId>"));
}
#[test]
fn signing_parameters_and_bare_key_ids_are_redacted() {
let r = redact(
"GET /b?X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F2026&X-Amz-Signature=deadbeef ok \
Authorization: AWS4-HMAC-SHA256 Credential=ASIAABCDEFGHIJKLMNOP/x, Signature=ff",
);
for secret in [
"AKIAIOSFODNN7EXAMPLE",
"deadbeef",
"ASIAABCDEFGHIJKLMNOP",
"=ff",
] {
assert!(!r.contains(secret), "{secret} survived: {r}");
}
assert!(r.contains(" ok "));
assert_eq!(redact("key AKIAIOSFODNN7EXAMPLE."), "key [redacted].");
assert_eq!(redact("AKIAIOSFODNN7EXAMPLEXX"), "AKIAIOSFODNN7EXAMPLEXX");
}
#[test]
fn a_store_error_displays_redacted() {
let e = BackupError::Store(object_store::Error::Generic {
store: "S3",
source: Box::new(std::io::Error::other(BODY)),
});
let shown = e.to_string();
assert!(!shown.contains("AKIAIOSFODNN7EXAMPLE"), "{shown}");
assert!(shown.contains("[redacted]"));
}
}