Skip to main content

lib_epub/
builder.rs

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