use std::path::{Path, PathBuf};
use std::sync::Arc;
use rayon::prelude::*;
use crate::args::{CancelToken, LinkOptions};
use crate::error::{Error, Result};
use crate::ids::FileId;
use super::append::AppendVec;
use super::archive::{Archive, Member, MemberData};
use super::identify::{FileFormat, GccLtoProbe, identify_with};
use super::map::{self, Backing};
use super::read;
use super::source::InputProvider;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Source {
Path(PathBuf),
Bytes {
name: PathBuf,
data: Arc<[u8]>,
},
}
#[derive(Debug)]
pub struct InputFile {
path: PathBuf,
member: Option<String>,
parent: Option<FileId>,
backing: Arc<Backing>,
start: usize,
end: usize,
format: FileFormat,
}
impl InputFile {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn member(&self) -> Option<&str> {
self.member.as_deref()
}
#[must_use]
pub fn parent(&self) -> Option<FileId> {
self.parent
}
#[must_use]
pub fn data(&self) -> &[u8] {
self.backing
.bytes()
.get(self.start..self.end)
.unwrap_or_default()
}
#[must_use]
pub fn format(&self) -> FileFormat {
self.format
}
#[must_use]
pub fn is_mapped(&self) -> bool {
self.backing.is_mapped()
}
pub fn malformed(&self, offset: u64, what: impl Into<String>) -> Error {
Error::Malformed {
file: self.path.clone(),
member: self.member.clone(),
offset,
what: what.into(),
}
}
pub fn archive(&self) -> Result<Archive<'_>> {
Archive::parse(&self.path, self.data())
}
}
#[derive(Debug, Default)]
pub struct FileTable {
files: AppendVec<InputFile>,
gcc_lto_probe: Option<GccLtoProbe>,
provider: Option<Arc<dyn InputProvider>>,
cancel: Option<CancelToken>,
}
fn too_many_files(path: &Path) -> Error {
Error::Limit(format!("too many input files (at {})", path.display()))
}
impl FileTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_gcc_lto_probe(probe: GccLtoProbe) -> Self {
Self {
gcc_lto_probe: Some(probe),
..Self::default()
}
}
#[must_use]
pub fn for_link(options: &LinkOptions) -> Self {
Self {
provider: options.input_provider.clone(),
cancel: options.cancel.clone(),
..Self::default()
}
}
#[must_use]
pub fn provider(&self) -> Option<&dyn InputProvider> {
self.provider.as_deref()
}
fn check_cancelled(&self) -> Result<()> {
self.cancel.as_ref().map_or(Ok(()), CancelToken::check)
}
fn open(&self, path: &Path) -> Result<Backing> {
self.check_cancelled()?;
if let Some(data) = self.provider.as_ref().and_then(|p| p.read(path)) {
return Ok(Backing::Shared(data));
}
map::load(path).map_err(|error| Error::io(path, error))
}
#[must_use]
pub fn len(&self) -> usize {
self.files.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, id: FileId) -> Option<&InputFile> {
self.files.get(id.index())
}
#[must_use]
pub fn data(&self, id: FileId) -> &[u8] {
self.get(id).map_or(&[], InputFile::data)
}
pub fn iter(&self) -> impl Iterator<Item = (FileId, &InputFile)> {
(0..self.len()).map_while(|index| Some((FileId::new(index), self.files.get(index)?)))
}
fn push(&self, file: InputFile) -> Result<FileId> {
let path = file.path.clone();
match self.files.push(file) {
Ok(index) => Ok(FileId::new(index)),
Err(_) => Err(too_many_files(&path)),
}
}
fn whole(&self, path: PathBuf, backing: Backing) -> InputFile {
let end = backing.bytes().len();
let format = identify_with(backing.bytes(), self.gcc_lto_probe);
InputFile {
path,
member: None,
parent: None,
backing: Arc::new(backing),
start: 0,
end,
format,
}
}
pub fn load_path(&self, path: &Path) -> Result<FileId> {
let backing = self.open(path)?;
self.push(self.whole(path.to_path_buf(), backing))
}
pub fn add_bytes(&self, name: impl Into<PathBuf>, data: Arc<[u8]>) -> Result<FileId> {
self.check_cancelled()?;
self.push(self.whole(name.into(), Backing::Shared(data)))
}
pub fn load(&self, source: &Source) -> Result<FileId> {
match source {
Source::Path(path) => self.load_path(path),
Source::Bytes { name, data } => self.add_bytes(name.clone(), Arc::clone(data)),
}
}
pub fn load_all(&self, sources: &[Source]) -> Vec<Result<FileId>> {
let mut first: std::collections::HashMap<&Path, usize> = std::collections::HashMap::new();
let repeat_of: Vec<Option<usize>> = sources
.iter()
.enumerate()
.map(|(index, source)| match source {
Source::Path(path) => match first.get(path.as_path()) {
Some(&earlier) => Some(earlier),
None => {
first.insert(path, index);
None
}
},
Source::Bytes { .. } => None,
})
.collect();
let loaded: Vec<Option<Result<InputFile>>> = sources
.par_iter()
.zip(&repeat_of)
.map(|(source, repeat)| {
repeat.is_none().then(|| match source {
Source::Path(path) => self
.open(path)
.map(|backing| self.whole(path.clone(), backing)),
Source::Bytes { name, data } => self
.check_cancelled()
.map(|()| self.whole(name.clone(), Backing::Shared(Arc::clone(data)))),
})
})
.collect();
let copy = |file: &InputFile| InputFile {
path: file.path.clone(),
member: None,
parent: None,
backing: Arc::clone(&file.backing),
start: file.start,
end: file.end,
format: file.format,
};
let copies: Vec<Option<Result<InputFile>>> = repeat_of
.iter()
.zip(sources)
.map(|(repeat, source)| {
let earlier = loaded.get((*repeat)?)?.as_ref()?;
Some(match (earlier, source) {
(Ok(file), _) => Ok(copy(file)),
(Err(_), Source::Path(path)) => self
.open(path)
.map(|backing| self.whole(path.clone(), backing)),
(Err(_), Source::Bytes { .. }) => {
Err(Error::Internal("repeated in-memory input".into()))
}
})
})
.collect();
loaded
.into_iter()
.zip(copies)
.map(|(loaded, copy)| {
loaded
.or(copy)
.unwrap_or_else(|| Err(Error::Internal("input not loaded".into())))
.and_then(|file| self.push(file))
})
.collect()
}
pub fn add_member(&self, archive: FileId, member: &Member<'_>) -> Result<FileId> {
let entry = self.member_entry(archive, member)?;
self.push_member(entry)
}
pub fn push_member(&self, entry: MemberEntry) -> Result<FileId> {
self.push(entry.0)
}
pub fn member_entry(&self, archive: FileId, member: &Member<'_>) -> Result<MemberEntry> {
let Some(parent) = self.get(archive) else {
return Err(Error::malformed(
"<unknown archive>",
member.header_offset,
"archive file ID",
));
};
let name = member.display_name();
let file = match member.data {
MemberData::Inline { offset, bytes } => {
let bad = || parent.malformed(member.header_offset, "archive member range");
let offset = read::to_usize(offset).ok_or_else(bad)?;
let start = parent.start.checked_add(offset).ok_or_else(bad)?;
let end = start.checked_add(bytes.len()).ok_or_else(bad)?;
if end > parent.end {
return Err(bad());
}
let data = parent.backing.bytes().get(start..end).ok_or_else(bad)?;
if !std::ptr::eq(data, bytes) {
return Err(bad());
}
InputFile {
path: parent.path.clone(),
member: Some(name),
parent: Some(archive),
backing: Arc::clone(&parent.backing),
start,
end,
format: identify_with(data, self.gcc_lto_probe),
}
}
MemberData::External { .. } => {
let path = member.external_path().unwrap_or_default();
let backing = self.open(&path)?;
let mut file = self.whole(path, backing);
file.member = Some(name);
file.parent = Some(archive);
file
}
};
Ok(MemberEntry(file))
}
}
#[derive(Debug)]
pub struct MemberEntry(InputFile);
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)] mod tests {
use super::*;
use crate::input::identify::TextKind;
#[test]
fn a_repeated_path_is_mapped_once() {
let table = FileTable::new();
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let missing = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("no-such-file");
let sources = [
Source::Path(manifest.clone()),
Source::Path(missing.clone()),
Source::Path(manifest.clone()),
Source::Path(missing),
];
let ids = table.load_all(&sources);
let first = *ids[0].as_ref().unwrap();
let again = *ids[2].as_ref().unwrap();
assert_ne!(first, again, "each occurrence has its own entry");
let (a, b) = (table.get(first).unwrap(), table.get(again).unwrap());
assert_eq!(a.path(), manifest);
assert_eq!(b.path(), manifest);
assert!(std::ptr::eq(a.data(), b.data()), "one mapping");
assert!(ids[1].is_err() && ids[3].is_err());
}
#[test]
fn bytes_inputs_and_lookup() {
let table = FileTable::new();
assert!(table.is_empty());
let data: Arc<[u8]> = Arc::from(&b"INPUT(a.o)\n"[..]);
let id = table.add_bytes("script", data).unwrap();
assert_eq!(id, FileId::new(0));
let file = table.get(id).unwrap();
assert_eq!(file.data(), b"INPUT(a.o)\n");
assert_eq!(file.format(), FileFormat::Text(TextKind::Other));
assert_eq!(file.path(), Path::new("script"));
assert!(!file.is_mapped());
assert!(table.get(FileId::new(1)).is_none());
assert_eq!(table.data(FileId::new(7)), b"");
}
#[test]
fn load_all_keeps_source_order_and_reports_failures() {
let sources: Vec<Source> = (0..50)
.map(|i| {
if i == 17 {
Source::Path(PathBuf::from("/nonexistent/qld/input/file.o"))
} else {
Source::Bytes {
name: PathBuf::from(format!("in{i}")),
data: Arc::from(format!("text {i}").into_bytes()),
}
}
})
.collect();
let table = FileTable::new();
let results = table.load_all(&sources);
assert_eq!(results.len(), 50);
assert!(matches!(results[17], Err(Error::Io { .. })));
let mut expected = 0;
for (i, result) in results.iter().enumerate() {
if i == 17 {
continue;
}
let id = *result.as_ref().unwrap();
assert_eq!(id.index(), expected);
expected += 1;
assert_eq!(table.data(id), format!("text {i}").as_bytes());
}
assert_eq!(table.iter().count(), 49);
}
}