use std::{collections::BTreeMap, fmt};
use camino::{Utf8Path, Utf8PathBuf};
use ltk_file::LeagueFileKind;
use ltk_hash::WadHash;
use crate::RecoveredNames;
#[derive(Debug, Clone)]
pub struct ExtractProgress<'a> {
pub(super) done: usize,
pub(super) total: usize,
pub(super) path_hash: WadHash,
pub(super) path: &'a str,
pub(super) named: bool,
pub(super) output_path: Option<&'a Utf8Path>,
pub(super) result: ExtractResult,
pub(super) bytes: u64,
}
impl ExtractProgress<'_> {
pub fn done(&self) -> usize {
self.done
}
pub fn total(&self) -> usize {
self.total
}
pub fn fraction(&self) -> f64 {
if self.total == 0 {
0.0
} else {
self.done as f64 / self.total as f64
}
}
pub fn path_hash(&self) -> WadHash {
self.path_hash
}
pub fn path(&self) -> &str {
self.path
}
pub fn is_named(&self) -> bool {
self.named
}
pub fn result(&self) -> ExtractResult {
self.result
}
pub fn bytes(&self) -> u64 {
self.bytes
}
pub fn output_path(&self) -> Option<&Utf8Path> {
self.output_path
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExtractResult {
Extracted,
SkippedByType,
SkippedByPath,
SkippedExisting,
SkippedRejectedPath,
SkippedDuplicatePath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PathIssue {
Rejected,
Duplicate,
Renamed(Utf8PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DisplacedChunk {
pub path_hash: WadHash,
pub path: String,
pub issue: PathIssue,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ExtractReport {
pub extracted: usize,
pub skipped_existing: usize,
pub skipped_by_filter: usize,
pub missing: Vec<WadHash>,
pub bytes_written: u64,
pub by_kind: BTreeMap<LeagueFileKind, usize>,
pub cancelled: bool,
pub recovered: RecoveredNames,
pub displaced: Vec<DisplacedChunk>,
}
impl ExtractReport {
fn record(&mut self, outcome: ChunkOutcome) {
match outcome {
ChunkOutcome::Written { kind, bytes } => {
self.extracted += 1;
self.bytes_written += bytes;
*self.by_kind.entry(kind).or_insert(0) += 1;
}
ChunkOutcome::SkippedByType | ChunkOutcome::SkippedByPath => {
self.skipped_by_filter += 1
}
ChunkOutcome::SkippedExisting => self.skipped_existing += 1,
ChunkOutcome::SkippedRejectedPath | ChunkOutcome::SkippedDuplicatePath => {}
}
}
pub(super) fn record_chunk(
&mut self,
outcome: ChunkOutcome,
displaced: Option<DisplacedChunk>,
) {
self.record(outcome);
if let Some(displaced) = displaced {
self.displaced.push(displaced);
}
}
pub fn merge(&mut self, other: Self) {
let Self {
extracted,
skipped_existing,
skipped_by_filter,
missing,
bytes_written,
by_kind,
cancelled,
recovered,
displaced,
} = other;
self.extracted += extracted;
self.skipped_existing += skipped_existing;
self.skipped_by_filter += skipped_by_filter;
self.missing.extend(missing);
self.bytes_written += bytes_written;
for (kind, count) in by_kind {
*self.by_kind.entry(kind).or_default() += count;
}
self.cancelled |= cancelled;
self.recovered.merge(recovered);
self.displaced.extend(displaced);
}
pub fn rejected(&self) -> usize {
self.issues(|issue| matches!(issue, PathIssue::Rejected))
}
pub fn duplicates(&self) -> usize {
self.issues(|issue| matches!(issue, PathIssue::Duplicate))
}
pub fn renamed(&self) -> usize {
self.issues(|issue| matches!(issue, PathIssue::Renamed(_)))
}
fn issues(&self, issue: impl Fn(&PathIssue) -> bool) -> usize {
self.displaced
.iter()
.filter(|chunk| issue(&chunk.issue))
.count()
}
}
impl fmt::Display for ExtractReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} extracted, {} bytes",
self.extracted, self.bytes_written
)?;
if self.skipped_existing > 0 {
write!(f, ", {} existed", self.skipped_existing)?;
}
if self.skipped_by_filter > 0 {
write!(f, ", {} filtered out", self.skipped_by_filter)?;
}
let rejected = self.rejected();
if rejected > 0 {
write!(f, ", {rejected} rejected paths")?;
}
let duplicates = self.duplicates();
if duplicates > 0 {
write!(f, ", {duplicates} duplicate paths")?;
}
let renamed = self.renamed();
if renamed > 0 {
write!(f, ", {renamed} renamed")?;
}
if !self.missing.is_empty() {
write!(f, ", {} missing", self.missing.len())?;
}
if !self.recovered.is_empty() {
write!(f, ", {} names recovered", self.recovered.len())?;
}
if self.cancelled {
f.write_str(", cancelled")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum ChunkOutcome {
Written { kind: LeagueFileKind, bytes: u64 },
SkippedByType,
SkippedByPath,
SkippedExisting,
SkippedRejectedPath,
SkippedDuplicatePath,
}
impl ChunkOutcome {
pub(super) fn bytes(self) -> u64 {
match self {
Self::Written { bytes, .. } => bytes,
Self::SkippedByType
| Self::SkippedByPath
| Self::SkippedExisting
| Self::SkippedRejectedPath
| Self::SkippedDuplicatePath => 0,
}
}
}
pub(super) struct WriteOutcome {
pub(super) outcome: ChunkOutcome,
pub(super) path: Option<Utf8PathBuf>,
pub(super) issue: Option<PathIssue>,
}
impl WriteOutcome {
pub(super) fn skipped(outcome: ChunkOutcome, issue: Option<PathIssue>) -> Self {
Self {
outcome,
path: None,
issue,
}
}
}
impl From<ChunkOutcome> for ExtractResult {
fn from(outcome: ChunkOutcome) -> Self {
match outcome {
ChunkOutcome::Written { .. } => ExtractResult::Extracted,
ChunkOutcome::SkippedByType => ExtractResult::SkippedByType,
ChunkOutcome::SkippedByPath => ExtractResult::SkippedByPath,
ChunkOutcome::SkippedExisting => ExtractResult::SkippedExisting,
ChunkOutcome::SkippedRejectedPath => ExtractResult::SkippedRejectedPath,
ChunkOutcome::SkippedDuplicatePath => ExtractResult::SkippedDuplicatePath,
}
}
}