use {
crate::{
control::{
ControlField, ControlParser, ControlStanza, Field, MutableControlFile,
MutableControlStanza, ParseError,
},
hash::Hash,
indexfile::IndexFile,
parse_size,
version::{ProvidedName, Version},
RepositoryFile,
},
ouroboros::self_referencing,
serde::{Deserialize, Serialize},
smol::io::{AsyncRead, AsyncReadExt},
std::{io, sync::Arc},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FileHash {
Sha512,
Sha256,
Md5,
}
impl FileHash {
fn name(self) -> &'static str {
match self {
FileHash::Sha512 => "SHA512",
FileHash::Sha256 => "SHA256",
FileHash::Md5 => "MD5sum",
}
}
fn priority(self) -> u8 {
match self {
FileHash::Sha512 => 0,
FileHash::Sha256 => 1,
FileHash::Md5 => 2,
}
}
}
#[derive(Default, Clone, Debug)]
pub struct Source<'a> {
src: &'a str,
name: &'a str,
binary: &'a str,
version: &'a str,
directory: Option<&'a str>,
files: Option<(&'a str, FileHash)>,
repo_files: Vec<RepositoryFile>,
}
impl std::fmt::Display for Source<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}={}", self.name, self.version)
}
}
impl AsRef<str> for Source<'_> {
fn as_ref(&self) -> &str {
self.src
}
}
impl<'a> Source<'a> {
pub fn name(&self) -> &'a str {
self.name
}
pub fn binary(&self) -> impl Iterator<Item = &'a str> + 'a {
self.binary
.split(',')
.map(str::trim)
.filter(|b| !b.is_empty())
}
pub fn version(&self) -> &'a str {
self.version
}
pub fn parsed_version(&self) -> Result<Version<&'a str>, ParseError> {
Version::try_from(self.version)
}
pub fn directory(&self) -> Option<&'a str> {
self.directory
}
pub fn hash_field(&self) -> Option<&'static str> {
self.files.as_ref().map(|(_, hash)| hash.name())
}
fn parse_file_list<F: Fn(&str) -> String>(
dir: &str,
field: &str,
hash_field: &'static str,
make_url: F,
) -> Result<Vec<RepositoryFile>, ParseError> {
let with_sep = dir.ends_with('/');
field
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(|line| {
let mut parts = line.split_ascii_whitespace();
let digest = parts.next().ok_or_else(|| {
ParseError::from(format!("invalid file entry, missing digest: {}", line))
})?;
let size = parts.next().ok_or_else(|| {
ParseError::from(format!("invalid file entry, missing size: {}", line))
})?;
let path = parts.next().ok_or_else(|| {
ParseError::from(format!("invalid file entry, missing path: {}", line))
})?;
if parts.next().is_some() {
return Err(ParseError::from(format!(
"invalid file entry, too many columns: {}",
line
)));
}
let hash = Hash::from_hex(hash_field, digest).map_err(|err| {
ParseError::from(format!("invalid {} digest {}: {}", hash_field, digest, err))
})?;
let size = parse_size(size.as_bytes())
.map_err(|err| ParseError::from(format!("invalid size {}: {}", size, err)))?;
let mut full_path =
String::with_capacity(dir.len() + path.len() + if with_sep { 0 } else { 1 });
full_path.push_str(dir);
if !with_sep {
full_path.push('/');
}
full_path.push_str(path);
Ok(RepositoryFile {
path: make_url(&full_path),
fetch_path: None,
size,
hash,
})
})
.collect()
}
pub(crate) fn repo_files<F: Fn(&str) -> String>(
&self,
make_url: F,
) -> Result<Vec<RepositoryFile>, ParseError> {
let dir = self.directory.ok_or_else(|| {
ParseError::from(format!("Source {} lacks Directory field", self.name))
})?;
let (block, hash) = self.files.as_ref().ok_or_else(|| {
ParseError::from(format!(
"Source {} lacks Checksums-Sha512, Checksums-Sha256 or Files field",
self.name
))
})?;
Self::parse_file_list(dir, block, hash.name(), make_url)
}
pub fn control(&self) -> Result<ControlStanza<'a>, ParseError> {
ControlStanza::parse(self.src)
}
pub fn field(&self, name: &str) -> Option<&'a str> {
ControlParser::new(self.src)
.map(|f| f.unwrap())
.find(|f| f.is_a(name))
.map(|f| f.value())
}
pub fn ensure_field(&self, name: &str) -> Result<&'a str, ParseError> {
self.field(name).ok_or_else(|| {
ParseError::from(format!(
"Source {} description lacks field {}",
&self.name, name
))
})
}
pub fn files(&self) -> impl Iterator<Item = &RepositoryFile> {
self.repo_files.iter()
}
pub fn fields(&self) -> impl Iterator<Item = ControlField<'a>> {
ControlParser::new(self.src).map(|f| f.unwrap())
}
pub(crate) fn clone_with_files<F: Fn(&str) -> String>(&self, f: F) -> Result<Self, ParseError> {
let repo_files = self.repo_files(&f)?;
Ok(Self {
repo_files,
..self.clone()
})
}
fn try_parse_from(parser: &mut ControlParser<'a>) -> Result<Option<Source<'a>>, ParseError> {
let mut parsed = false;
let snap = unsafe { parser.snap() };
let mut source = Source::<'a>::default();
let mut best: Option<(&'a str, FileHash)> = None;
parser.try_fold(
(),
|(), field: Result<ControlField<'a>, ParseError>| -> Result<(), ParseError> {
let field = field?;
if !parsed {
parsed = true;
}
if field.is_a("Package") {
source.name = field.value().trim();
} else if field.is_a("Binary") {
source.binary = field.value().trim();
} else if field.is_a("Version") {
source.version = field.value().trim();
} else if field.is_a("Directory") {
source.directory = Some(field.value().trim());
} else if field.is_a("Checksums-Sha512") {
let cand = (field.value(), FileHash::Sha512);
if best
.as_ref()
.is_none_or(|(_, h)| cand.1.priority() < h.priority())
{
best = Some(cand);
}
} else if field.is_a("Checksums-Sha256") {
let cand = (field.value(), FileHash::Sha256);
if best
.as_ref()
.is_none_or(|(_, h)| cand.1.priority() < h.priority())
{
best = Some(cand);
}
} else if field.is_a("Files") {
let cand = (field.value(), FileHash::Md5);
if best
.as_ref()
.is_none_or(|(_, h)| cand.1.priority() < h.priority())
{
best = Some(cand);
}
}
Ok(())
},
)?;
if !parsed {
Ok(None)
} else if source.name.is_empty() {
Err(ParseError::from("Field Package not found"))
} else if source.version.is_empty() {
Err(ParseError::from("Field Version not found"))
} else {
source.src = unsafe { snap.into_slice(parser) };
source.files = best;
Ok(Some(source))
}
}
}
impl<'a> From<&Source<'a>> for MutableControlStanza {
fn from(stanza: &Source<'a>) -> Self {
MutableControlStanza::parse(stanza.src).unwrap()
}
}
pub struct Sources {
inner: Arc<SourcesInner>,
}
impl Clone for Sources {
fn clone(&self) -> Self {
Sources {
inner: Arc::clone(&self.inner),
}
}
}
impl Sources {
pub fn get(&self, index: usize) -> Option<&Source<'_>> {
self.inner.with_sources(|sources| sources.get(index))
}
pub fn len(&self) -> usize {
self.inner.with_sources(|sources| sources.len())
}
pub fn is_empty(&self) -> bool {
self.inner.with_sources(|sources| sources.is_empty())
}
pub fn source_by_name(&self, name: &str) -> Option<&Source<'_>> {
self.inner
.with_sources(|sources| sources.iter().find(|src| src.name() == name))
}
pub fn archive_id(&self) -> u32 {
self.inner.with_archive_id(|id| *id)
}
pub fn sources(&self) -> impl Iterator<Item = &Source<'_>> {
self.inner.with_sources(|sources| sources.iter())
}
pub fn new(data: IndexFile, archive_id: u32) -> Result<Self, ParseError> {
Ok(Sources {
inner: Arc::new(
SourcesInnerTryBuilder {
archive_id,
data,
sources_builder: |data: &'_ IndexFile| -> Result<Vec<Source<'_>>, ParseError> {
let mut parser = ControlParser::new(data.as_str());
let mut sources: Vec<Source<'_>> = vec![];
while let Some(source) = Source::try_parse_from(&mut parser)? {
sources.push(source);
}
Ok(sources)
},
}
.try_build()?,
),
})
}
pub async fn read<R: AsyncRead + Unpin + Send>(r: &mut R) -> io::Result<Self> {
let mut buf = String::new();
r.read_to_string(&mut buf).await?;
buf.try_into().map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Error parsing sources file: {}", err),
)
})
}
}
impl From<&Sources> for MutableControlFile {
fn from(srcs: &Sources) -> Self {
srcs.inner
.with_sources(|srcs| srcs.iter())
.map(MutableControlStanza::from)
.collect()
}
}
impl Serialize for Sources {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.inner.with_data(|d| d.as_str());
serializer.serialize_str(s)
}
}
impl<'de> Deserialize<'de> for Sources {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Sources::try_from(s).map_err(serde::de::Error::custom)
}
}
impl TryFrom<&str> for Sources {
type Error = ParseError;
fn try_from(inp: &str) -> Result<Self, Self::Error> {
Self::new(inp.to_owned().into(), 0)
}
}
impl TryFrom<String> for Sources {
type Error = ParseError;
fn try_from(inp: String) -> Result<Self, Self::Error> {
Self::new(inp.into(), 0)
}
}
impl TryFrom<Vec<u8>> for Sources {
type Error = ParseError;
fn try_from(inp: Vec<u8>) -> Result<Self, Self::Error> {
Self::new(
String::from_utf8(inp)
.map_err(|err| ParseError::from(format!("{}", err)))?
.into(),
0,
)
}
}
#[self_referencing]
struct SourcesInner {
archive_id: u32,
data: IndexFile,
#[borrows(data)]
#[covariant]
sources: Vec<Source<'this>>,
}
#[derive(Default, Clone)]
pub struct SourceUniverse {
sources: Vec<Sources>,
}
impl SourceUniverse {
pub fn new() -> Self {
Self { sources: vec![] }
}
pub fn from_sources(sources: Vec<Sources>) -> Self {
Self { sources }
}
pub fn push(&mut self, sources: Sources) {
self.sources.push(sources);
}
pub fn len(&self) -> usize {
self.sources.iter().map(|s| s.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.sources.iter().all(|s| s.is_empty())
}
pub fn all(&self) -> impl Iterator<Item = &Sources> {
self.sources.iter()
}
pub fn source<'a, 'b>(&'a self, name: &'b str) -> impl Iterator<Item = &'a Source<'a>> + 'a
where
'b: 'a,
{
self.sources
.iter()
.flat_map(move |sources| sources.sources().filter(move |src| src.name() == name))
}
pub(crate) fn find<R, N>(&self, name: N) -> Result<Vec<(&'_ Source<'_>, usize)>, ParseError>
where
R: AsRef<str>,
N: AsRef<ProvidedName<R>>,
{
let name = name.as_ref();
let target_name = name.name().as_ref();
let target_version = name.version().map(|v| v.as_ref().as_ref());
let mut found = Vec::new();
for sources in &self.sources {
for src in sources.sources() {
if src.name() != target_name && src.binary().all(|b| b != target_name) {
continue;
}
if let Some(tv) = &target_version {
if let Ok(v) = src.parsed_version() {
if v != tv {
continue;
}
}
}
found.push((src, sources.archive_id() as usize));
}
}
Ok(found)
}
}
impl<R> AsRef<ProvidedName<R>> for ProvidedName<R> {
fn as_ref(&self) -> &ProvidedName<R> {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_source() -> &'static str {
"Package: 1oom
Binary: 1oom
Version: 1.11.2-1
Maintainer: Debian Games Team <pkg-games-devel@lists.alioth.debian.org>
Uploaders: Joseph Nahmias <jello@debian.org>
Build-Depends: debhelper-compat (= 13), libsamplerate0-dev, libsdl2-dev, libsdl2-mixer-dev
Architecture: any
Standards-Version: 4.7.2
Format: 3.0 (quilt)
Checksums-Sha512:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 10 1oom_1.11.2-1.dsc
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 20 1oom_1.11.2.orig.tar.gz
Checksums-Sha256:
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef 42 1oom_1.11.2-1.dsc
2222222222222222222222222222222222222222222222222222222222222222 1337 1oom_1.11.2.orig.tar.gz
Files:
ffffffffffffffffffffffffffffffff 42 1oom_1.11.2-1.dsc
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1337 1oom_1.11.2.orig.tar.gz
Directory: pool/contrib/1/1oom
Priority: optional
Section: contrib/misc
"
}
#[test]
fn parses_files_prefers_sha512_then_sha256() {
let sources: Sources = sample_source().try_into().unwrap();
let src = sources.get(0).unwrap();
let files = src.repo_files(|s| s.to_string()).unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0].path, "pool/contrib/1/1oom/1oom_1.11.2-1.dsc");
assert_eq!(files[0].size, 10);
assert_eq!(files[1].size, 20);
assert_eq!(src.hash_field(), Some("SHA512"));
}
#[test]
fn repo_files_join_directory() {
let sources: Sources = sample_source().try_into().unwrap();
let src = sources.get(0).unwrap();
let files = src.repo_files(|s| s.to_string()).unwrap();
assert_eq!(files[0].path, "pool/contrib/1/1oom/1oom_1.11.2-1.dsc");
}
#[test]
fn universe_find_matches_any_or_exact() {
let srcs: Sources = sample_source().try_into().unwrap();
let uni = SourceUniverse::from_sources(vec![srcs]);
let any = ProvidedName::Any("1oom");
assert_eq!(uni.find(&any).unwrap().len(), 1);
let exact = ProvidedName::Exact("1oom", Version::new("1.11.2-1"));
assert_eq!(uni.find(&exact).unwrap().len(), 1);
let wrong = ProvidedName::Exact("1oom", Version::new("9.9.9"));
assert!(uni.find(&wrong).unwrap().is_empty());
}
#[test]
fn universe_find_collects_all_matches_when_no_version() {
let s1: Sources = sample_source().try_into().unwrap();
let s2: Sources = sample_source().try_into().unwrap();
let uni = SourceUniverse::from_sources(vec![s1, s2]);
let any = ProvidedName::Any("1oom");
assert_eq!(uni.find(any).unwrap().len(), 2);
}
}