use crate::file_attr::RelativeFileAttr;
use regex::Regex;
use relative_path::RelativePathBuf;
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::{BufRead, BufReader, Lines};
use std::path::Path;
enum Section {
Md5,
Sha256,
Sha512,
}
#[derive(Debug, thiserror::Error)]
pub enum ReleaseError {
#[error("{0}")]
Io(#[from] io::Error),
#[error("unexpected line format: {0}")]
Format(String),
#[error("size error for line: {0}")]
Size(String),
}
fn parse_release_line_stream(
stream: impl Iterator<Item = io::Result<String>>,
) -> Result<Vec<RelativeFileAttr>, ReleaseError> {
let algorithm_re = Regex::new(r"^(\w+):$").unwrap();
let split_re = Regex::new(r"\s+").unwrap();
let mut files = HashMap::new();
let mut current_section = None;
for line_result in stream {
let origin_line = line_result?;
if !origin_line.starts_with(' ') {
current_section = if let Some(m) = algorithm_re.captures(&origin_line) {
match m.get(1).unwrap().as_str() {
"MD5Sum" => Some(Section::Md5),
"SHA256" => Some(Section::Sha256),
"SHA512" => Some(Section::Sha512),
_ => None, }
} else {
None };
continue;
}
let line = origin_line.trim();
if line.is_empty() {
continue;
}
if let Some(section) = current_section.as_ref() {
let mut parts = split_re.splitn(line, 3);
let hash = parts
.next()
.ok_or_else(|| ReleaseError::Format(line.to_string()))?;
let size = parts
.next()
.ok_or_else(|| ReleaseError::Format(line.to_string()))?
.parse()
.map_err(|_| ReleaseError::Size(line.to_string()))?;
let path = parts
.next()
.map(|s| RelativePathBuf::from(s))
.ok_or_else(|| ReleaseError::Format(line.to_string()))?;
let attr = files
.entry(path.clone())
.or_insert_with(|| RelativeFileAttr {
path,
size: Some(size),
md5sum: None,
sha256sum: None,
sha512sum: None,
});
match section {
Section::Md5 => {
attr.md5sum = Some(hash.to_string());
}
Section::Sha256 => {
attr.sha256sum = Some(hash.to_string());
}
Section::Sha512 => {
attr.sha512sum = Some(hash.to_string());
}
}
}
}
Ok(files.into_values().collect())
}
pub fn parse_release_file<P>(path: P) -> Result<Vec<RelativeFileAttr>, ReleaseError>
where
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
parse_release_line_stream(reader.lines())
}
enum SignedMessageParserState {
ExpectStart,
ExpectHash,
ExpectBlank,
WaitEnd,
Finished,
}
struct SignedMessageLines<R: BufRead> {
lines: Lines<R>,
state: SignedMessageParserState,
}
impl<R: BufRead> SignedMessageLines<R> {
pub fn new(reader: R) -> Self {
Self {
lines: reader.lines(),
state: SignedMessageParserState::ExpectStart,
}
}
}
impl<R: BufRead> Iterator for SignedMessageLines<R> {
type Item = io::Result<String>;
fn next(&mut self) -> Option<Self::Item> {
if matches!(self.state, SignedMessageParserState::Finished) {
return None;
}
while let Some(line_res) = self.lines.next() {
match line_res {
Ok(line) => match self.state {
SignedMessageParserState::ExpectStart => {
if line.starts_with("-----BEGIN PGP SIGNED MESSAGE-----") {
self.state = SignedMessageParserState::ExpectHash;
}
}
SignedMessageParserState::ExpectHash => {
if line.starts_with("Hash:") {
self.state = SignedMessageParserState::ExpectBlank;
}
}
SignedMessageParserState::ExpectBlank => {
if line.trim().is_empty() {
self.state = SignedMessageParserState::WaitEnd;
}
}
SignedMessageParserState::WaitEnd => {
return if line.starts_with("-----BEGIN PGP SIGNATURE-----") {
self.state = SignedMessageParserState::Finished;
None
} else {
Some(Ok(line))
};
}
SignedMessageParserState::Finished => {
panic!("unreachable");
}
},
Err(e) => return Some(Err(e)),
}
}
None
}
}
pub fn parse_inrelease_file<P>(path: P) -> Result<Vec<RelativeFileAttr>, ReleaseError>
where
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
parse_release_line_stream(SignedMessageLines::new(reader))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_parse_release_file() {
let release_content = r#"Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
0ed6d4c8891eb86358b94bb35d9e4da4 1484322 contrib/Contents-all
d0a0325a97c42fd5f66a8c3e29bcea64 98581 contrib/Contents-all.gz
SHA256:
d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79 1484322 contrib/Contents-all
c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0 98581 contrib/Contents-all.gz
"#;
let mut test_file = NamedTempFile::new().unwrap();
test_file.write_all(release_content.as_bytes()).unwrap();
test_file.flush().unwrap();
let result = parse_release_file(test_file.path()).unwrap();
assert_eq!(result.len(), 2);
let mut found_contents_all = false;
let mut found_contents_all_gz = false;
for file in &result {
if file.path.as_str() == "contrib/Contents-all" {
assert_eq!(file.size, Some(1484322));
assert_eq!(
file.md5sum,
Some("0ed6d4c8891eb86358b94bb35d9e4da4".to_string())
);
assert_eq!(
file.sha256sum,
Some(
"d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79"
.to_string()
)
);
found_contents_all = true;
} else if file.path.as_str() == "contrib/Contents-all.gz" {
assert_eq!(file.size, Some(98581));
assert_eq!(
file.md5sum,
Some("d0a0325a97c42fd5f66a8c3e29bcea64".to_string())
);
assert_eq!(
file.sha256sum,
Some(
"c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0"
.to_string()
)
);
found_contents_all_gz = true;
}
}
assert!(found_contents_all, "Should find contrib/Contents-all");
assert!(found_contents_all_gz, "Should find contrib/Contents-all.gz");
}
#[test]
fn test_parse_inrelease_file() {
let inrelease_content = r#"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
0ed6d4c8891eb86358b94bb35d9e4da4 1484322 contrib/Contents-all
d0a0325a97c42fd5f66a8c3e29bcea64 98581 contrib/Contents-all.gz
SHA256:
d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79 1484322 contrib/Contents-all
c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0 98581 contrib/Contents-all.gz
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEETLUBkCB7R1ij9zp5btDnuCZD4TEFAmi8FQAACgkQbtDnuCZD
4TFrlw/7B94IEcTIVbakR7nZ9/ThA5HsdOo3UmUSh75owDRbs/dCs9IZOauo+qov
cg9DtMOwpab77HYFDcIWFPyA50VaHzaMc7CnzKjq1lX6KchDH6cNrvzTeTzJdVsj
rvvYcMAMQGiJgOU2MPwka/VoD2p4HINNSAsxvrnrAybS+7Uerop0GeSDpXoJ3ynr
9OZaBS1f8bavmQgfDiSpsLJZ+LulynrawM4bcgCCTrvndX+0aRtcE4qkmQ9jq2a4
IldfWVo3vUGvTWhuzXBtvl8hWCktdzj/2OXHwMSSIGcgusZgG18LzRoJeem/JUKu
PQUmJFVdbhUt3zIo0FRCG22T6Q/wKZMj0cJuvdGSkPV/jgUZGplPSF7t/aMCN8p7
zxIt79SKXPeHFI5dZQvAT9fazg3JX2R0NJOL2hH6rZjtWE3OThSPXDacsneDQ4T+
F7kVlutS0SfEdF8nnd4tboNZS/GHYFM98e9TGeWgxvvs4KMXO6hdA1dmEl8gO9x6
AxobWe00TL1vpeNZse9CxUPdGD28ZNjn4lUO4mMQL6JwMsNeK+DusQZNNUcbCRdu
tBCfF6j+YygiHITBKBj2T6H02utYD9JuOINkm4I5xXvOO0ZZyUetYlyd3+AWpFr6
7S5MsrsFJSvQ2Oo6Kt0D1mOJJqBVL85eHMVhGZJ9VvBBH/KqJpOJAjMEAQEIAB0W
IQS45fExdtKnp1IgAoB426O8R+8iZQUCaLwVAQAKCRB426O8R+8iZTNLD/0WDudI
H3jrsKTZ/CmKeidU5xLvt5V5XdSpQD9gDDRiU16+3iXlTyr21+uT6t1ln85xp6f/
a7r545PfgRvfP+ujSHfQnfybgCv1Z4irBv+Yn0wfIYhyvTS65FBr1ek5U41d+ZKF
PwRywZ9ZgdwtSXos7uN63v7UZ4UZIFaTVRIWAXYgxfz9JOS3sr/j2C64mRj/z8q/
rCS9lAleC/cl3genM2sJA1NvAnXXFsCm2Xuii+nTeLQdLkvYQDaI/miWZ56HfbD8
/T8HPjbRdV8kzCrhT5RZ9Y3EE8SdbeoaFi92Oe/0j6ERP9Hct33mXbJfMBfl2DZI
b9apdTiF5NWkCtIta8fAI5JUn2iwoha65q1wHGDCVruyFy3DLbABtpYGLzBr0oaM
5x+483i+QbZ8mFSS/9Z+pRFHjZJGnyZ2gUaIAf/FLZbugeNDBTTBY76l07/z7RKH
QQXXK6EVEV9VZw4rdzEQ9HMRIWQYQMsii4cZc+AJwLXLTnhEkag4OnsSJbnOsa5R
lvtAfkExNdOJAMhbElgYUfrD6jDHZn1e1J0omq2SYV7mrCDbwrfJaEywTI4AbB7V
cpQKe/FJuY3rjHEhvwxN5tW3x+z3jJ4BkV+xBLAZgEU/CQP7b2cLNwkIEr2b8Evt
osLESOMHayZxXFgj+h329+QHP9bSxv6s9FXBDoh1BAEWCAAdFiEETWT+wRnCApBn
1ueR+NJYW4eD1IEFAmi8FYQACgkQ+NJYW4eD1IEcCwEAxNzCORTxjfZvSd7n7SVH
hNmCeDqJgGNMgkwYcwjwqpgBAPvdB2qYLunIpWcGu6VEQFOkC3A9iHsp+WFAXkMP
4NYJ
-----END PGP SIGNATURE-----
"#;
let mut test_file = NamedTempFile::new().unwrap();
test_file.write_all(inrelease_content.as_bytes()).unwrap();
test_file.flush().unwrap();
let result = parse_inrelease_file(test_file.path()).unwrap();
assert_eq!(result.len(), 2);
let mut found_contents_all = false;
let mut found_contents_all_gz = false;
for file in &result {
if file.path.as_str() == "contrib/Contents-all" {
assert_eq!(file.size, Some(1484322));
assert_eq!(
file.md5sum,
Some("0ed6d4c8891eb86358b94bb35d9e4da4".to_string())
);
assert_eq!(
file.sha256sum,
Some(
"d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79"
.to_string()
)
);
found_contents_all = true;
} else if file.path.as_str() == "contrib/Contents-all.gz" {
assert_eq!(file.size, Some(98581));
assert_eq!(
file.md5sum,
Some("d0a0325a97c42fd5f66a8c3e29bcea64".to_string())
);
assert_eq!(
file.sha256sum,
Some(
"c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0"
.to_string()
)
);
found_contents_all_gz = true;
}
}
assert!(found_contents_all, "Should find contrib/Contents-all");
assert!(found_contents_all_gz, "Should find contrib/Contents-all.gz");
}
#[test]
fn test_parse_inrelease_file_no_signature() {
let inrelease_content = r#"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
Origin: Debian
Label: Debian
Suite: oldstable
Version: 12.12
MD5Sum:
0ed6d4c8891eb86358b94bb35d9e4da4 1484322 contrib/Contents-all
d0a0325a97c42fd5f66a8c3e29bcea64 98581 contrib/Contents-all.gz
SHA256:
d6c9c82f4e61b4662f9ba16b9ebb379c57b4943f8b7813091d1f637325ddfb79 1484322 contrib/Contents-all
c22d03bdd4c7619e1e39e73b4a7b9dfdf1cc1141ed9b10913fbcac58b3a943d0 98581 contrib/Contents-all.gz
"#;
let mut test_file = NamedTempFile::new().unwrap();
test_file.write_all(inrelease_content.as_bytes()).unwrap();
test_file.flush().unwrap();
let result = parse_inrelease_file(test_file.path()).unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_inrelease_file_empty() {
let inrelease_content = "";
let mut test_file = NamedTempFile::new().unwrap();
test_file.write_all(inrelease_content.as_bytes()).unwrap();
test_file.flush().unwrap();
let result = parse_inrelease_file(test_file.path()).unwrap();
assert_eq!(result.len(), 0);
}
}