use crate::epc::{EpcStore, SaveOptions};
use crate::error::{Error, Result};
use crate::graph::Graph;
use crate::resource::ObjectSummary;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Broken {
pub from: String,
pub to: String,
pub path: String,
pub in_source: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Plan {
pub kept: Vec<String>,
pub unknown: Vec<String>,
pub left: usize,
pub clean: usize,
pub broken: Vec<Broken>,
}
impl Plan {
pub fn is_clean(&self) -> bool {
!self.broken.iter().any(|one| one.in_source)
}
}
pub fn plan(resources: &[ObjectSummary], graph: &Graph, wanted: &[String]) -> Plan {
let held: std::collections::HashSet<&str> = resources.iter().map(|one| one.uri.as_str()).collect();
let mut asked: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut unknown = Vec::new();
for uri in wanted {
if held.contains(uri.as_str()) {
asked.insert(uri.clone());
} else if !unknown.contains(uri) {
unknown.push(uri.clone());
}
}
let kept: Vec<String> = resources
.iter()
.filter(|one| asked.contains(&one.uri))
.map(|one| one.uri.clone())
.collect();
let present: std::collections::HashSet<&str> = graph
.nodes
.iter()
.filter(|node| node.present)
.map(|node| node.uri.as_str())
.collect();
let mut broken = Vec::new();
for edge in &graph.edges {
if !asked.contains(&edge.from) || asked.contains(&edge.to) {
continue;
}
broken.push(Broken {
from: edge.from.clone(),
to: edge.to.clone(),
path: edge.path.clone(),
in_source: present.contains(edge.to.as_str()),
});
}
let named_by_kept: std::collections::HashSet<&str> = graph
.edges
.iter()
.filter(|edge| asked.contains(&edge.from))
.map(|edge| edge.to.as_str())
.collect();
let left: Vec<&ObjectSummary> = resources.iter().filter(|one| !asked.contains(&one.uri)).collect();
let clean = left
.iter()
.filter(|one| !named_by_kept.contains(one.uri.as_str()))
.count();
Plan {
kept,
unknown,
left: left.len(),
clean,
broken,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Arrays {
Copy,
Leave,
}
#[derive(Debug, Clone)]
pub struct Extraction {
pub plan: Plan,
pub arrays: usize,
pub arrays_missed: Vec<String>,
pub unreadable: Vec<String>,
}
pub fn empty_package(created: Option<i64>) -> Result<Vec<u8>> {
use crate::epc::core_props::{
CORE_CONTENT_TYPE, CORE_PART, CORE_RELATIONSHIP, CoreProperties, EPC_VERSION,
};
use std::fmt::Write as _;
use std::io::Write;
let core = CoreProperties {
creator: "energyml-rust".to_string(),
created: match created {
Some(seconds) => crate::epc::core_props::w3cdtf(seconds),
None => crate::epc::core_props::now_w3cdtf(),
},
version: EPC_VERSION.to_string(),
..CoreProperties::default()
};
let content_types = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/{CORE_PART}" ContentType="{CORE_CONTENT_TYPE}"/>
</Types>"#
);
let mut rels = String::from(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">
",
);
let _ = writeln!(
rels,
" <Relationship Id=\"coreProperties\" Type=\"{CORE_RELATIONSHIP}\" Target=\"{CORE_PART}\"/>"
);
rels.push_str(
"</Relationships>
",
);
let mut bytes = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes));
let options =
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", options)
.map_err(|e| Error::Package(format!("writing the content types: {e}")))?;
zip.write_all(content_types.as_bytes())?;
zip.start_file("_rels/.rels", options)
.map_err(|e| Error::Package(format!("writing the package relationships: {e}")))?;
zip.write_all(rels.as_bytes())?;
zip.start_file(CORE_PART, options)
.map_err(|e| Error::Package(format!("writing the core properties: {e}")))?;
zip.write_all(core.to_xml().as_bytes())?;
zip.finish()
.map_err(|e| Error::Package(format!("closing the package: {e}")))?;
}
Ok(bytes)
}
pub fn extract(
source: &EpcStore,
wanted: &[String],
out: &std::path::Path,
arrays: Arrays,
options: &SaveOptions,
) -> Result<Extraction> {
let resources = source.resources();
let mut objects = Vec::new();
for summary in &resources {
if let Ok(object) = source.get_object(&summary.uri) {
objects.push((summary, object));
}
}
let graph = crate::graph::build(objects.iter().map(|(s, o)| (*s, o.as_reflect())));
let made = plan(&resources, &graph, wanted);
std::fs::write(out, empty_package(None)?)?;
let mut target = EpcStore::open(out, source.registry_arc())?;
let filled = fill(source, &mut target, &made.kept)?;
let mut copied = 0usize;
let mut missed = Vec::new();
if arrays == Arrays::Copy {
for location in &filled.arrays {
match source.read_array(location) {
Ok(data) => {
target.write_array_to(&location.file, &location.path_in_file, &data)?;
copied += 1;
}
Err(_) => missed.push(format!("{}:{}", location.file, location.path_in_file)),
}
}
}
target.save(options)?;
Ok(Extraction {
plan: made,
arrays: copied,
arrays_missed: missed,
unreadable: filled.unreadable,
})
}
struct Filled {
unreadable: Vec<String>,
arrays: Vec<crate::array::ArrayLocation>,
}
fn fill(source: &EpcStore, target: &mut EpcStore, kept: &[String]) -> Result<Filled> {
let mut unreadable = Vec::new();
let mut arrays = Vec::new();
let mut seen: std::collections::HashSet<(String, String)> = Default::default();
let mut files: std::collections::BTreeMap<String, String> = Default::default();
for uri in kept {
let object = match source.get_object(uri) {
Ok(object) => object,
Err(_) => {
unreadable.push(uri.clone());
continue;
}
};
for location in source.arrays_of(object.as_reflect()) {
if let Some(proxy) = location.proxy_uuid.as_ref().filter(|_| !location.file.is_empty()) {
files.insert(proxy.to_ascii_lowercase(), location.file.clone());
}
if seen.insert((location.file.clone(), location.path_in_file.clone())) {
arrays.push(location);
}
}
target.put(object)?;
}
for (proxy, file) in &files {
target.register_external_file(proxy, file.clone());
}
Ok(Filled { unreadable, arrays })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataFile {
pub from: String,
pub to: String,
pub arrays: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArrayCopy {
pub from: String,
pub path: String,
pub to: String,
}
#[derive(Debug, Clone, Default)]
pub struct Cut {
pub plan: Plan,
pub files: Vec<DataFile>,
pub arrays: Vec<ArrayCopy>,
pub unnamed: Vec<String>,
pub unreadable: Vec<String>,
pub fields: usize,
pub relationships: usize,
pub refusals: Vec<String>,
}
impl Cut {
pub fn is_allowed(&self) -> bool {
self.refusals.is_empty()
}
}
pub fn quote(
source: &EpcStore,
wanted: &[String],
names: &std::collections::BTreeMap<String, String>,
) -> Result<Cut> {
prepare(source, wanted, names, Some(0)).map(|(cut, _)| cut)
}
pub fn to_bytes(
source: &EpcStore,
wanted: &[String],
names: &std::collections::BTreeMap<String, String>,
created: Option<i64>,
) -> Result<(Cut, Vec<u8>)> {
let (cut, target) = prepare(source, wanted, names, created)?;
if !cut.is_allowed() {
return Err(Error::Package(format!(
"this cut was refused: {}",
cut.refusals.join("; ")
)));
}
let mut options = SaveOptions::default();
if let Some(seconds) = created {
options.created = Some(crate::epc::core_props::w3cdtf(seconds));
}
let mut out = std::io::Cursor::new(Vec::new());
target.write_to(&mut out, &options)?;
Ok((cut, out.into_inner()))
}
fn prepare(
source: &EpcStore,
wanted: &[String],
names: &std::collections::BTreeMap<String, String>,
created: Option<i64>,
) -> Result<(Cut, EpcStore)> {
let resources = source.resources();
let (held, _) = source.read_objects(None);
let graph = crate::graph::build(
held.iter()
.map(|(summary, object)| (summary, object.as_reflect())),
);
let made = plan(&resources, &graph, wanted);
let blank = empty_package(created)?;
let mut target = EpcStore::from_reader(
Box::new(std::io::Cursor::new(blank)),
"selection.epc",
source.registry_arc(),
)?;
let filled = fill(source, &mut target, &made.kept)?;
let mut cut = Cut {
unreadable: filled.unreadable,
..Cut::default()
};
for location in &filled.arrays {
if location.file.is_empty() {
cut.unnamed
.push(format!("{} ({})", location.path_in_file, location.field_path));
continue;
}
let bare = bare_name(&location.file);
match cut.files.iter_mut().find(|one| one.from == bare) {
Some(file) => file.arrays += 1,
None => cut.files.push(DataFile {
from: bare.to_string(),
to: names.get(bare).cloned().unwrap_or_else(|| bare.to_string()),
arrays: 1,
}),
}
}
if made.kept.is_empty() {
cut.refusals
.push("none of the chosen objects is in this package, so there is nothing to write".to_string());
}
let breaks = made.broken.iter().filter(|one| one.in_source).count();
if breaks > 0 {
cut.refusals.push(format!(
"this selection is not closed: {breaks} reference(s) would be broken - add what they name"
));
}
for asked in names.keys() {
if !cut.files.iter().any(|one| &one.from == asked) {
let named: Vec<&str> = cut.files.iter().map(|one| one.from.as_str()).collect();
cut.refusals.push(format!(
"`{asked}` is not a data file the kept objects name - they name {}",
if named.is_empty() {
"none".to_string()
} else {
named.join(", ")
}
));
}
}
for file in &cut.files {
target.external_files_mut().declare_present(file.from.clone());
}
for file in cut.files.clone() {
if file.to == file.from {
continue;
}
let asked = crate::rename::plan(&target, &file.from, &file.to);
if !asked.is_allowed() {
cut.refusals.extend(
asked
.refusals
.iter()
.map(|why| format!("renaming `{}` to `{}`: {why}", file.from, file.to)),
);
continue;
}
let done = crate::rename::apply(&mut target, &asked)?;
cut.fields += done.fields;
cut.relationships += done.relationships;
}
cut.arrays = filled
.arrays
.iter()
.filter(|location| !location.file.is_empty())
.map(|location| {
let bare = bare_name(&location.file);
let to = cut
.files
.iter()
.find(|one| one.from == bare)
.map(|one| one.to.clone())
.unwrap_or_else(|| bare.to_string());
ArrayCopy {
from: location.file.clone(),
path: location.path_in_file.clone(),
to,
}
})
.collect();
cut.plan = made;
Ok((cut, target))
}
fn bare_name(declared: &str) -> &str {
match declared.rfind(['/', '\\']) {
Some(at) => &declared[at + 1..],
None => declared,
}
}
#[derive(Debug, Clone, Default)]
pub struct Copied {
pub objects: Vec<String>,
pub added: Vec<String>,
pub missing: Vec<String>,
pub arrays: usize,
pub arrays_missed: Vec<String>,
pub unreadable: Vec<String>,
}
pub fn copy_into(from: &EpcStore, into: &mut EpcStore, wanted: &[String], arrays: Arrays) -> Result<Copied> {
let (held, unreadable) = from.read_objects(None);
let graph = crate::graph::build(
held.iter()
.map(|(summary, object)| (summary, object.as_reflect())),
);
let closed = crate::graph::closure(&graph, wanted);
let asked: std::collections::HashSet<&str> = wanted.iter().map(String::as_str).collect();
let mut done = Copied {
added: closed
.needed
.iter()
.filter(|uri| !asked.contains(uri.as_str()))
.cloned()
.collect(),
missing: closed.missing.clone(),
unreadable,
..Default::default()
};
let mut files: std::collections::BTreeMap<String, String> = Default::default();
for uri in &closed.needed {
let object = match from.get_object(uri) {
Ok(object) => object,
Err(_) => {
done.unreadable.push(uri.clone());
continue;
}
};
for location in from.arrays_of(object.as_reflect()) {
if let Some(proxy) = location.proxy_uuid.clone() {
files.insert(proxy.to_ascii_lowercase(), location.file.clone());
}
if arrays != Arrays::Copy {
continue;
}
match from.read_array(&location) {
Ok(data) => {
into.write_array_to(&location.file, &location.path_in_file, &data)?;
done.arrays += 1;
}
Err(_) => done
.arrays_missed
.push(format!("{}:{}", location.file, location.path_in_file)),
}
}
into.put(object)?;
done.objects.push(uri.clone());
}
for (proxy, file) in &files {
into.register_external_file(proxy, file.clone());
}
Ok(done)
}