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