use crate::ebook::archive::ResourceProvider;
use crate::ebook::element::{Attribute, Attributes, Properties};
use crate::ebook::errors::ArchiveResult;
use crate::ebook::resource::{self, ResourceContent};
use crate::epub::archive::EpubArchive;
use crate::epub::consts::opf;
use crate::epub::errors::EpubError;
use crate::epub::manifest::{
EpubManifest, EpubManifestContext, EpubManifestData, EpubManifestEntry, EpubManifestEntryData,
};
use crate::epub::metadata::{DetachedEpubMetaEntry, EpubMetadataData, EpubRefinementsMut};
use crate::epub::package::EpubPackageMetaContext;
use crate::epub::spine::EpubSpineData;
use crate::epub::toc::EpubTocData;
use crate::input::{IntoOption, Many};
use crate::util;
use crate::util::uri::{self, UriResolver};
use std::fmt::{Debug, Write};
impl<'ebook> EpubManifestContext<'ebook> {
fn attached(archive: &'ebook EpubArchive, package: EpubPackageMetaContext<'ebook>) -> Self {
Self::new(ResourceProvider::Archive(archive), package, None)
}
fn detached(content: Option<&'ebook ResourceContent>) -> Self {
Self::new(
match content {
Some(c) => ResourceProvider::Single(c),
None => ResourceProvider::Empty,
},
EpubPackageMetaContext::EMPTY,
None,
)
}
}
impl EpubManifestData {
pub(crate) fn generate_unique_id(&self, mut id: String) -> String {
let mut count = 1;
let original_len = id.len();
while self.entries.contains_key(&id) {
id.truncate(original_len);
write!(&mut id, "-{count}").ok();
count += 1;
}
id
}
pub(crate) fn generate_unique_href(&self, mut href: String) -> String {
let mut count = 1;
let mut count_len = 0;
let ext = href.rfind('.').unwrap_or(href.len());
while self.entries.iter().any(|(_, entry)| entry.href == href) {
let count_str = count.to_string();
href.replace_range(ext..ext + count_len, &count_str);
count_len = count_str.len();
count += 1;
}
href
}
pub(crate) fn remove_non_existent_references(&mut self) {
let entries = &mut self.entries;
for i in 0..entries.len() {
let data = &entries[i];
let invalid_fallback = data
.fallback
.as_deref()
.is_some_and(|idref| !entries.contains_key(idref));
let invalid_media_overlay = data
.media_overlay
.as_deref()
.is_some_and(|idref| !entries.contains_key(idref));
let data = &mut entries[i];
if invalid_fallback {
data.fallback = None;
}
if invalid_media_overlay {
data.media_overlay = None;
}
}
}
}
impl EpubManifestEntryData {
fn set_href_raw(&mut self, resolver: UriResolver<'_>, href_raw: String) -> (String, String) {
let href = std::mem::replace(&mut self.href, resolver.resolve(&href_raw));
let href_raw = std::mem::replace(&mut self.href_raw, href_raw);
(href, href_raw)
}
fn resolve_href(&mut self, resolver: UriResolver<'_>) {
self.href = resolver.resolve(&self.href_raw);
}
}
struct AttachedEntryContext<'ebook> {
index: usize,
href_resolver: UriResolver<'ebook>,
archive: &'ebook mut EpubArchive,
manifest: &'ebook mut EpubManifestData,
metadata: &'ebook mut EpubMetadataData,
spine: &'ebook mut EpubSpineData,
toc: &'ebook mut EpubTocData,
}
impl AttachedEntryContext<'_> {
fn update_entry_id(&mut self, options: IdOptions) -> Result<String, EpubError> {
let new_id = options.id;
if self.manifest.entries.contains_key(&new_id) {
return Err(EpubError::DuplicateItemId(new_id));
}
let (old_id, data) = self
.manifest
.entries
.shift_remove_index(self.index)
.expect(ManifestEntryDataHandle::ENTRY_EXPECTED);
if options.cascade {
self.cascade_new_id(&old_id, &new_id);
}
self.manifest.entries.shift_insert(self.index, new_id, data);
Ok(old_id)
}
fn update_entry_href(&mut self, options: HrefOptions) -> String {
let data = &mut self.manifest.entries[self.index];
let (old, old_raw) = data.set_href_raw(self.href_resolver, options.href);
let new_href = std::mem::take(&mut data.href);
if options.cascade {
self.cascade_new_href(&old, &new_href);
}
self.archive.relocate(old, &new_href);
self.manifest.entries[self.index].href = new_href;
old_raw
}
fn cascade_new_id(&mut self, old_id: &str, new_id: &str) {
fn update_idref(reference: Option<&mut String>, old_id: &str, new_id: &str) {
if let Some(reference) = reference
&& reference == old_id
{
new_id.clone_into(reference);
}
}
for (_, entry) in &mut self.manifest.entries {
update_idref(entry.fallback.as_mut(), old_id, new_id);
update_idref(entry.media_overlay.as_mut(), old_id, new_id);
}
for entry in &mut self.spine.entries {
update_idref(Some(&mut entry.idref), old_id, new_id);
}
if let Some(cover) = self.metadata.entries.get_mut(opf::COVER)
&& let Some(entry) = cover.first_mut()
&& entry.id.as_deref().is_some_and(|id| id == old_id)
{
new_id.clone_into(&mut entry.value);
}
}
fn cascade_new_href(&mut self, old_href: &str, new_href: &str) {
for (_, root) in &mut self.toc.entries {
root.cascade_toc_href(uri::path(old_href), new_href);
}
}
}
enum ManifestEntryDataHandle<'ebook> {
Attached(AttachedEntryContext<'ebook>),
Detached {
id: &'ebook mut String,
data: &'ebook mut EpubManifestEntryData,
content: &'ebook mut Option<ResourceContent>,
},
}
impl ManifestEntryDataHandle<'_> {
const ENTRY_EXPECTED: &'static str = "[rbook] Manifest entry ID missing from map. This indicates a bug in `try_set_id` or `insert` logic.";
fn get_mut(&mut self) -> &mut EpubManifestEntryData {
self.get_mut_with_id().1
}
fn get_with_id(&self) -> (&str, &EpubManifestEntryData) {
match self {
Self::Detached { id, data, .. } => (id, data),
Self::Attached(ctx) => {
let (key, data) = ctx
.manifest
.entries
.get_index(ctx.index)
.expect(Self::ENTRY_EXPECTED);
(key, data)
}
}
}
fn get_mut_with_id(&mut self) -> (&str, &mut EpubManifestEntryData) {
match self {
Self::Detached { id, data, .. } => (id, data),
Self::Attached(ctx) => {
let (key, data) = ctx
.manifest
.entries
.get_index_mut(ctx.index)
.expect(Self::ENTRY_EXPECTED);
(key, data)
}
}
}
}
impl Debug for ManifestEntryDataHandle<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("ManifestEntryDataHandle");
match self {
ManifestEntryDataHandle::Attached(ctx) => debug
.field("href_resolver", &ctx.href_resolver)
.field("index", &ctx.index),
ManifestEntryDataHandle::Detached { id, data, content } => debug
.field("id", id)
.field("data", data)
.field("content", content),
}
.finish_non_exhaustive()
}
}
impl EpubManifestEntry<'_> {
pub fn to_detached(&self) -> DetachedEpubManifestEntry {
DetachedEpubManifestEntry {
id: self.id.to_owned(),
data: self.data.clone(),
content: None,
}
}
pub fn to_detached_with_content(&self) -> ArchiveResult<DetachedEpubManifestEntry> {
self.read_bytes()
.map(|content| self.to_detached().content(content))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DetachedEpubManifestEntry {
id: String,
data: EpubManifestEntryData,
content: Option<ResourceContent>,
}
impl DetachedEpubManifestEntry {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
data: EpubManifestEntryData::default(),
content: None,
}
}
pub fn as_mut(&mut self) -> EpubManifestEntryMut<'_> {
EpubManifestEntryMut::new(
EpubPackageMetaContext::EMPTY,
ManifestEntryDataHandle::Detached {
id: &mut self.id,
data: &mut self.data,
content: &mut self.content,
},
)
}
pub fn as_view(&self) -> EpubManifestEntry<'_> {
EpubManifestContext::detached(self.content.as_ref()).create_entry(&self.id, &self.data)
}
pub fn content_ref(&self) -> Option<&ResourceContent> {
self.content.as_ref()
}
pub fn content_mut(&mut self) -> Option<&mut ResourceContent> {
self.content.as_mut()
}
pub fn content(mut self, content: impl Into<ResourceContent>) -> Self {
self.as_mut().set_content(content);
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.as_mut().set_id(id);
self
}
pub fn href(mut self, raw_href: impl Into<String>) -> Self {
self.as_mut().set_href(raw_href);
self
}
pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
self.as_mut().set_media_type(media_type);
self
}
pub fn fallback(mut self, idref: impl IntoOption<String>) -> Self {
self.as_mut().set_fallback(idref);
self
}
pub fn media_overlay(mut self, idref: impl IntoOption<String>) -> Self {
self.as_mut().set_media_overlay(idref);
self
}
pub fn property(mut self, property: &str) -> Self {
self.as_mut().properties_mut().insert(property);
self
}
pub fn attribute(mut self, attribute: impl Many<Attribute>) -> Self {
self.as_mut().attributes_mut().extend(attribute.iter_many());
self
}
pub fn refinement(mut self, detached: impl Many<DetachedEpubMetaEntry>) -> Self {
self.as_mut().refinements_mut().push(detached);
self
}
}
impl<H: Into<String>, C: Into<ResourceContent>> From<(H, C)> for DetachedEpubManifestEntry {
fn from((href, content): (H, C)) -> Self {
let href = href.into();
let mut id = util::str::slugify(&href);
if id.chars().next().is_some_and(char::is_numeric) {
id.insert(0, '_');
}
Self::new(id).href(href).content(content)
}
}
pub struct EpubManifestMut<'ebook> {
href_resolver: UriResolver<'ebook>,
meta_ctx: EpubPackageMetaContext<'ebook>,
archive: &'ebook mut EpubArchive,
manifest: &'ebook mut EpubManifestData,
metadata: &'ebook mut EpubMetadataData,
spine: &'ebook mut EpubSpineData,
toc: &'ebook mut EpubTocData,
}
impl<'ebook> EpubManifestMut<'ebook> {
pub(in crate::epub) fn new(
href_resolver: UriResolver<'ebook>,
meta_ctx: EpubPackageMetaContext<'ebook>,
archive: &'ebook mut EpubArchive,
manifest: &'ebook mut EpubManifestData,
metadata: &'ebook mut EpubMetadataData,
spine: &'ebook mut EpubSpineData,
toc: &'ebook mut EpubTocData,
) -> Self {
Self {
href_resolver,
meta_ctx,
archive,
manifest,
metadata,
spine,
toc,
}
}
fn get(&mut self, index: usize) -> EpubManifestEntryMut<'_> {
EpubManifestEntryMut::new(
self.meta_ctx,
ManifestEntryDataHandle::Attached(AttachedEntryContext {
href_resolver: self.href_resolver,
archive: self.archive,
manifest: self.manifest,
metadata: self.metadata,
spine: self.spine,
toc: self.toc,
index,
}),
)
}
fn insert_detached(&mut self, mut entry: DetachedEpubManifestEntry) {
entry.data.resolve_href(self.href_resolver);
if let Some(binary) = entry.content {
self.archive.insert(entry.data.href.clone(), binary);
}
if entry.data.media_type.is_empty() {
entry.data.media_type = resource::write::infer_media_type(&entry.data.href);
}
let (i, replaced) = self.manifest.entries.insert_full(entry.id, entry.data);
if let Some(old) = replaced {
let new = &self.manifest.entries[i];
if old.href != new.href {
self.archive.remove(&old.href);
}
}
}
pub fn push(&mut self, detached: impl Many<DetachedEpubManifestEntry>) {
for entry in detached.iter_many() {
self.insert_detached(entry);
}
}
pub fn by_id_mut(&mut self, id: &str) -> Option<EpubManifestEntryMut<'_>> {
self.manifest
.entries
.get_index_of(id)
.map(|index| self.get(index))
}
pub fn cover_image_mut(&mut self) -> Option<EpubManifestEntryMut<'_>> {
for (i, data) in self.manifest.entries.values().enumerate() {
if data.properties.has_property(opf::COVER_IMAGE) {
return Some(self.get(i));
}
}
if let Some(cover_id) = self.metadata.epub2_cover_image_id() {
return self
.manifest
.entries
.get_index_of(cover_id)
.map(|i| self.get(i));
}
None
}
pub fn for_each_mut(&mut self, mut f: impl FnMut(&mut EpubManifestEntryMut<'_>)) {
let mut entries = self.iter_mut();
while let Some(mut entry) = entries.next() {
f(&mut entry);
}
}
pub fn iter_mut(&mut self) -> EpubManifestMutIter<'_> {
EpubManifestMutIter {
href_resolver: self.href_resolver,
meta_ctx: self.meta_ctx,
archive: self.archive,
metadata: self.metadata,
manifest: self.manifest,
spine: self.spine,
toc: self.toc,
index: 0,
}
}
#[allow(clippy::should_implement_trait)]
pub fn into_iter(self) -> EpubManifestMutIter<'ebook> {
EpubManifestMutIter {
href_resolver: self.href_resolver,
meta_ctx: self.meta_ctx,
archive: self.archive,
manifest: self.manifest,
metadata: self.metadata,
spine: self.spine,
toc: self.toc,
index: 0,
}
}
pub fn remove_by_id(&mut self, id: &str) -> Option<DetachedEpubManifestEntry> {
self.manifest
.entries
.shift_remove_entry(id)
.map(|(id, data)| DetachedEpubManifestEntry {
content: self.archive.remove(&data.href),
id,
data,
})
}
pub fn retain(&mut self, mut f: impl FnMut(EpubManifestEntry<'_>) -> bool) {
self.manifest.entries.retain(|id, entry| {
let ctx = EpubManifestContext::attached(self.archive, self.meta_ctx);
let retain = f(ctx.create_entry(id, entry));
if !retain {
self.archive.remove(&entry.href);
}
retain
});
}
pub fn extract_if(
&mut self,
mut f: impl FnMut(EpubManifestEntry<'_>) -> bool,
) -> impl Iterator<Item = DetachedEpubManifestEntry> {
self.manifest
.entries
.extract_if(.., move |id, entry| {
f(EpubManifestContext::EMPTY.create_entry(id, entry))
})
.map(|(id, data)| DetachedEpubManifestEntry {
content: self.archive.remove(&data.href),
id,
data,
})
}
pub fn drain(&mut self) -> impl Iterator<Item = DetachedEpubManifestEntry> {
self.manifest
.entries
.drain(..)
.map(|(id, data)| DetachedEpubManifestEntry {
content: self.archive.remove(&data.href),
id,
data,
})
}
pub fn clear(&mut self) {
for (_, removed) in self.manifest.entries.drain(..) {
self.archive.remove(&removed.href);
}
}
pub fn as_view(&self) -> EpubManifest<'_> {
EpubManifest::new(
ResourceProvider::Archive(self.archive),
self.meta_ctx,
self.manifest,
self.metadata,
)
}
}
impl Debug for EpubManifestMut<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("EpubManifestMut")
.field("href_resolver", &self.href_resolver)
.field("manifest", &self.manifest)
.finish_non_exhaustive()
}
}
impl Extend<DetachedEpubManifestEntry> for EpubManifestMut<'_> {
fn extend<T: IntoIterator<Item = DetachedEpubManifestEntry>>(&mut self, iter: T) {
for entry in iter {
self.insert_detached(entry);
}
}
}
pub struct EpubManifestMutIter<'ebook> {
href_resolver: UriResolver<'ebook>,
meta_ctx: EpubPackageMetaContext<'ebook>,
archive: &'ebook mut EpubArchive,
manifest: &'ebook mut EpubManifestData,
metadata: &'ebook mut EpubMetadataData,
spine: &'ebook mut EpubSpineData,
toc: &'ebook mut EpubTocData,
index: usize,
}
impl EpubManifestMutIter<'_> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<EpubManifestEntryMut<'_>> {
let entries = &mut self.manifest.entries;
if self.index < entries.len() {
let index = self.index;
self.index += 1;
Some(EpubManifestEntryMut::new(
self.meta_ctx,
ManifestEntryDataHandle::Attached(AttachedEntryContext {
href_resolver: self.href_resolver,
archive: self.archive,
manifest: self.manifest,
metadata: self.metadata,
spine: self.spine,
toc: self.toc,
index,
}),
))
} else {
None
}
}
}
impl Debug for EpubManifestMutIter<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("EpubManifestMutIter")
.field("href_resolver", &self.href_resolver)
.field("index", &self.index)
.finish_non_exhaustive()
}
}
pub struct EpubManifestEntryMut<'ebook> {
meta_ctx: EpubPackageMetaContext<'ebook>,
data: ManifestEntryDataHandle<'ebook>,
}
impl<'ebook> EpubManifestEntryMut<'ebook> {
fn new(
meta_ctx: EpubPackageMetaContext<'ebook>,
data: ManifestEntryDataHandle<'ebook>,
) -> Self {
Self { meta_ctx, data }
}
pub fn set_content(&mut self, content: impl Into<ResourceContent>) -> Option<ResourceContent> {
match &mut self.data {
ManifestEntryDataHandle::Attached(ctx) => {
let data = &mut ctx.manifest.entries[ctx.index];
ctx.archive.insert(data.href.clone(), content.into())
}
ManifestEntryDataHandle::Detached {
content: current, ..
} => current.replace(content.into()),
}
}
pub fn set_id(&mut self, id: impl Into<IdOptions>) -> String {
let mut id_options = id.into();
if let ManifestEntryDataHandle::Attached(ctx) = &self.data {
id_options.id = ctx.manifest.generate_unique_id(id_options.id);
}
self.try_set_id(id_options.id)
.expect("The given id should be unique")
}
pub fn try_set_id(&mut self, id: impl Into<IdOptions>) -> Result<String, EpubError> {
let options = id.into();
match &mut self.data {
ManifestEntryDataHandle::Attached(ctx) => ctx.update_entry_id(options),
ManifestEntryDataHandle::Detached { id, .. } => Ok(std::mem::replace(id, options.id)),
}
}
pub fn set_href(&mut self, raw_href: impl Into<HrefOptions>) -> String {
let options = raw_href.into();
match &mut self.data {
ManifestEntryDataHandle::Attached(ctx) => ctx.update_entry_href(options),
ManifestEntryDataHandle::Detached { data, .. } => {
data.href = String::new();
std::mem::replace(&mut data.href_raw, options.href)
}
}
}
pub fn set_media_type(&mut self, media_type: impl Into<String>) -> String {
std::mem::replace(&mut self.data.get_mut().media_type, media_type.into())
}
pub fn set_fallback(&mut self, idref: impl IntoOption<String>) -> Option<String> {
std::mem::replace(&mut self.data.get_mut().fallback, idref.into_option())
}
pub fn set_media_overlay(&mut self, idref: impl IntoOption<String>) -> Option<String> {
std::mem::replace(&mut self.data.get_mut().media_overlay, idref.into_option())
}
pub fn properties_mut(&mut self) -> &mut Properties {
&mut self.data.get_mut().properties
}
pub fn attributes_mut(&mut self) -> &mut Attributes {
&mut self.data.get_mut().attributes
}
pub fn refinements_mut(&mut self) -> EpubRefinementsMut<'_> {
let (id, data) = self.data.get_mut_with_id();
EpubRefinementsMut::new(self.meta_ctx, Some(id), &mut data.refinements)
}
pub fn as_view(&self) -> EpubManifestEntry<'_> {
let (id, data) = self.data.get_with_id();
match &self.data {
ManifestEntryDataHandle::Attached(ctx) => EpubManifestContext::new(
ResourceProvider::Archive(ctx.archive),
self.meta_ctx,
Some(ctx.manifest),
)
.create_entry(id, data),
ManifestEntryDataHandle::Detached { content, .. } => {
EpubManifestContext::detached(content.as_ref()).create_entry(id, data)
}
}
}
}
impl Debug for EpubManifestEntryMut<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("EpubManifestEntryMut")
.field("data", &self.data)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct IdOptions {
id: String,
cascade: bool,
}
impl IdOptions {
pub fn new(id: String) -> Self {
Self { cascade: true, id }
}
pub fn cascade(mut self, cascade: bool) -> Self {
self.cascade = cascade;
self
}
}
impl<I: Into<String>> From<I> for IdOptions {
fn from(id: I) -> Self {
Self::new(id.into())
}
}
#[derive(Clone, Debug)]
pub struct HrefOptions {
href: String,
cascade: bool,
}
impl HrefOptions {
pub fn new(href: String) -> Self {
Self {
cascade: true,
href,
}
}
pub fn cascade(mut self, cascade: bool) -> Self {
self.cascade = cascade;
self
}
}
impl<I: Into<String>> From<I> for HrefOptions {
fn from(href: I) -> Self {
Self::new(href.into())
}
}