Skip to main content

lib_epub/
builder.rs

1//! EPUB build functionality
2//!
3//! This module provides functionality for creating and building EPUB eBook files.
4//! The `EpubBuilder` structure implements the build logic of the EPUB 3.0 specification,
5//! allowing users to create standard-compliant EPUB files from scratch.
6//!
7//! ## Usage
8//!
9//! ```rust, no_run
10//! # #[cfg(feature = "builder")] {
11//! # fn main() -> Result<(), lib_epub::error::EpubError> {
12//! use lib_epub::{
13//!     builder::{EpubBuilder, EpubVersion3},
14//!     types::{MetadataItem, ManifestItem, SpineItem},
15//! };
16//!
17//! let mut builder = EpubBuilder::<EpubVersion3>::new()?;
18//! builder
19//!     .add_rootfile("OEBPS/content.opf")?
20//!     .add_metadata(MetadataItem::new("title", "Test Book"))
21//!     .add_manifest(
22//!         "path/to/content",
23//!         ManifestItem::new("content_id", "target/path")?,
24//!     )?
25//!     .add_spine(SpineItem::new("content.xhtml"));
26//!
27//! builder.build("output.epub")?;
28//! # Ok(())
29//! # }
30//! # }
31//! ```
32//!
33//! ## Notes
34//!
35//! - Requires `builder` functionality to use this module.
36
37#[cfg(feature = "no-indexmap")]
38use std::collections::HashMap;
39use std::{
40    cmp::Reverse,
41    env,
42    fs::{self, File},
43    io::{BufReader, Cursor, Read, Seek, Write},
44    marker::PhantomData,
45    path::{Path, PathBuf},
46};
47
48use chrono::{SecondsFormat, Utc};
49#[cfg(not(feature = "no-indexmap"))]
50use indexmap::IndexMap;
51use infer::Infer;
52use log::warn;
53use quick_xml::{
54    Writer,
55    events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
56};
57use walkdir::WalkDir;
58use zip::{CompressionMethod, ZipWriter, write::FileOptions};
59
60#[cfg(feature = "content-builder")]
61use crate::builder::content::ContentBuilder;
62use crate::{
63    epub::EpubDoc,
64    error::{EpubBuilderError, EpubError},
65    types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
66    utils::{
67        ELEMENT_IN_DC_NAMESPACE, check_realtive_link_leakage, local_time, remove_leading_slash,
68    },
69};
70
71#[cfg(feature = "content-builder")]
72pub mod content;
73
74type XmlWriter = Writer<Cursor<Vec<u8>>>;
75
76// struct EpubVersion2;
77#[cfg_attr(test, derive(Debug))]
78pub struct EpubVersion3;
79
80/// EPUB Builder
81///
82/// The main structure used to create and build EPUB ebook files.
83/// Supports the EPUB 3.0 specification and can build a complete EPUB file structure.
84///
85/// ## Usage
86///
87/// ```rust, no_run
88/// # #[cfg(feature = "builder")]
89/// # fn main() -> Result<(), lib_epub::error::EpubError> {
90/// use lib_epub::{
91///     builder::{EpubBuilder, EpubVersion3},
92///     types::{MetadataItem, ManifestItem, NavPoint, SpineItem},
93/// };
94///
95/// let mut builder = EpubBuilder::<EpubVersion3>::new()?;
96///
97/// builder
98///     .add_rootfile("EPUB/content.opf")?
99///     .add_metadata(MetadataItem::new("title", "Test Book"))
100///     .add_metadata(MetadataItem::new("language", "en"))
101///     .add_metadata(
102///         MetadataItem::new("identifier", "unique-id")
103///             .with_id("pub-id")
104///             .build(),
105///     )
106///     .add_manifest(
107///         "./test_case/Overview.xhtml",
108///         ManifestItem::new("content", "target/path")?,
109///     )?
110///     .add_spine(SpineItem::new("content"))
111///     .add_catalog_item(NavPoint::new("label"));
112///
113/// builder.build("output.epub")?;
114///
115/// # Ok(())
116/// # }
117/// ```
118///
119/// ## Notes
120///
121/// - All resource files **must** exist on the local file system.
122/// - **At least one rootfile** must be added before adding manifest items.
123/// - Requires at least one `title`, `language`, and `identifier` with id `pub-id`.
124#[cfg_attr(test, derive(Debug))]
125pub struct EpubBuilder<Version> {
126    /// EPUB version placeholder
127    epub_version: PhantomData<Version>,
128
129    /// Temporary directory path for storing files during the build process
130    temp_dir: PathBuf,
131
132    /// List of root file paths
133    rootfiles: Vec<String>,
134
135    /// List of metadata items
136    metadata: Vec<MetadataItem>,
137
138    /// Manifest item mapping table, with ID as the key and manifest item as the value
139    #[cfg(feature = "no-indexmap")]
140    manifest: HashMap<String, ManifestItem>,
141    #[cfg(not(feature = "no-indexmap"))]
142    manifest: IndexMap<String, ManifestItem>,
143
144    /// List of spine items, defining the reading order
145    spine: Vec<SpineItem>,
146
147    catalog_title: String,
148
149    /// List of catalog navigation points
150    catalog: Vec<NavPoint>,
151
152    /// List of content builder
153    #[cfg(feature = "content-builder")]
154    content: Vec<(PathBuf, ContentBuilder)>,
155}
156
157impl EpubBuilder<EpubVersion3> {
158    /// Create a new `EpubBuilder` instance
159    ///
160    /// ## Return
161    /// - `Ok(EpubBuilder)`: Builder instance created successfully
162    /// - `Err(EpubError)`: Error occurred during builder initialization
163    pub fn new() -> Result<Self, EpubError> {
164        let temp_dir = env::temp_dir().join(local_time());
165        fs::create_dir(&temp_dir)?;
166        fs::create_dir(temp_dir.join("META-INF"))?;
167
168        let mime_file = temp_dir.join("mimetype");
169        fs::write(mime_file, "application/epub+zip")?;
170
171        Ok(EpubBuilder {
172            epub_version: PhantomData,
173            temp_dir,
174
175            rootfiles: vec![],
176            metadata: vec![],
177            #[cfg(feature = "no-indexmap")]
178            manifest: HashMap::new(),
179            #[cfg(not(feature = "no-indexmap"))]
180            manifest: IndexMap::new(),
181            spine: vec![],
182
183            catalog_title: String::new(),
184            catalog: vec![],
185
186            #[cfg(feature = "content-builder")]
187            content: vec![],
188        })
189    }
190
191    /// Add a rootfile path
192    ///
193    /// The added path points to an OPF file that does not yet exist
194    /// and will be created when building the Epub file.
195    ///
196    /// ## Parameters
197    /// - `rootfile`: Rootfile path
198    ///
199    /// ## Notes
200    /// - The added rootfile path must be a relative path and cannot start with "../".
201    /// - At least one rootfile must be added before adding metadata items.
202    pub fn add_rootfile(&mut self, rootfile: &str) -> Result<&mut Self, EpubError> {
203        let rootfile = if rootfile.starts_with("/") || rootfile.starts_with("../") {
204            return Err(EpubBuilderError::IllegalRootfilePath.into());
205        } else if let Some(rootfile) = rootfile.strip_prefix("./") {
206            rootfile
207        } else {
208            rootfile
209        };
210
211        self.rootfiles.push(rootfile.to_string());
212
213        Ok(self)
214    }
215
216    /// Remove the last added rootfile from the builder
217    pub fn remove_last_rootfile(&mut self) -> &mut Self {
218        self.rootfiles.pop();
219        self
220    }
221
222    /// Remove and return the last added rootfile
223    ///
224    /// ## Return
225    /// - `Some(String)`: The last added rootfile
226    /// - `None`: If no rootfile exists
227    pub fn take_last_rootfile(&mut self) -> Option<String> {
228        self.rootfiles.pop()
229    }
230
231    /// Clear all configured rootfile entries from the builder
232    pub fn clear_rootfiles(&mut self) -> &mut Self {
233        self.rootfiles.clear();
234        self
235    }
236
237    /// Add metadata item
238    ///
239    /// Required metadata includes title, language, and an identifier with 'pub-id'.
240    /// Missing this data will result in an error when building the epub file.
241    ///
242    /// ## Parameters
243    /// - `item`: Metadata items to add
244    pub fn add_metadata(&mut self, item: MetadataItem) -> &mut Self {
245        self.metadata.push(item);
246        self
247    }
248
249    /// Remove the last metadata item
250    pub fn remove_last_metadata(&mut self) -> &mut Self {
251        self.metadata.pop();
252        self
253    }
254
255    /// Remove and return the last metadata item
256    ///
257    /// ## Return
258    /// - `Some(MetadataItem)`: The last metadata item
259    /// - `None`: If no metadata exists
260    pub fn take_last_metadata(&mut self) -> Option<MetadataItem> {
261        self.metadata.pop()
262    }
263
264    /// Clear all metadata entries
265    pub fn clear_metadatas(&mut self) -> &mut Self {
266        self.metadata.clear();
267        self
268    }
269
270    /// Add manifest item and corresponding resource file
271    ///
272    /// The builder will automatically recognize the file type of
273    /// the added resource and update it in `ManifestItem`.
274    ///
275    /// ## Parameters
276    /// - `manifest_source` - Local resource file path
277    /// - `manifest_item` - Manifest item information
278    ///
279    /// ## Return
280    /// - `Ok(&mut Self)` - Successful addition, returns a reference to itself
281    /// - `Err(EpubError)` - Error occurred during the addition process
282    ///
283    /// ## Notes
284    /// - At least one rootfile must be added before adding manifest items.
285    /// - If the manifest item ID already exists in the manifest, the manifest item will be overwritten.
286    pub fn add_manifest(
287        &mut self,
288        manifest_source: &str,
289        manifest_item: ManifestItem,
290    ) -> Result<&mut Self, EpubError> {
291        if self.rootfiles.is_empty() {
292            return Err(EpubBuilderError::MissingRootfile.into());
293        }
294
295        // Check if the source path is a file
296        let source = PathBuf::from(manifest_source);
297        if !source.is_file() {
298            return Err(EpubBuilderError::TargetIsNotFile {
299                target_path: manifest_source.to_string(),
300            }
301            .into());
302        }
303
304        // Get the file extension
305        let extension = match source.extension() {
306            Some(ext) => ext.to_string_lossy().to_lowercase(),
307            None => String::new(),
308        };
309
310        // Read the file
311        let buf = fs::read(source)?;
312
313        // Get the mime type
314        let real_mime = match Infer::new().get(&buf) {
315            Some(infer_mime) => refine_mime_type(infer_mime.mime_type(), &extension),
316            None => {
317                return Err(EpubBuilderError::UnknownFileFormat {
318                    file_path: manifest_source.to_string(),
319                }
320                .into());
321            }
322        };
323
324        let target_path = self.normalize_manifest_path(&manifest_item.path, &manifest_item.id)?;
325        if let Some(parent_dir) = target_path.parent() {
326            if !parent_dir.exists() {
327                fs::create_dir_all(parent_dir)?
328            }
329        }
330
331        match fs::write(target_path, buf) {
332            Ok(_) => {
333                self.manifest
334                    .insert(manifest_item.id.clone(), manifest_item.set_mime(real_mime));
335                Ok(self)
336            }
337            Err(err) => Err(err.into()),
338        }
339    }
340
341    /// Remove manifest item and corresponding resource file
342    ///
343    /// This function removes the manifest item from the manifest list and also deletes
344    /// the corresponding resource file from the temporary directory.
345    ///
346    /// ## Parameters
347    /// - `id`: The unique identifier of the manifest item to remove
348    ///
349    /// ## Return
350    /// - `Ok(&mut Self)` Successfully removed the manifest item
351    /// - `Err(EpubError)` Error occurred during the removal process
352    pub fn remove_manifest(&mut self, id: &str) -> Result<&mut Self, EpubError> {
353        #[cfg(feature = "no-indexmap")]
354        let manifest = self.manifest.remove(id);
355        #[cfg(not(feature = "no-indexmap"))]
356        let manifest = self.manifest.shift_remove(id);
357
358        if let Some(manifest) = manifest {
359            let target_path = self.normalize_manifest_path(&manifest.path, &manifest.id)?;
360            fs::remove_file(target_path)?;
361        }
362
363        Ok(self)
364    }
365
366    /// Remove and return the specified manifest item
367    ///
368    /// ## Parameters
369    /// - `id`: The unique identifier of the manifest item to remove
370    ///
371    /// ## Return
372    /// - `Some(ManifestItem)`: The removed manifest item
373    /// - `None`: If the manifest item does not exist or error occurs during the removal process
374    pub fn take_manifest(&mut self, id: &str) -> Option<ManifestItem> {
375        #[cfg(feature = "no-indexmap")]
376        let manifest = self.manifest.remove(id);
377        #[cfg(not(feature = "no-indexmap"))]
378        let manifest = self.manifest.shift_remove(id);
379
380        if let Some(manifest) = manifest {
381            let target_path = self
382                .normalize_manifest_path(&manifest.path, &manifest.id)
383                .ok()?;
384            fs::remove_file(target_path).ok()?;
385
386            return Some(manifest);
387        }
388
389        None
390    }
391
392    /// Clear all manifest items and their corresponding resource files
393    ///
394    /// ## Return
395    /// - `Ok(&mut Self)` - Successfully cleared all manifest items, returns a reference to itself
396    /// - `Err(EpubError)` - Error occurred during the clearing process
397    pub fn clear_manifests(&mut self) -> Result<&mut Self, EpubError> {
398        let paths = self
399            .manifest
400            .values()
401            .map(|manifest| &manifest.path)
402            .collect::<Vec<&PathBuf>>();
403
404        for path in paths {
405            let _ = fs::remove_file(path);
406        }
407
408        self.manifest.clear();
409
410        Ok(self)
411    }
412
413    /// Add spine item
414    ///
415    /// The spine item defines the reading order of the book.
416    ///
417    /// ## Parameters
418    /// - `item`: Spine item to add
419    pub fn add_spine(&mut self, item: SpineItem) -> &mut Self {
420        self.spine.push(item);
421        self
422    }
423
424    /// Remove the last spine item from the builder
425    pub fn remove_last_spine(&mut self) -> &mut Self {
426        self.spine.pop();
427        self
428    }
429
430    /// Remove and return the last spine item from the builder
431    ///
432    /// ## Return
433    /// - `Some(SpineItem)`: The last spine item if it existed
434    /// - `None`: If no spine items exist in the list
435    pub fn take_last_spine(&mut self) -> Option<SpineItem> {
436        self.spine.pop()
437    }
438
439    /// Clear all spine items from the builder
440    pub fn clear_spines(&mut self) -> &mut Self {
441        self.spine.clear();
442        self
443    }
444
445    /// Set catalog title
446    ///
447    /// ## Parameters
448    /// - `title`: Catalog title
449    pub fn set_catalog_title(&mut self, title: &str) -> &mut Self {
450        self.catalog_title = title.to_string();
451        self
452    }
453
454    /// Add catalog item
455    ///
456    /// Added directory items will be added to the end of the existing list.
457    ///
458    /// ## Parameters
459    /// - `item`: Catalog item to add
460    pub fn add_catalog_item(&mut self, item: NavPoint) -> &mut Self {
461        self.catalog.push(item);
462        self
463    }
464
465    /// Remove the last catalog item
466    pub fn remove_last_catalog_item(&mut self) -> &mut Self {
467        self.catalog.pop();
468        self
469    }
470
471    /// Remove and return the last catalog item
472    ///
473    /// ## Return
474    /// - `Some(NavPoint)`: The last catalog item if it existed
475    /// - `None`: If no catalog items exist in the list
476    pub fn take_last_catalog_item(&mut self) -> Option<NavPoint> {
477        self.catalog.pop()
478    }
479
480    /// Re-/ Set catalog
481    ///
482    /// The passed list will overwrite existing data.
483    ///
484    /// ## Parameters
485    /// - `catalog`: Catalog to set
486    pub fn set_catalog(&mut self, catalog: Vec<NavPoint>) -> &mut Self {
487        self.catalog = catalog;
488        self
489    }
490
491    /// Clear all catalog items
492    pub fn clear_catalog(&mut self) -> &mut Self {
493        self.catalog.clear();
494        self
495    }
496
497    /// Add content
498    ///
499    /// The content builder can be used to generate content for the book.
500    /// It is recommended to use the `content-builder` feature to use this function.
501    ///
502    /// ## Parameters
503    /// - `target_path`: The path to the resource file within the EPUB container
504    /// - `content`: The content builder to generate content
505    #[cfg(feature = "content-builder")]
506    pub fn add_content(&mut self, target_path: &str, content: ContentBuilder) -> &mut Self {
507        self.content.push((PathBuf::from(target_path), content));
508        self
509    }
510
511    /// Remove the last content builder
512    #[cfg(feature = "content-builder")]
513    pub fn remove_last_content(&mut self) -> &mut Self {
514        self.content.pop();
515        self
516    }
517
518    /// Remove and return the last content builder
519    ///
520    /// ## Return
521    /// - `Some((PathBuf, ContentBuilder))`: The last content builder if it existed
522    /// - `None`: If no content builders exist in the list
523    #[cfg(feature = "content-builder")]
524    pub fn take_last_content(&mut self) -> Option<(PathBuf, ContentBuilder)> {
525        self.content.pop()
526    }
527
528    /// Clear all content builders
529    #[cfg(feature = "content-builder")]
530    pub fn clear_contents(&mut self) -> &mut Self {
531        self.content.clear();
532        self
533    }
534
535    /// Clear all data from the builder
536    ///
537    /// This function clears all metadata, manifest items, spine items, catalog items, etc.
538    /// from the builder, effectively resetting it to an empty state.
539    ///
540    /// ## Return
541    /// - `Ok(&mut Self)`: Successfully cleared all data
542    /// - `Err(EpubError)`: Error occurred during the clearing process (specifically during manifest clearing)
543    pub fn clear_all(&mut self) -> Result<&mut Self, EpubError> {
544        self.catalog_title = String::new();
545        self.clear_metadatas()
546            .clear_manifests()?
547            .clear_spines()
548            .clear_catalog();
549
550        #[cfg(feature = "content-builder")]
551        self.clear_contents();
552
553        Ok(self)
554    }
555
556    /// Builds an EPUB file and saves it to the specified path
557    ///
558    /// ## Parameters
559    /// - `output_path`: Output file path
560    ///
561    /// ## Return
562    /// - `Ok(())`: Build successful
563    /// - `Err(EpubError)`: Error occurred during the build process
564    pub fn make<P: AsRef<Path>>(mut self, output_path: P) -> Result<(), EpubError> {
565        // Create the container.xml, navigation document, and OPF files in sequence.
566        // The associated metadata will initialized when navigation document is created;
567        // therefore, the navigation document must be created before the opf file is created.
568        self.make_container_xml()?;
569        self.make_navigation_document()?;
570        #[cfg(feature = "content-builder")]
571        self.make_contents()?;
572        self.make_opf_file()?;
573        self.remove_empty_dirs()?;
574
575        if let Some(parent) = output_path.as_ref().parent() {
576            if !parent.exists() {
577                fs::create_dir_all(parent)?;
578            }
579        }
580
581        // pack zip file
582        let file = File::create(output_path)?;
583        let mut zip = ZipWriter::new(file);
584        let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
585
586        for entry in WalkDir::new(&self.temp_dir) {
587            let entry = entry?;
588            let path = entry.path();
589
590            // It can be asserted that the path is prefixed with temp_dir,
591            // and there will be no boundary cases of symbolic links and hard links, etc.
592            let relative_path = path.strip_prefix(&self.temp_dir).unwrap();
593            let target_path = relative_path.to_string_lossy().replace("\\", "/");
594
595            if path.is_file() {
596                zip.start_file(target_path, options)?;
597
598                let mut buf = Vec::new();
599                File::open(path)?.read_to_end(&mut buf)?;
600
601                zip.write_all(&buf)?;
602            } else if path.is_dir() {
603                zip.add_directory(target_path, options)?;
604            }
605        }
606
607        zip.finish()?;
608        Ok(())
609    }
610
611    /// Builds an EPUB file and returns a `EpubDoc`
612    ///
613    /// Builds an EPUB file at the specified location and parses it into a usable EpubDoc object.
614    ///
615    /// ## Parameters
616    /// - `output_path`: Output file path
617    ///
618    /// ## Return
619    /// - `Ok(EpubDoc)`: Build successful
620    /// - `Err(EpubError)`: Error occurred during the build process
621    pub fn build<P: AsRef<Path>>(
622        self,
623        output_path: P,
624    ) -> Result<EpubDoc<BufReader<File>>, EpubError> {
625        self.make(&output_path)?;
626
627        EpubDoc::new(output_path)
628    }
629
630    /// Creates an `EpubBuilder` instance from an existing `EpubDoc`
631    ///
632    /// This function takes an existing parsed EPUB document and creates a new builder
633    /// instance with all the document's metadata, manifest items, spine, and catalog information.
634    /// It essentially reverses the EPUB building process by extracting all the necessary
635    /// components from the parsed document and preparing them for reconstruction.
636    ///
637    /// The function copies the following information from the provided `EpubDoc`:
638    /// - Rootfile path (based on the document's base path)
639    /// - All metadata items (title, author, identifier, etc.)
640    /// - Spine items (reading order of the publication)
641    /// - Catalog information (navigation points)
642    /// - Catalog title
643    /// - All manifest items (except those with 'nav' property, which are skipped)
644    ///
645    /// ## Parameters
646    /// - `doc`: A mutable reference to an `EpubDoc` instance that contains the parsed EPUB data
647    ///
648    /// ## Return
649    /// - `Ok(EpubBuilder)`: Successfully created builder instance populated with the document's data
650    /// - `Err(EpubError)`: Error occurred during the extraction process
651    ///
652    /// ## Notes
653    /// - This type of conversion will upgrade Epub2.x publications to Epub3.x.
654    ///   This upgrade conversion may encounter unknown errors (it is unclear whether
655    ///   it will cause errors), so please use it with caution.
656    pub fn from<R: Read + Seek>(doc: &mut EpubDoc<R>) -> Result<Self, EpubError> {
657        let mut builder = Self::new()?;
658
659        builder.add_rootfile(&doc.package_path.clone().to_string_lossy())?;
660        builder.metadata = doc.metadata.clone();
661        builder.spine = doc.spine.clone();
662        builder.catalog = doc.catalog.clone();
663        builder.catalog_title = doc.catalog_title.clone();
664
665        // clone manifest hashmap to avoid mut borrow conflict
666        for (_, mut manifest) in doc.manifest.clone().into_iter() {
667            if let Some(properties) = &manifest.properties {
668                if properties.contains("nav") {
669                    continue;
670                }
671            }
672
673            // because manifest paths in EpubDoc are converted to absolute paths rooted in containers,
674            // but in the form of 'path/to/manifest', they need to be converted here to absolute paths
675            // in the form of '/path/to/manifest'.
676            manifest.path = PathBuf::from("/").join(manifest.path);
677
678            let (buf, _) = doc.get_manifest_item(&manifest.id)?; // read raw file
679            let target_path = builder.normalize_manifest_path(&manifest.path, &manifest.id)?;
680            if let Some(parent_dir) = target_path.parent() {
681                if !parent_dir.exists() {
682                    fs::create_dir_all(parent_dir)?
683                }
684            }
685
686            fs::write(target_path, buf)?;
687            builder.manifest.insert(manifest.id.clone(), manifest);
688        }
689
690        Ok(builder)
691    }
692
693    /// Creates the `container.xml` file
694    ///
695    /// An error will occur if the `rootfile` path is not set
696    fn make_container_xml(&self) -> Result<(), EpubError> {
697        if self.rootfiles.is_empty() {
698            return Err(EpubBuilderError::MissingRootfile.into());
699        }
700
701        let mut writer = Writer::new(Cursor::new(Vec::new()));
702
703        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
704
705        writer.write_event(Event::Start(BytesStart::new("container").with_attributes(
706            [
707                ("version", "1.0"),
708                ("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container"),
709            ],
710        )))?;
711        writer.write_event(Event::Start(BytesStart::new("rootfiles")))?;
712
713        for rootfile in &self.rootfiles {
714            writer.write_event(Event::Empty(BytesStart::new("rootfile").with_attributes([
715                ("full-path", rootfile.as_str()),
716                ("media-type", "application/oebps-package+xml"),
717            ])))?;
718        }
719
720        writer.write_event(Event::End(BytesEnd::new("rootfiles")))?;
721        writer.write_event(Event::End(BytesEnd::new("container")))?;
722
723        let file_path = self.temp_dir.join("META-INF").join("container.xml");
724        let file_data = writer.into_inner().into_inner();
725        fs::write(file_path, file_data)?;
726
727        Ok(())
728    }
729
730    /// Creates the content document
731    #[cfg(feature = "content-builder")]
732    fn make_contents(&mut self) -> Result<(), EpubError> {
733        let mut buf = vec![0; 512];
734        let contents = std::mem::take(&mut self.content);
735
736        for (target, mut content) in contents.into_iter() {
737            let manifest_id = content.id.clone();
738
739            // target is relative to the epub file, so we need to normalize it
740            let absolute_target = self.normalize_manifest_path(&target, &manifest_id)?;
741            let mut resources = content.make(&absolute_target)?;
742
743            // Helper to compute absolute container path
744            let to_container_path = |p: &PathBuf| -> PathBuf {
745                match p.strip_prefix(&self.temp_dir) {
746                    Ok(rel) => PathBuf::from("/").join(rel.to_string_lossy().replace("\\", "/")),
747                    Err(_) => unreachable!("path MUST under temp directory"),
748                }
749            };
750
751            // Document (first element, guaranteed to exist)
752            let path = resources.swap_remove(0);
753            let mut file = std::fs::File::open(&path)?;
754            let _ = file.read(&mut buf)?;
755            let extension = path
756                .extension()
757                .map(|e| e.to_string_lossy().to_lowercase())
758                .unwrap_or_default();
759            let mime = match Infer::new().get(&buf) {
760                Some(infer) => refine_mime_type(infer.mime_type(), &extension),
761                None => {
762                    return Err(EpubBuilderError::UnknownFileFormat {
763                        file_path: path.to_string_lossy().to_string(),
764                    }
765                    .into());
766                }
767            }
768            .to_string();
769
770            self.manifest.insert(
771                manifest_id.clone(),
772                ManifestItem {
773                    id: manifest_id.clone(),
774                    path: to_container_path(&path),
775                    mime,
776                    properties: None,
777                    fallback: None,
778                },
779            );
780
781            // Other resources (if any): generate stable ids and add to manifest
782            for res in resources {
783                let mut file = fs::File::open(&res)?;
784                let _ = file.read(&mut buf)?;
785                let extension = res
786                    .extension()
787                    .map(|e| e.to_string_lossy().to_lowercase())
788                    .unwrap_or_default();
789                let mime = match Infer::new().get(&buf) {
790                    Some(ft) => refine_mime_type(ft.mime_type(), &extension),
791                    None => {
792                        return Err(EpubBuilderError::UnknownFileFormat {
793                            file_path: path.to_string_lossy().to_string(),
794                        }
795                        .into());
796                    }
797                }
798                .to_string();
799
800                let file_name = res
801                    .file_name()
802                    .map(|s| s.to_string_lossy().to_string())
803                    .unwrap_or_default();
804                let res_id = format!("{}-{}", manifest_id, file_name);
805
806                self.manifest.insert(
807                    res_id.clone(),
808                    ManifestItem {
809                        id: res_id,
810                        path: to_container_path(&res),
811                        mime,
812                        properties: None,
813                        fallback: None,
814                    },
815                );
816            }
817        }
818
819        Ok(())
820    }
821
822    /// Creates the `navigation document`
823    ///
824    /// An error will occur if navigation information is not initialized.
825    fn make_navigation_document(&mut self) -> Result<(), EpubError> {
826        if self.catalog.is_empty() {
827            return Err(EpubBuilderError::NavigationInfoUninitalized.into());
828        }
829
830        let mut writer = Writer::new(Cursor::new(Vec::new()));
831
832        writer.write_event(Event::Start(BytesStart::new("html").with_attributes([
833            ("xmlns", "http://www.w3.org/1999/xhtml"),
834            ("xmlns:epub", "http://www.idpf.org/2007/ops"),
835        ])))?;
836
837        // make head
838        writer.write_event(Event::Start(BytesStart::new("head")))?;
839        writer.write_event(Event::Start(BytesStart::new("title")))?;
840        writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
841        writer.write_event(Event::End(BytesEnd::new("title")))?;
842        writer.write_event(Event::End(BytesEnd::new("head")))?;
843
844        // make body
845        writer.write_event(Event::Start(BytesStart::new("body")))?;
846        writer.write_event(Event::Start(
847            BytesStart::new("nav").with_attributes([("epub:type", "toc")]),
848        ))?;
849
850        if !self.catalog_title.is_empty() {
851            writer.write_event(Event::Start(BytesStart::new("h1")))?;
852            writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
853            writer.write_event(Event::End(BytesEnd::new("h1")))?;
854        }
855
856        Self::make_nav(&mut writer, &self.catalog)?;
857
858        writer.write_event(Event::End(BytesEnd::new("nav")))?;
859        writer.write_event(Event::End(BytesEnd::new("body")))?;
860
861        writer.write_event(Event::End(BytesEnd::new("html")))?;
862
863        let file_path = self.temp_dir.join("nav.xhtml");
864        let file_data = writer.into_inner().into_inner();
865        fs::write(file_path, file_data)?;
866
867        self.manifest.insert(
868            "nav".to_string(),
869            ManifestItem {
870                id: "nav".to_string(),
871                path: PathBuf::from("/nav.xhtml"),
872                mime: "application/xhtml+xml".to_string(),
873                properties: Some("nav".to_string()),
874                fallback: None,
875            },
876        );
877
878        Ok(())
879    }
880
881    /// Creates the `OPF` file
882    ///
883    /// ## Error conditions
884    /// - Missing necessary metadata
885    /// - Circular reference exists in the manifest backlink
886    /// - Navigation information is not initialized
887    fn make_opf_file(&mut self) -> Result<(), EpubError> {
888        if !self.validate_metadata() {
889            return Err(EpubBuilderError::MissingNecessaryMetadata.into());
890        }
891        self.validate_manifest_fallback_chains()?;
892        self.validate_manifest_nav()?;
893
894        let mut writer = Writer::new(Cursor::new(Vec::new()));
895
896        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
897
898        writer.write_event(Event::Start(BytesStart::new("package").with_attributes([
899            ("xmlns", "http://www.idpf.org/2007/opf"),
900            ("xmlns:dc", "http://purl.org/dc/elements/1.1/"),
901            ("unique-identifier", "pub-id"),
902            ("version", "3.0"),
903        ])))?;
904
905        self.make_opf_metadata(&mut writer)?;
906        self.make_opf_manifest(&mut writer)?;
907        self.make_opf_spine(&mut writer)?;
908
909        writer.write_event(Event::End(BytesEnd::new("package")))?;
910
911        let file_path = self.temp_dir.join(&self.rootfiles[0]);
912        let file_data = writer.into_inner().into_inner();
913        fs::write(file_path, file_data)?;
914
915        Ok(())
916    }
917
918    fn make_opf_metadata(&mut self, writer: &mut XmlWriter) -> Result<(), EpubError> {
919        self.metadata.push(MetadataItem {
920            id: None,
921            property: "dcterms:modified".to_string(),
922            value: Utc::now().to_rfc3339_opts(SecondsFormat::AutoSi, true),
923            lang: None,
924            refined: vec![],
925        });
926
927        writer.write_event(Event::Start(BytesStart::new("metadata")))?;
928
929        for metadata in &self.metadata {
930            let tag_name = if ELEMENT_IN_DC_NAMESPACE.contains(&metadata.property.as_str()) {
931                format!("dc:{}", metadata.property)
932            } else {
933                "meta".to_string()
934            };
935
936            writer.write_event(Event::Start(
937                BytesStart::new(tag_name.as_str()).with_attributes(metadata.attributes()),
938            ))?;
939            writer.write_event(Event::Text(BytesText::new(metadata.value.as_str())))?;
940            writer.write_event(Event::End(BytesEnd::new(tag_name.as_str())))?;
941
942            for refinement in &metadata.refined {
943                writer.write_event(Event::Start(
944                    BytesStart::new("meta").with_attributes(refinement.attributes()),
945                ))?;
946                writer.write_event(Event::Text(BytesText::new(refinement.value.as_str())))?;
947                writer.write_event(Event::End(BytesEnd::new("meta")))?;
948            }
949        }
950
951        writer.write_event(Event::End(BytesEnd::new("metadata")))?;
952
953        Ok(())
954    }
955
956    fn make_opf_manifest(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
957        writer.write_event(Event::Start(BytesStart::new("manifest")))?;
958
959        for manifest in self.manifest.values() {
960            writer.write_event(Event::Empty(
961                BytesStart::new("item").with_attributes(manifest.attributes()),
962            ))?;
963        }
964
965        writer.write_event(Event::End(BytesEnd::new("manifest")))?;
966
967        Ok(())
968    }
969
970    fn make_opf_spine(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
971        writer.write_event(Event::Start(BytesStart::new("spine")))?;
972
973        for spine in &self.spine {
974            writer.write_event(Event::Empty(
975                BytesStart::new("itemref").with_attributes(spine.attributes()),
976            ))?;
977        }
978
979        writer.write_event(Event::End(BytesEnd::new("spine")))?;
980
981        Ok(())
982    }
983
984    fn make_nav(writer: &mut XmlWriter, navgations: &Vec<NavPoint>) -> Result<(), EpubError> {
985        writer.write_event(Event::Start(BytesStart::new("ol")))?;
986
987        for nav in navgations {
988            writer.write_event(Event::Start(BytesStart::new("li")))?;
989
990            if let Some(path) = &nav.content {
991                writer.write_event(Event::Start(
992                    BytesStart::new("a").with_attributes([("href", path.to_string_lossy())]),
993                ))?;
994                writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
995                writer.write_event(Event::End(BytesEnd::new("a")))?;
996            } else {
997                writer.write_event(Event::Start(BytesStart::new("span")))?;
998                writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
999                writer.write_event(Event::End(BytesEnd::new("span")))?;
1000            }
1001
1002            if !nav.children.is_empty() {
1003                Self::make_nav(writer, &nav.children)?;
1004            }
1005
1006            writer.write_event(Event::End(BytesEnd::new("li")))?;
1007        }
1008
1009        writer.write_event(Event::End(BytesEnd::new("ol")))?;
1010
1011        Ok(())
1012    }
1013
1014    /// Verify metadata integrity
1015    ///
1016    /// Check if the required metadata items are included: title, language, and identifier with pub-id.
1017    fn validate_metadata(&self) -> bool {
1018        let has_title = self.metadata.iter().any(|item| item.property == "title");
1019        let has_language = self.metadata.iter().any(|item| item.property == "language");
1020        let has_identifier = self.metadata.iter().any(|item| {
1021            item.property == "identifier" && item.id.as_ref().is_some_and(|id| id == "pub-id")
1022        });
1023
1024        has_title && has_identifier && has_language
1025    }
1026
1027    // TODO: consider using BFS to validate fallback chains, to provide efficient
1028    fn validate_manifest_fallback_chains(&self) -> Result<(), EpubError> {
1029        for (id, item) in &self.manifest {
1030            if item.fallback.is_none() {
1031                continue;
1032            }
1033
1034            let mut fallback_chain = Vec::new();
1035            self.validate_fallback_chain(id, &mut fallback_chain)?;
1036        }
1037
1038        Ok(())
1039    }
1040
1041    /// Recursively verify the validity of a single fallback chain
1042    ///
1043    /// This function recursively traces the fallback chain to check for the following issues:
1044    /// - Circular reference
1045    /// - The referenced fallback resource does not exist
1046    fn validate_fallback_chain(
1047        &self,
1048        manifest_id: &str,
1049        fallback_chain: &mut Vec<String>,
1050    ) -> Result<(), EpubError> {
1051        if fallback_chain.contains(&manifest_id.to_string()) {
1052            fallback_chain.push(manifest_id.to_string());
1053
1054            return Err(EpubBuilderError::ManifestCircularReference {
1055                fallback_chain: fallback_chain.join("->"),
1056            }
1057            .into());
1058        }
1059
1060        // Get the current item; its existence can be ensured based on the calling context.
1061        let item = self.manifest.get(manifest_id).unwrap();
1062
1063        if let Some(fallback_id) = &item.fallback {
1064            if !self.manifest.contains_key(fallback_id) {
1065                return Err(EpubBuilderError::ManifestNotFound {
1066                    manifest_id: fallback_id.to_owned(),
1067                }
1068                .into());
1069            }
1070
1071            fallback_chain.push(manifest_id.to_string());
1072            self.validate_fallback_chain(fallback_id, fallback_chain)
1073        } else {
1074            // The end of the fallback chain
1075            Ok(())
1076        }
1077    }
1078
1079    /// Validate navigation list items
1080    ///
1081    /// Check if there is only one list item with the `nav` property.
1082    fn validate_manifest_nav(&self) -> Result<(), EpubError> {
1083        if self
1084            .manifest
1085            .values()
1086            .filter(|&item| {
1087                if let Some(properties) = &item.properties {
1088                    properties.split(" ").any(|property| property == "nav")
1089                } else {
1090                    false
1091                }
1092            })
1093            .count()
1094            == 1
1095        {
1096            Ok(())
1097        } else {
1098            Err(EpubBuilderError::TooManyNavFlags.into())
1099        }
1100    }
1101
1102    /// Normalize manifest path to absolute path within EPUB container
1103    ///
1104    /// This function takes a path (relative or absolute) and normalizes it to an absolute
1105    /// path within the EPUB container structure. It handles various path formats including:
1106    /// - Relative paths starting with "../" (with security check to prevent directory traversal)
1107    /// - Absolute paths starting with "/" (relative to EPUB root)
1108    /// - Relative paths starting with "./" (current directory)
1109    /// - Plain relative paths (relative to the OPF file location)
1110    ///
1111    /// ## Parameters
1112    /// - `path`: The input path that may be relative or absolute. Can be any type that
1113    ///   implements `AsRef<Path>`, such as `&str`, `String`, `Path`, `PathBuf`, etc.
1114    /// - `id`: The id of the manifest item
1115    ///
1116    /// ## Return
1117    /// - `Ok(PathBuf)`: The normalized absolute path within the EPUB container,
1118    ///   and the absolute path is not starting with "/"
1119    /// - `Err(EpubError)`: Error if path traversal is detected outside the EPUB container,
1120    ///   or failed to locate the absolute path.
1121    fn normalize_manifest_path<P: AsRef<Path>>(
1122        &self,
1123        path: P,
1124        id: &str,
1125    ) -> Result<PathBuf, EpubError> {
1126        let opf_path = PathBuf::from(&self.rootfiles[0]);
1127        let basic_path = remove_leading_slash(opf_path.parent().unwrap());
1128
1129        // convert manifest path to absolute path(physical path)
1130        let mut target_path = if path.as_ref().starts_with("../") {
1131            check_realtive_link_leakage(
1132                self.temp_dir.clone(),
1133                basic_path.to_path_buf(),
1134                &path.as_ref().to_string_lossy(),
1135            )
1136            .map(PathBuf::from)
1137            .ok_or_else(|| EpubError::RelativeLinkLeakage {
1138                path: path.as_ref().to_string_lossy().to_string(),
1139            })?
1140        } else if let Ok(path) = path.as_ref().strip_prefix("/") {
1141            self.temp_dir.join(path)
1142        } else if path.as_ref().starts_with("./") {
1143            // can not anlyze where the 'current' directory is
1144            Err(EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() })?
1145        } else {
1146            self.temp_dir.join(basic_path).join(path)
1147        };
1148
1149        #[cfg(windows)]
1150        {
1151            target_path = PathBuf::from(target_path.to_string_lossy().replace('\\', "/"));
1152        }
1153
1154        Ok(target_path)
1155    }
1156
1157    /// Remove empty directories under the builder temporary directory
1158    ///
1159    /// By enumerate directories under `self.temp_dir` (excluding the root itself)
1160    /// and deletes directories that are empty. Directories are processed from deepest
1161    /// to shallowest so that parent directories which become empty after child
1162    /// deletion can also be removed.
1163    ///
1164    /// ## Return
1165    /// - `Ok(())`: Successfully removed all empty directories
1166    /// - `Err(EpubError)`: IO error
1167    fn remove_empty_dirs(&self) -> Result<(), EpubError> {
1168        let mut dirs = WalkDir::new(self.temp_dir.as_path())
1169            .min_depth(1)
1170            .into_iter()
1171            .filter_map(|entry| entry.ok())
1172            .filter(|entry| entry.file_type().is_dir())
1173            .map(|entry| entry.into_path())
1174            .collect::<Vec<PathBuf>>();
1175
1176        dirs.sort_by_key(|p| Reverse(p.components().count()));
1177
1178        for dir in dirs {
1179            if fs::read_dir(&dir)?.next().is_none() {
1180                fs::remove_dir(dir)?;
1181            }
1182        }
1183
1184        Ok(())
1185    }
1186}
1187
1188impl<Version> Drop for EpubBuilder<Version> {
1189    /// Remove temporary directory when dropped
1190    fn drop(&mut self) {
1191        if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
1192            warn!("{}", err);
1193        };
1194    }
1195}
1196
1197/// Refine the mime type
1198///
1199/// Optimize mime types inferred from file content based on file extensions
1200fn refine_mime_type<'a>(infer_mime: &'a str, extension: &'a str) -> &'a str {
1201    match (infer_mime, extension) {
1202        ("text/xml", "xhtml")
1203        | ("application/xml", "xhtml")
1204        | ("text/xml", "xht")
1205        | ("application/xml", "xht") => "application/xhtml+xml",
1206
1207        ("text/xml", "opf") | ("application/xml", "opf") => "application/oebps-package+xml",
1208
1209        ("text/xml", "ncx") | ("application/xml", "ncx") => "application/x-dtbncx+xml",
1210
1211        ("application/zip", "epub") => "application/epub+zip",
1212
1213        ("text/plain", "css") => "text/css",
1214        ("text/plain", "js") => "application/javascript",
1215        ("text/plain", "json") => "application/json",
1216        ("text/plain", "svg") => "image/svg+xml",
1217
1218        _ => infer_mime,
1219    }
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224    use std::{env, fs, path::PathBuf};
1225
1226    use crate::{
1227        builder::{EpubBuilder, EpubVersion3, refine_mime_type},
1228        epub::EpubDoc,
1229        error::{EpubBuilderError, EpubError},
1230        types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
1231        utils::local_time,
1232    };
1233
1234    #[test]
1235    fn test_epub_builder_new() {
1236        let builder = EpubBuilder::<EpubVersion3>::new();
1237        assert!(builder.is_ok());
1238
1239        let builder = builder.unwrap();
1240        assert!(builder.temp_dir.exists());
1241        assert!(builder.rootfiles.is_empty());
1242        assert!(builder.metadata.is_empty());
1243        assert!(builder.manifest.is_empty());
1244        assert!(builder.spine.is_empty());
1245        assert!(builder.catalog_title.is_empty());
1246        assert!(builder.catalog.is_empty());
1247    }
1248
1249    #[test]
1250    fn test_add_rootfile() {
1251        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1252        assert!(builder.add_rootfile("content.opf").is_ok());
1253
1254        assert_eq!(builder.rootfiles.len(), 1);
1255        assert_eq!(builder.rootfiles[0], "content.opf");
1256
1257        assert!(builder.add_rootfile("./another.opf").is_ok());
1258        assert_eq!(builder.rootfiles.len(), 2);
1259        assert_eq!(builder.rootfiles, vec!["content.opf", "another.opf"]);
1260    }
1261
1262    #[test]
1263    fn test_add_rootfile_fail() {
1264        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1265
1266        let result = builder.add_rootfile("/rootfile.opf");
1267        assert!(result.is_err());
1268        assert_eq!(
1269            result.unwrap_err(),
1270            EpubBuilderError::IllegalRootfilePath.into()
1271        );
1272
1273        let result = builder.add_rootfile("../rootfile.opf");
1274        assert!(result.is_err());
1275        assert_eq!(
1276            result.unwrap_err(),
1277            EpubBuilderError::IllegalRootfilePath.into()
1278        );
1279    }
1280
1281    #[test]
1282    fn test_remove_last_rootfile() {
1283        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1284
1285        assert!(builder.add_rootfile("first.opf").is_ok());
1286        assert!(builder.add_rootfile("second.opf").is_ok());
1287        assert!(builder.add_rootfile("third.opf").is_ok());
1288        assert_eq!(builder.rootfiles.len(), 3);
1289
1290        let result = builder.remove_last_rootfile();
1291        assert_eq!(result.rootfiles.len(), 2);
1292        assert_eq!(builder.rootfiles, vec!["first.opf", "second.opf"]);
1293
1294        builder.remove_last_rootfile();
1295        assert_eq!(builder.rootfiles.len(), 1);
1296        assert_eq!(builder.rootfiles[0], "first.opf");
1297
1298        builder.remove_last_rootfile();
1299        assert!(builder.rootfiles.is_empty());
1300
1301        builder.remove_last_rootfile();
1302        assert!(builder.rootfiles.is_empty());
1303    }
1304
1305    #[test]
1306    fn test_take_last_rootfile() {
1307        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1308
1309        let result = builder.take_last_rootfile();
1310        assert!(result.is_none());
1311
1312        builder.add_rootfile("first.opf").unwrap();
1313        builder.add_rootfile("second.opf").unwrap();
1314        builder.add_rootfile("third.opf").unwrap();
1315        assert_eq!(builder.rootfiles.len(), 3);
1316
1317        let result = builder.take_last_rootfile();
1318        assert!(result.is_some());
1319        assert_eq!(result.unwrap(), "third.opf");
1320        assert_eq!(builder.rootfiles.len(), 2);
1321
1322        let result = builder.take_last_rootfile();
1323        assert_eq!(result.unwrap(), "second.opf");
1324        assert_eq!(builder.rootfiles.len(), 1);
1325
1326        let result = builder.take_last_rootfile();
1327        assert_eq!(result.unwrap(), "first.opf");
1328        assert!(builder.rootfiles.is_empty());
1329
1330        let result = builder.take_last_rootfile();
1331        assert!(result.is_none());
1332    }
1333
1334    #[test]
1335    fn test_clear_rootfiles() {
1336        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1337
1338        builder.clear_rootfiles();
1339        assert!(builder.rootfiles.is_empty());
1340
1341        builder.add_rootfile("first.opf").unwrap();
1342        builder.add_rootfile("second.opf").unwrap();
1343        builder.add_rootfile("third.opf").unwrap();
1344        assert_eq!(builder.rootfiles.len(), 3);
1345
1346        builder.clear_rootfiles();
1347        assert!(builder.rootfiles.is_empty());
1348        assert_eq!(builder.rootfiles.len(), 0);
1349
1350        builder.add_rootfile("new.opf").unwrap();
1351        assert_eq!(builder.rootfiles.len(), 1);
1352        assert_eq!(builder.rootfiles[0], "new.opf");
1353    }
1354
1355    #[test]
1356    fn test_add_metadata() {
1357        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1358        let metadata_item = MetadataItem::new("title", "Test Book");
1359
1360        builder.add_metadata(metadata_item);
1361
1362        assert_eq!(builder.metadata.len(), 1);
1363        assert_eq!(builder.metadata[0].property, "title");
1364        assert_eq!(builder.metadata[0].value, "Test Book");
1365    }
1366
1367    #[test]
1368    fn test_remove_last_metadata() {
1369        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1370        builder.add_metadata(MetadataItem::new("title", "Test Book"));
1371        builder.add_metadata(MetadataItem::new("author", "Test Author"));
1372
1373        assert_eq!(builder.metadata.len(), 2);
1374
1375        builder.remove_last_metadata();
1376
1377        assert_eq!(builder.metadata.len(), 1);
1378        assert_eq!(builder.metadata[0].property, "title");
1379
1380        builder.remove_last_metadata();
1381        builder.remove_last_metadata();
1382        assert_eq!(builder.metadata.len(), 0);
1383    }
1384
1385    #[test]
1386    fn test_take_last_metadata() {
1387        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1388        let metadata1 = MetadataItem::new("title", "Test Book");
1389        let metadata2 = MetadataItem::new("author", "Test Author");
1390
1391        builder.add_metadata(metadata1);
1392        builder.add_metadata(metadata2);
1393        assert_eq!(builder.metadata.len(), 2);
1394
1395        let taken = builder.take_last_metadata();
1396        assert!(taken.is_some());
1397        assert_eq!(taken.unwrap().property, "author");
1398        assert_eq!(builder.metadata.len(), 1);
1399
1400        let _ = builder.take_last_metadata();
1401        let result = builder.take_last_metadata();
1402        assert!(result.is_none());
1403        assert_eq!(builder.metadata.len(), 0);
1404    }
1405
1406    #[test]
1407    fn test_clear_metadatas() {
1408        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1409        builder.add_metadata(MetadataItem::new("title", "Test Book"));
1410        builder.add_metadata(MetadataItem::new("author", "Test Author"));
1411        builder.add_metadata(MetadataItem::new("language", "en"));
1412
1413        assert_eq!(builder.metadata.len(), 3);
1414
1415        builder.clear_metadatas();
1416
1417        assert_eq!(builder.metadata.len(), 0);
1418
1419        builder.clear_metadatas();
1420        assert_eq!(builder.metadata.len(), 0);
1421    }
1422
1423    #[test]
1424    fn test_add_manifest_success() {
1425        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1426        assert!(builder.add_rootfile("content.opf").is_ok());
1427
1428        // Create a temporary file for testing
1429        let temp_dir = env::temp_dir().join(local_time());
1430        fs::create_dir_all(&temp_dir).unwrap();
1431        let test_file = temp_dir.join("test.xhtml");
1432        fs::write(&test_file, "<html><body>Hello World</body></html>").unwrap();
1433
1434        let manifest_item = ManifestItem::new("test", "/epub/test.xhtml").unwrap();
1435        let result = builder.add_manifest(test_file.to_str().unwrap(), manifest_item);
1436
1437        assert!(result.is_ok());
1438        assert_eq!(builder.manifest.len(), 1);
1439        assert!(builder.manifest.contains_key("test"));
1440
1441        fs::remove_dir_all(temp_dir).unwrap();
1442    }
1443
1444    #[test]
1445    fn test_add_manifest_no_rootfile() {
1446        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1447
1448        let manifest_item = ManifestItem {
1449            id: "main".to_string(),
1450            path: PathBuf::from("/Overview.xhtml"),
1451            mime: String::new(),
1452            properties: None,
1453            fallback: None,
1454        };
1455
1456        let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item.clone());
1457        assert!(result.is_err());
1458        assert_eq!(
1459            result.unwrap_err(),
1460            EpubBuilderError::MissingRootfile.into()
1461        );
1462
1463        let result = builder.add_rootfile("package.opf");
1464        assert!(result.is_ok());
1465
1466        let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item);
1467        assert!(result.is_ok());
1468    }
1469
1470    #[test]
1471    fn test_add_manifest_nonexistent_file() {
1472        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1473        assert!(builder.add_rootfile("content.opf").is_ok());
1474
1475        let manifest_item = ManifestItem::new("test", "nonexistent.xhtml").unwrap();
1476        let result = builder.add_manifest("nonexistent.xhtml", manifest_item);
1477
1478        assert!(result.is_err());
1479        assert_eq!(
1480            result.unwrap_err(),
1481            EpubBuilderError::TargetIsNotFile {
1482                target_path: "nonexistent.xhtml".to_string()
1483            }
1484            .into()
1485        );
1486    }
1487
1488    #[test]
1489    fn test_add_manifest_unknow_file_format() {
1490        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1491        let result = builder.add_rootfile("package.opf");
1492        assert!(result.is_ok());
1493
1494        let result = builder.add_manifest(
1495            "./test_case/unknown_file_format.xhtml",
1496            ManifestItem {
1497                id: "file".to_string(),
1498                path: PathBuf::from("unknown_file_format.xhtml"),
1499                mime: String::new(),
1500                properties: None,
1501                fallback: None,
1502            },
1503        );
1504
1505        assert!(result.is_err());
1506        assert_eq!(
1507            result.unwrap_err(),
1508            EpubBuilderError::UnknownFileFormat {
1509                file_path: "./test_case/unknown_file_format.xhtml".to_string(),
1510            }
1511            .into()
1512        )
1513    }
1514
1515    #[test]
1516    fn test_remove_manifest() {
1517        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1518        builder.add_rootfile("package.opf").unwrap();
1519
1520        builder
1521            .add_manifest(
1522                "./test_case/Overview.xhtml",
1523                ManifestItem::new("item1", "content1.xhtml").unwrap(),
1524            )
1525            .unwrap();
1526        builder
1527            .add_manifest(
1528                "./test_case/Overview.xhtml",
1529                ManifestItem::new("item2", "content2.xhtml").unwrap(),
1530            )
1531            .unwrap();
1532        builder
1533            .add_manifest(
1534                "./test_case/Overview.xhtml",
1535                ManifestItem::new("item3", "content3.xhtml").unwrap(),
1536            )
1537            .unwrap();
1538
1539        assert_eq!(builder.manifest.len(), 3);
1540
1541        let result = builder.remove_manifest("item2");
1542        assert!(result.is_ok());
1543        assert_eq!(builder.manifest.len(), 2);
1544        assert!(!builder.manifest.contains_key("item2"));
1545        assert!(builder.manifest.contains_key("item1"));
1546        assert!(builder.manifest.contains_key("item3"));
1547
1548        builder.remove_manifest("item1").unwrap();
1549        assert_eq!(builder.manifest.len(), 1);
1550        assert!(builder.manifest.contains_key("item3"));
1551
1552        let result = builder.remove_manifest("nonexistent");
1553        assert!(result.is_ok());
1554        assert_eq!(builder.manifest.len(), 1);
1555    }
1556
1557    #[test]
1558    fn test_take_manifest() {
1559        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1560        builder.add_rootfile("package.opf").unwrap();
1561
1562        builder
1563            .add_manifest(
1564                "./test_case/Overview.xhtml",
1565                ManifestItem::new("item1", "content1.xhtml").unwrap(),
1566            )
1567            .unwrap();
1568        builder
1569            .add_manifest(
1570                "./test_case/Overview.xhtml",
1571                ManifestItem::new("item2", "content2.xhtml").unwrap(),
1572            )
1573            .unwrap();
1574
1575        assert_eq!(builder.manifest.len(), 2);
1576
1577        let taken = builder.take_manifest("item1");
1578        assert!(taken.is_some());
1579        assert_eq!(taken.unwrap().id, "item1");
1580        assert_eq!(builder.manifest.len(), 1);
1581        assert!(!builder.manifest.contains_key("item1"));
1582
1583        let taken = builder.take_manifest("item2");
1584        assert!(taken.is_some());
1585        assert_eq!(taken.unwrap().id, "item2");
1586        assert!(builder.manifest.is_empty());
1587
1588        let taken = builder.take_manifest("item1");
1589        assert!(taken.is_none());
1590
1591        builder
1592            .add_manifest(
1593                "./test_case/Overview.xhtml",
1594                ManifestItem::new("item3", "content3.xhtml").unwrap(),
1595            )
1596            .unwrap();
1597        let taken = builder.take_manifest("nonexistent");
1598        assert!(taken.is_none());
1599        assert_eq!(builder.manifest.len(), 1);
1600    }
1601
1602    #[test]
1603    fn test_clear_manifests() {
1604        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1605        builder.add_rootfile("package.opf").unwrap();
1606
1607        let result = builder.clear_manifests();
1608        assert!(result.is_ok());
1609        assert!(builder.manifest.is_empty());
1610
1611        builder
1612            .add_manifest(
1613                "./test_case/Overview.xhtml",
1614                ManifestItem::new("item1", "content1.xhtml").unwrap(),
1615            )
1616            .unwrap();
1617        builder
1618            .add_manifest(
1619                "./test_case/Overview.xhtml",
1620                ManifestItem::new("item2", "content2.xhtml").unwrap(),
1621            )
1622            .unwrap();
1623        builder
1624            .add_manifest(
1625                "./test_case/Overview.xhtml",
1626                ManifestItem::new("item3", "content3.xhtml").unwrap(),
1627            )
1628            .unwrap();
1629
1630        assert_eq!(builder.manifest.len(), 3);
1631
1632        let result = builder.clear_manifests();
1633        assert!(result.is_ok());
1634        assert!(builder.manifest.is_empty());
1635
1636        builder
1637            .add_manifest(
1638                "./test_case/Overview.xhtml",
1639                ManifestItem::new("new_item", "new_content.xhtml").unwrap(),
1640            )
1641            .unwrap();
1642        assert_eq!(builder.manifest.len(), 1);
1643        assert_eq!(builder.manifest.get("new_item").unwrap().id, "new_item");
1644    }
1645
1646    #[test]
1647    fn test_add_spine() {
1648        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1649        let spine_item = SpineItem::new("test_item");
1650
1651        builder.add_spine(spine_item.clone());
1652
1653        assert_eq!(builder.spine.len(), 1);
1654        assert_eq!(builder.spine[0].idref, "test_item");
1655    }
1656
1657    #[test]
1658    fn test_remove_last_spine() {
1659        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1660
1661        builder.add_spine(SpineItem::new("chapter1"));
1662        builder.add_spine(SpineItem::new("chapter2"));
1663        builder.add_spine(SpineItem::new("chapter3"));
1664        assert_eq!(builder.spine.len(), 3);
1665
1666        builder.remove_last_spine();
1667        assert_eq!(builder.spine.len(), 2);
1668        assert_eq!(builder.spine[0].idref, "chapter1");
1669        assert_eq!(builder.spine[1].idref, "chapter2");
1670
1671        builder.remove_last_spine();
1672        assert_eq!(builder.spine.len(), 1);
1673        assert_eq!(builder.spine[0].idref, "chapter1");
1674
1675        builder.remove_last_spine();
1676        assert!(builder.spine.is_empty());
1677
1678        builder.remove_last_spine();
1679        assert!(builder.spine.is_empty());
1680    }
1681
1682    #[test]
1683    fn test_take_last_spine() {
1684        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1685
1686        let result = builder.take_last_spine();
1687        assert!(result.is_none());
1688
1689        builder.add_spine(SpineItem::new("chapter1"));
1690        builder.add_spine(SpineItem::new("chapter2"));
1691        builder.add_spine(SpineItem::new("chapter3"));
1692        assert_eq!(builder.spine.len(), 3);
1693
1694        let result = builder.take_last_spine();
1695        assert!(result.is_some());
1696        assert_eq!(result.unwrap().idref, "chapter3");
1697        assert_eq!(builder.spine.len(), 2);
1698
1699        let result = builder.take_last_spine();
1700        assert_eq!(result.unwrap().idref, "chapter2");
1701        assert_eq!(builder.spine.len(), 1);
1702
1703        let result = builder.take_last_spine();
1704        assert_eq!(result.unwrap().idref, "chapter1");
1705        assert!(builder.spine.is_empty());
1706
1707        let result = builder.take_last_spine();
1708        assert!(result.is_none());
1709    }
1710
1711    #[test]
1712    fn test_clear_spines() {
1713        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1714
1715        builder.clear_spines();
1716        assert!(builder.spine.is_empty());
1717
1718        builder.add_spine(SpineItem::new("chapter1"));
1719        builder.add_spine(SpineItem::new("chapter2"));
1720        builder.add_spine(SpineItem::new("chapter3"));
1721        assert_eq!(builder.spine.len(), 3);
1722
1723        builder.clear_spines();
1724        assert!(builder.spine.is_empty());
1725        assert_eq!(builder.spine.len(), 0);
1726
1727        builder.add_spine(SpineItem::new("new_chapter"));
1728        assert_eq!(builder.spine.len(), 1);
1729        assert_eq!(builder.spine[0].idref, "new_chapter");
1730    }
1731
1732    #[test]
1733    fn test_set_catalog_title() {
1734        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1735        let title = "Test Catalog Title";
1736
1737        builder.set_catalog_title(title);
1738
1739        assert_eq!(builder.catalog_title, title);
1740    }
1741
1742    #[test]
1743    fn test_add_catalog_item() {
1744        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1745        let nav_point = NavPoint::new("Chapter 1");
1746
1747        builder.add_catalog_item(nav_point.clone());
1748
1749        assert_eq!(builder.catalog.len(), 1);
1750        assert_eq!(builder.catalog[0].label, "Chapter 1");
1751    }
1752
1753    #[test]
1754    fn test_remove_last_catalog_item() {
1755        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1756
1757        builder.add_catalog_item(NavPoint::new("Chapter 1"));
1758        builder.add_catalog_item(NavPoint::new("Chapter 2"));
1759        builder.add_catalog_item(NavPoint::new("Chapter 3"));
1760        assert_eq!(builder.catalog.len(), 3);
1761
1762        builder.remove_last_catalog_item();
1763        assert_eq!(builder.catalog.len(), 2);
1764        assert_eq!(builder.catalog[0].label, "Chapter 1");
1765        assert_eq!(builder.catalog[1].label, "Chapter 2");
1766
1767        builder.remove_last_catalog_item();
1768        assert_eq!(builder.catalog.len(), 1);
1769        assert_eq!(builder.catalog[0].label, "Chapter 1");
1770
1771        builder.remove_last_catalog_item();
1772        assert!(builder.catalog.is_empty());
1773
1774        builder.remove_last_catalog_item();
1775        assert!(builder.catalog.is_empty());
1776    }
1777
1778    #[test]
1779    fn test_take_last_catalog_item() {
1780        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1781
1782        let result = builder.take_last_catalog_item();
1783        assert!(result.is_none());
1784
1785        builder.add_catalog_item(NavPoint::new("Chapter 1"));
1786        builder.add_catalog_item(NavPoint::new("Chapter 2"));
1787        builder.add_catalog_item(NavPoint::new("Chapter 3"));
1788        assert_eq!(builder.catalog.len(), 3);
1789
1790        let result = builder.take_last_catalog_item();
1791        assert!(result.is_some());
1792        assert_eq!(result.unwrap().label, "Chapter 3");
1793        assert_eq!(builder.catalog.len(), 2);
1794
1795        let result = builder.take_last_catalog_item();
1796        assert_eq!(result.unwrap().label, "Chapter 2");
1797        assert_eq!(builder.catalog.len(), 1);
1798
1799        let result = builder.take_last_catalog_item();
1800        assert_eq!(result.unwrap().label, "Chapter 1");
1801        assert!(builder.catalog.is_empty());
1802
1803        let result = builder.take_last_catalog_item();
1804        assert!(result.is_none());
1805    }
1806
1807    #[test]
1808    fn test_set_catalog() {
1809        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1810        let nav_points = vec![NavPoint::new("Chapter 1"), NavPoint::new("Chapter 2")];
1811
1812        builder.set_catalog(nav_points.clone());
1813
1814        assert_eq!(builder.catalog.len(), 2);
1815        assert_eq!(builder.catalog[0].label, "Chapter 1");
1816        assert_eq!(builder.catalog[1].label, "Chapter 2");
1817    }
1818
1819    #[test]
1820    fn test_clear_catalog() {
1821        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1822
1823        builder.clear_catalog();
1824        assert!(builder.catalog.is_empty());
1825
1826        builder.add_catalog_item(NavPoint::new("Chapter 1"));
1827        builder.add_catalog_item(NavPoint::new("Chapter 2"));
1828        builder.add_catalog_item(NavPoint::new("Chapter 3"));
1829        assert_eq!(builder.catalog.len(), 3);
1830
1831        builder.clear_catalog();
1832        assert!(builder.catalog.is_empty());
1833        assert_eq!(builder.catalog.len(), 0);
1834
1835        builder.add_catalog_item(NavPoint::new("New Chapter"));
1836        assert_eq!(builder.catalog.len(), 1);
1837        assert_eq!(builder.catalog[0].label, "New Chapter");
1838    }
1839
1840    #[test]
1841    fn test_clear_all() {
1842        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1843
1844        builder.add_rootfile("content.opf").unwrap();
1845        builder.add_metadata(MetadataItem::new("title", "Test Book"));
1846        builder.add_metadata(MetadataItem::new("language", "en"));
1847        builder.add_spine(SpineItem::new("chapter1"));
1848        builder.add_spine(SpineItem::new("chapter2"));
1849        builder.add_catalog_item(NavPoint::new("Chapter 1"));
1850        builder.add_catalog_item(NavPoint::new("Chapter 2"));
1851        builder.set_catalog_title("Table of Contents");
1852
1853        assert_eq!(builder.metadata.len(), 2);
1854        assert_eq!(builder.spine.len(), 2);
1855        assert_eq!(builder.catalog.len(), 2);
1856        assert_eq!(builder.catalog_title, "Table of Contents");
1857
1858        let result = builder.clear_all();
1859        assert!(result.is_ok());
1860
1861        assert!(builder.metadata.is_empty());
1862        assert!(builder.spine.is_empty());
1863        assert!(builder.catalog.is_empty());
1864        assert!(builder.catalog_title.is_empty());
1865        assert!(builder.manifest.is_empty());
1866
1867        builder.add_metadata(MetadataItem::new("title", "New Book"));
1868        builder.add_spine(SpineItem::new("new_chapter"));
1869        builder.add_catalog_item(NavPoint::new("New Chapter"));
1870
1871        assert_eq!(builder.metadata.len(), 1);
1872        assert_eq!(builder.spine.len(), 1);
1873        assert_eq!(builder.catalog.len(), 1);
1874    }
1875
1876    #[test]
1877    fn test_make_container_file() {
1878        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1879
1880        let result = builder.make_container_xml();
1881        assert!(result.is_err());
1882        assert_eq!(
1883            result.unwrap_err(),
1884            EpubBuilderError::MissingRootfile.into()
1885        );
1886
1887        assert!(builder.add_rootfile("content.opf").is_ok());
1888        let result = builder.make_container_xml();
1889        assert!(result.is_ok());
1890    }
1891
1892    #[test]
1893    fn test_make_navigation_document() {
1894        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1895
1896        let result = builder.make_navigation_document();
1897        assert!(result.is_err());
1898        assert_eq!(
1899            result.unwrap_err(),
1900            EpubBuilderError::NavigationInfoUninitalized.into()
1901        );
1902
1903        builder.set_catalog(vec![NavPoint::new("test")]);
1904        assert!(builder.make_navigation_document().is_ok());
1905    }
1906
1907    #[test]
1908    fn test_validate_metadata_success() {
1909        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1910
1911        builder.add_metadata(MetadataItem::new("title", "Test Book"));
1912        builder.add_metadata(MetadataItem::new("language", "en"));
1913        builder.add_metadata(
1914            MetadataItem::new("identifier", "urn:isbn:1234567890")
1915                .with_id("pub-id")
1916                .build(),
1917        );
1918
1919        assert!(builder.validate_metadata());
1920    }
1921
1922    #[test]
1923    fn test_validate_metadata_missing_required() {
1924        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1925
1926        builder.add_metadata(MetadataItem::new("title", "Test Book"));
1927        builder.add_metadata(MetadataItem::new("language", "en"));
1928
1929        assert!(!builder.validate_metadata());
1930    }
1931
1932    #[test]
1933    fn test_validate_fallback_chain_valid() {
1934        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1935
1936        let item3 = ManifestItem::new("item3", "path3");
1937        assert!(item3.is_ok());
1938
1939        let item3 = item3.unwrap();
1940        let item2 = ManifestItem::new("item2", "path2")
1941            .unwrap()
1942            .with_fallback("item3")
1943            .build();
1944        let item1 = ManifestItem::new("item1", "path1")
1945            .unwrap()
1946            .with_fallback("item2")
1947            .build();
1948
1949        builder.manifest.insert("item3".to_string(), item3);
1950        builder.manifest.insert("item2".to_string(), item2);
1951        builder.manifest.insert("item1".to_string(), item1);
1952
1953        let result = builder.validate_manifest_fallback_chains();
1954        assert!(result.is_ok());
1955    }
1956
1957    #[test]
1958    fn test_validate_fallback_chain_circular_reference() {
1959        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1960
1961        let item2 = ManifestItem::new("item2", "path2")
1962            .unwrap()
1963            .with_fallback("item1")
1964            .build();
1965        let item1 = ManifestItem::new("item1", "path1")
1966            .unwrap()
1967            .with_fallback("item2")
1968            .build();
1969
1970        builder.manifest.insert("item1".to_string(), item1);
1971        builder.manifest.insert("item2".to_string(), item2);
1972
1973        let result = builder.validate_manifest_fallback_chains();
1974        assert!(result.is_err());
1975        assert!(
1976            result.unwrap_err().to_string().starts_with(
1977                "Epub builder error: Circular reference detected in fallback chain for"
1978            ),
1979        );
1980    }
1981
1982    #[test]
1983    fn test_validate_fallback_chain_not_found() {
1984        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1985
1986        let item1 = ManifestItem::new("item1", "path1")
1987            .unwrap()
1988            .with_fallback("nonexistent")
1989            .build();
1990
1991        builder.manifest.insert("item1".to_string(), item1);
1992
1993        let result = builder.validate_manifest_fallback_chains();
1994        assert!(result.is_err());
1995        assert_eq!(
1996            result.unwrap_err().to_string(),
1997            "Epub builder error: Fallback resource 'nonexistent' does not exist in manifest."
1998        );
1999    }
2000
2001    #[test]
2002    fn test_validate_manifest_nav_single() {
2003        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2004
2005        let nav_item = ManifestItem::new("nav", "nav.xhtml")
2006            .unwrap()
2007            .append_property("nav")
2008            .build();
2009        builder.manifest.insert("nav".to_string(), nav_item);
2010
2011        let result = builder.validate_manifest_nav();
2012        assert!(result.is_ok());
2013    }
2014
2015    #[test]
2016    fn test_validate_manifest_nav_multiple() {
2017        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2018
2019        let nav_item1 = ManifestItem::new("nav1", "nav1.xhtml")
2020            .unwrap()
2021            .append_property("nav")
2022            .build();
2023        let nav_item2 = ManifestItem::new("nav2", "nav2.xhtml")
2024            .unwrap()
2025            .append_property("nav")
2026            .build();
2027
2028        builder.manifest.insert("nav1".to_string(), nav_item1);
2029        builder.manifest.insert("nav2".to_string(), nav_item2);
2030
2031        let result = builder.validate_manifest_nav();
2032        assert!(result.is_err());
2033        assert_eq!(
2034            result.unwrap_err().to_string(),
2035            "Epub builder error: There are too many items with 'nav' property in the manifest."
2036        );
2037    }
2038
2039    #[test]
2040    fn test_make_opf_file_success() {
2041        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2042
2043        assert!(builder.add_rootfile("content.opf").is_ok());
2044        builder.add_metadata(MetadataItem::new("title", "Test Book"));
2045        builder.add_metadata(MetadataItem::new("language", "en"));
2046        builder.add_metadata(
2047            MetadataItem::new("identifier", "urn:isbn:1234567890")
2048                .with_id("pub-id")
2049                .build(),
2050        );
2051
2052        let temp_dir = env::temp_dir().join(local_time());
2053        fs::create_dir_all(&temp_dir).unwrap();
2054
2055        let test_file = temp_dir.join("test.xhtml");
2056        fs::write(&test_file, "<html></html>").unwrap();
2057
2058        let manifest_result = builder.add_manifest(
2059            test_file.to_str().unwrap(),
2060            ManifestItem::new("test", "test.xhtml").unwrap(),
2061        );
2062        assert!(manifest_result.is_ok());
2063
2064        builder.add_catalog_item(NavPoint::new("Chapter"));
2065        builder.add_spine(SpineItem::new("test"));
2066
2067        let result = builder.make_navigation_document();
2068        assert!(result.is_ok());
2069
2070        let result = builder.make_opf_file();
2071        assert!(result.is_ok());
2072
2073        let opf_path = builder.temp_dir.join("content.opf");
2074        assert!(opf_path.exists());
2075
2076        fs::remove_dir_all(temp_dir).unwrap();
2077    }
2078
2079    #[test]
2080    fn test_make_opf_file_missing_metadata() {
2081        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2082        assert!(builder.add_rootfile("content.opf").is_ok());
2083
2084        let result = builder.make_opf_file();
2085        assert!(result.is_err());
2086        assert_eq!(
2087            result.unwrap_err().to_string(),
2088            "Epub builder error: Requires at least one 'title', 'language', and 'identifier' with id 'pub-id'."
2089        );
2090    }
2091
2092    #[test]
2093    fn test_make() {
2094        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2095
2096        assert!(builder.add_rootfile("content.opf").is_ok());
2097        builder.add_metadata(MetadataItem::new("title", "Test Book"));
2098        builder.add_metadata(MetadataItem::new("language", "en"));
2099        builder.add_metadata(
2100            MetadataItem::new("identifier", "test_identifier")
2101                .with_id("pub-id")
2102                .build(),
2103        );
2104
2105        assert!(
2106            builder
2107                .add_manifest(
2108                    "./test_case/Overview.xhtml",
2109                    ManifestItem {
2110                        id: "test".to_string(),
2111                        path: PathBuf::from("test.xhtml"),
2112                        mime: String::new(),
2113                        properties: None,
2114                        fallback: None,
2115                    },
2116                )
2117                .is_ok()
2118        );
2119
2120        builder.add_catalog_item(NavPoint::new("Chapter"));
2121        builder.add_spine(SpineItem::new("test"));
2122
2123        let file = env::temp_dir()
2124            .join("temp_dir")
2125            .join(format!("{}.epub", local_time()));
2126        assert!(builder.make(&file).is_ok());
2127        assert!(EpubDoc::new(&file).is_ok());
2128    }
2129
2130    #[test]
2131    fn test_build() {
2132        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2133
2134        assert!(builder.add_rootfile("content.opf").is_ok());
2135        builder.add_metadata(MetadataItem::new("title", "Test Book"));
2136        builder.add_metadata(MetadataItem::new("language", "en"));
2137        builder.add_metadata(
2138            MetadataItem::new("identifier", "test_identifier")
2139                .with_id("pub-id")
2140                .build(),
2141        );
2142
2143        assert!(
2144            builder
2145                .add_manifest(
2146                    "./test_case/Overview.xhtml",
2147                    ManifestItem {
2148                        id: "test".to_string(),
2149                        path: PathBuf::from("test.xhtml"),
2150                        mime: String::new(),
2151                        properties: None,
2152                        fallback: None,
2153                    },
2154                )
2155                .is_ok()
2156        );
2157
2158        builder.add_catalog_item(NavPoint::new("Chapter"));
2159        builder.add_spine(SpineItem::new("test"));
2160
2161        let file = env::temp_dir().join(format!("{}.epub", local_time()));
2162        assert!(builder.build(&file).is_ok());
2163    }
2164
2165    #[test]
2166    fn test_from() {
2167        let builder = EpubBuilder::<EpubVersion3>::new();
2168        assert!(builder.is_ok());
2169
2170        let metadata = vec![
2171            MetadataItem {
2172                id: None,
2173                property: "title".to_string(),
2174                value: "Test Book".to_string(),
2175                lang: None,
2176                refined: vec![],
2177            },
2178            MetadataItem {
2179                id: None,
2180                property: "language".to_string(),
2181                value: "en".to_string(),
2182                lang: None,
2183                refined: vec![],
2184            },
2185            MetadataItem {
2186                id: Some("pub-id".to_string()),
2187                property: "identifier".to_string(),
2188                value: "test-book".to_string(),
2189                lang: None,
2190                refined: vec![],
2191            },
2192        ];
2193        let spine = vec![SpineItem {
2194            id: None,
2195            idref: "main".to_string(),
2196            linear: true,
2197            properties: None,
2198        }];
2199        let catalog = vec![
2200            NavPoint {
2201                label: "Nav".to_string(),
2202                content: None,
2203                children: vec![],
2204                play_order: None,
2205            },
2206            NavPoint {
2207                label: "Overview".to_string(),
2208                content: None,
2209                children: vec![],
2210                play_order: None,
2211            },
2212        ];
2213
2214        let mut builder = builder.unwrap();
2215        assert!(builder.add_rootfile("content.opf").is_ok());
2216        builder.metadata = metadata.clone();
2217        builder.spine = spine.clone();
2218        builder.catalog = catalog.clone();
2219        builder.set_catalog_title("catalog title");
2220        let result = builder.add_manifest(
2221            "./test_case/Overview.xhtml",
2222            ManifestItem {
2223                id: "main".to_string(),
2224                path: PathBuf::from("Overview.xhtml"),
2225                mime: String::new(),
2226                properties: None,
2227                fallback: None,
2228            },
2229        );
2230        assert!(result.is_ok());
2231
2232        let epub_file = env::temp_dir().join(format!("{}.epub", local_time()));
2233        let result = builder.make(&epub_file);
2234        assert!(result.is_ok());
2235
2236        let doc = EpubDoc::new(&epub_file);
2237        assert!(doc.is_ok());
2238
2239        let mut doc = doc.unwrap();
2240        let builder = EpubBuilder::from(&mut doc);
2241        assert!(builder.is_ok());
2242        let builder = builder.unwrap();
2243
2244        assert_eq!(builder.metadata.len(), metadata.len() + 1);
2245        assert_eq!(builder.manifest.len(), 1); // skip nav file
2246        assert_eq!(builder.spine.len(), spine.len());
2247        assert_eq!(builder.catalog, catalog);
2248        assert_eq!(builder.catalog_title, "catalog title");
2249
2250        fs::remove_file(epub_file).unwrap();
2251    }
2252
2253    #[test]
2254    fn test_normalize_manifest_path() {
2255        let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2256
2257        assert!(builder.add_rootfile("content.opf").is_ok());
2258
2259        let result = builder.normalize_manifest_path("../../test.xhtml", "id");
2260        assert!(result.is_err());
2261        assert_eq!(
2262            result.unwrap_err(),
2263            EpubError::RelativeLinkLeakage { path: "../../test.xhtml".to_string() }
2264        );
2265
2266        let result = builder.normalize_manifest_path("/test.xhtml", "id");
2267        assert!(result.is_ok());
2268        assert_eq!(result.unwrap(), builder.temp_dir.join("test.xhtml"));
2269
2270        let result = builder.normalize_manifest_path("./test.xhtml", "manifest_id");
2271        assert!(result.is_err());
2272        assert_eq!(
2273            result.unwrap_err(),
2274            EpubBuilderError::IllegalManifestPath { manifest_id: "manifest_id".to_string() }.into(),
2275        );
2276    }
2277
2278    #[test]
2279    fn test_refine_mime_type() {
2280        assert_eq!(
2281            refine_mime_type("text/xml", "xhtml"),
2282            "application/xhtml+xml"
2283        );
2284        assert_eq!(refine_mime_type("text/xml", "xht"), "application/xhtml+xml");
2285        assert_eq!(
2286            refine_mime_type("application/xml", "opf"),
2287            "application/oebps-package+xml"
2288        );
2289        assert_eq!(
2290            refine_mime_type("text/xml", "ncx"),
2291            "application/x-dtbncx+xml"
2292        );
2293        assert_eq!(refine_mime_type("text/plain", "css"), "text/css");
2294        assert_eq!(refine_mime_type("text/plain", "unknown"), "text/plain");
2295    }
2296
2297    #[cfg(feature = "content-builder")]
2298    mod make_contents_tests {
2299        use std::path::PathBuf;
2300
2301        use crate::builder::{EpubBuilder, EpubVersion3, content::ContentBuilder};
2302
2303        #[test]
2304        fn test_make_contents_basic() {
2305            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2306            builder.add_rootfile("content.opf").unwrap();
2307
2308            let mut content_builder = ContentBuilder::new("chapter1", "en").unwrap();
2309            content_builder
2310                .set_title("Test Chapter")
2311                .add_text_block("This is a test paragraph.", vec![])
2312                .unwrap();
2313
2314            builder.add_content("OEBPS/chapter1.xhtml", content_builder);
2315
2316            let result = builder.make_contents();
2317            assert!(result.is_ok());
2318            assert!(builder.temp_dir.join("OEBPS/chapter1.xhtml").exists());
2319        }
2320
2321        #[test]
2322        fn test_make_contents_multiple_blocks() {
2323            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2324            builder.add_rootfile("content.opf").unwrap();
2325
2326            let mut content_builder = ContentBuilder::new("chapter2", "zh-CN").unwrap();
2327            content_builder
2328                .set_title("多个区块章节")
2329                .add_text_block("第一段文本。", vec![])
2330                .unwrap()
2331                .add_quote_block("这是一个引用。", vec![])
2332                .unwrap()
2333                .add_title_block("子标题", 2, vec![])
2334                .unwrap()
2335                .add_text_block("最后的文本段落。", vec![])
2336                .unwrap();
2337
2338            builder.add_content("OEBPS/chapter2.xhtml", content_builder);
2339
2340            let result = builder.make_contents();
2341            assert!(result.is_ok());
2342            assert!(builder.temp_dir.join("OEBPS/chapter2.xhtml").exists());
2343        }
2344
2345        #[test]
2346        fn test_make_contents_with_media() {
2347            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2348            builder.add_rootfile("content.opf").unwrap();
2349
2350            let img_path = PathBuf::from("./test_case/image.jpg");
2351
2352            let mut content_builder = ContentBuilder::new("chapter3", "en").unwrap();
2353            content_builder
2354                .set_title("Chapter with Media")
2355                .add_text_block("Text before image.", vec![])
2356                .unwrap()
2357                .add_image_block(
2358                    img_path,
2359                    Some("Test Image".to_string()),
2360                    Some("Figure 1: A test image".to_string()),
2361                    vec![],
2362                )
2363                .unwrap()
2364                .add_text_block("Text after image.", vec![])
2365                .unwrap();
2366
2367            builder.add_content("OEBPS/chapter3.xhtml", content_builder);
2368
2369            let result = builder.make_contents();
2370            assert!(result.is_ok());
2371            assert!(builder.temp_dir.join("OEBPS/chapter3.xhtml").exists());
2372            assert!(builder.temp_dir.join("OEBPS/img").exists());
2373            assert!(builder.temp_dir.join("OEBPS/img/image.jpg").exists());
2374        }
2375
2376        #[test]
2377        fn test_make_contents_multiple_documents() {
2378            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2379            builder.add_rootfile("content.opf").unwrap();
2380
2381            let mut content = ContentBuilder::new("ch1", "en").unwrap();
2382            content
2383                .set_title("Chapter 1")
2384                .add_text_block("Content of chapter 1", vec![])
2385                .unwrap();
2386            builder.add_content("OEBPS/chapter1.xhtml", content);
2387
2388            let mut content = ContentBuilder::new("ch2", "en").unwrap();
2389            content
2390                .set_title("Chapter 2")
2391                .add_text_block("Content of chapter 2", vec![])
2392                .unwrap();
2393            builder.add_content("OEBPS/chapter2.xhtml", content);
2394
2395            let mut content = ContentBuilder::new("ch3", "en").unwrap();
2396            content
2397                .set_title("Chapter 3")
2398                .add_text_block("Content of chapter 3", vec![])
2399                .unwrap();
2400            builder.add_content("OEBPS/chapter3.xhtml", content);
2401
2402            let result = builder.make_contents();
2403            assert!(result.is_ok());
2404            assert!(builder.temp_dir.join("OEBPS/chapter1.xhtml").exists());
2405            assert!(builder.temp_dir.join("OEBPS/chapter2.xhtml").exists());
2406            assert!(builder.temp_dir.join("OEBPS/chapter3.xhtml").exists());
2407        }
2408
2409        #[test]
2410        fn test_make_contents_different_languages() {
2411            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2412            builder.add_rootfile("content.opf").unwrap();
2413
2414            let mut content = ContentBuilder::new("en_ch", "en").unwrap();
2415            content
2416                .set_title("English Chapter")
2417                .add_text_block("English text.", vec![])
2418                .unwrap();
2419            builder.add_content("OEBPS/en_chapter.xhtml", content);
2420
2421            let mut content = ContentBuilder::new("zh_ch", "zh-CN").unwrap();
2422            content
2423                .set_title("中文章节")
2424                .add_text_block("中文文本。", vec![])
2425                .unwrap();
2426            builder.add_content("OEBPS/zh_chapter.xhtml", content);
2427
2428            let mut content = ContentBuilder::new("ja_ch", "ja").unwrap();
2429            content
2430                .set_title("日本語の章")
2431                .add_text_block("日本語のテキスト。", vec![])
2432                .unwrap();
2433            builder.add_content("OEBPS/ja_chapter.xhtml", content);
2434
2435            let result = builder.make_contents();
2436            assert!(result.is_ok());
2437            assert!(builder.temp_dir.join("OEBPS/en_chapter.xhtml").exists());
2438            assert!(builder.temp_dir.join("OEBPS/zh_chapter.xhtml").exists());
2439            assert!(builder.temp_dir.join("OEBPS/ja_chapter.xhtml").exists());
2440        }
2441
2442        #[test]
2443        fn test_make_contents_unique_identifiers() {
2444            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2445            builder.add_rootfile("content.opf").unwrap();
2446
2447            let mut content = ContentBuilder::new("unique_id_1", "en").unwrap();
2448            content.add_text_block("First content", vec![]).unwrap();
2449            builder.add_content("OEBPS/ch1.xhtml", content);
2450
2451            let mut content = ContentBuilder::new("unique_id_2", "en").unwrap();
2452            content.add_text_block("Second content", vec![]).unwrap();
2453            builder.add_content("OEBPS/ch2.xhtml", content);
2454
2455            let mut content = ContentBuilder::new("unique_id_1", "en").unwrap();
2456            content
2457                .add_text_block("Duplicate ID content", vec![])
2458                .unwrap();
2459            builder.add_content("OEBPS/ch3.xhtml", content);
2460
2461            let result = builder.make_contents();
2462            assert!(result.is_ok());
2463            assert!(builder.temp_dir.join("OEBPS/ch1.xhtml").exists()); // recovered by ch3
2464            assert!(builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
2465            assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
2466
2467            let manifest = builder.manifest.get("unique_id_1");
2468            assert!(manifest.is_some());
2469
2470            let manifest = manifest.unwrap();
2471            assert_eq!(manifest.path, PathBuf::from("/OEBPS/ch3.xhtml"));
2472        }
2473
2474        #[test]
2475        fn test_make_contents_complex_structure() {
2476            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2477            builder.add_rootfile("content.opf").unwrap();
2478
2479            let mut content = ContentBuilder::new("complex_ch", "en").unwrap();
2480            content
2481                .set_title("Complex Chapter")
2482                .add_title_block("Section 1", 2, vec![])
2483                .unwrap()
2484                .add_text_block("Introduction text.", vec![])
2485                .unwrap()
2486                .add_quote_block("A wise quote here.", vec![])
2487                .unwrap()
2488                .add_title_block("Section 2", 2, vec![])
2489                .unwrap()
2490                .add_text_block("More content with multiple paragraphs.", vec![])
2491                .unwrap()
2492                .add_text_block("Another paragraph.", vec![])
2493                .unwrap()
2494                .add_title_block("Section 3", 2, vec![])
2495                .unwrap()
2496                .add_quote_block("Another quotation.", vec![])
2497                .unwrap();
2498
2499            builder.add_content("OEBPS/complex_chapter.xhtml", content);
2500
2501            let result = builder.make_contents();
2502            assert!(result.is_ok());
2503            assert!(
2504                builder
2505                    .temp_dir
2506                    .join("OEBPS/complex_chapter.xhtml")
2507                    .exists()
2508            );
2509        }
2510
2511        #[test]
2512        fn test_make_contents_empty_document() {
2513            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2514            builder.add_rootfile("content.opf").unwrap();
2515
2516            let content = ContentBuilder::new("empty_ch", "en").unwrap();
2517            builder.add_content("OEBPS/empty.xhtml", content);
2518
2519            let result = builder.make_contents();
2520            assert!(result.is_ok());
2521            assert!(builder.temp_dir.join("OEBPS/empty.xhtml").exists());
2522        }
2523
2524        #[test]
2525        fn test_make_contents_path_normalization() {
2526            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2527            builder.add_rootfile("OEBPS/content.opf").unwrap();
2528
2529            let mut content = ContentBuilder::new("path_test", "en").unwrap();
2530            content.add_text_block("Path test content", vec![]).unwrap();
2531
2532            builder.add_content("/OEBPS/text/chapter.xhtml", content);
2533
2534            let result = builder.make_contents();
2535            assert!(result.is_ok());
2536            assert!(builder.temp_dir.join("OEBPS/text/chapter.xhtml").exists());
2537        }
2538
2539        #[test]
2540        fn test_make_contents_remove_and_readd() {
2541            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2542            builder.add_rootfile("content.opf").unwrap();
2543
2544            let mut content = ContentBuilder::new("ch1", "en").unwrap();
2545            content.add_text_block("First content", vec![]).unwrap();
2546            builder.add_content("OEBPS/ch1.xhtml", content);
2547
2548            let mut content = ContentBuilder::new("ch2", "en").unwrap();
2549            content.add_text_block("Second content", vec![]).unwrap();
2550            builder.add_content("OEBPS/ch2.xhtml", content);
2551
2552            builder.remove_last_content();
2553
2554            let taken = builder.take_last_content();
2555            assert!(taken.is_some());
2556            assert_eq!(taken.as_ref().unwrap().0, PathBuf::from("OEBPS/ch1.xhtml"));
2557
2558            let mut content = ContentBuilder::new("ch3", "en").unwrap();
2559            content.add_text_block("Third content", vec![]).unwrap();
2560            builder.add_content("OEBPS/ch3.xhtml", content);
2561
2562            let result = builder.make_contents();
2563            assert!(result.is_ok());
2564            assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
2565        }
2566
2567        #[test]
2568        fn test_make_contents_clear_all() {
2569            let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2570            builder.add_rootfile("content.opf").unwrap();
2571
2572            let mut content = ContentBuilder::new("ch1", "en").unwrap();
2573            content.add_text_block("Content 1", vec![]).unwrap();
2574            builder.add_content("OEBPS/ch1.xhtml", content);
2575
2576            let mut content = ContentBuilder::new("ch2", "en").unwrap();
2577            content.add_text_block("Content 2", vec![]).unwrap();
2578            builder.add_content("OEBPS/ch2.xhtml", content);
2579
2580            builder.clear_contents();
2581
2582            let result = builder.make_contents();
2583            assert!(result.is_ok());
2584            assert!(!builder.temp_dir.join("OEBPS/ch1.xhtml").exists());
2585            assert!(!builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
2586        }
2587    }
2588}