1use 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#[cfg_attr(test, derive(Debug))]
70pub struct EpubVersion3;
71
72#[cfg_attr(test, derive(Debug))]
117pub struct EpubBuilder<Version> {
118 epub_version: PhantomData<Version>,
120
121 temp_dir: PathBuf,
123
124 rootfiles: Vec<String>,
126
127 metadata: Vec<MetadataItem>,
129
130 manifest: HashMap<String, ManifestItem>,
132
133 spine: Vec<SpineItem>,
135
136 catalog_title: String,
137
138 catalog: Vec<NavPoint>,
140}
141
142impl EpubBuilder<EpubVersion3> {
143 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 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 pub fn remove_last_rootfile(&mut self) -> &mut Self {
197 self.rootfiles.pop();
198 self
199 }
200
201 pub fn take_last_rootfile(&mut self) -> Option<String> {
207 self.rootfiles.pop()
208 }
209
210 pub fn clear_rootfiles(&mut self) -> &mut Self {
212 self.rootfiles.clear();
213 self
214 }
215
216 pub fn add_metadata(&mut self, item: MetadataItem) -> &mut Self {
224 self.metadata.push(item);
225 self
226 }
227
228 pub fn remove_last_metadata(&mut self) -> &mut Self {
230 self.metadata.pop();
231 self
232 }
233
234 pub fn take_last_metadata(&mut self) -> Option<MetadataItem> {
240 self.metadata.pop()
241 }
242
243 pub fn clear_metadatas(&mut self) -> &mut Self {
245 self.metadata.clear();
246 self
247 }
248
249 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 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 let extension = match source.extension() {
284 Some(ext) => ext.to_string_lossy().to_lowercase(),
285 None => String::new(),
286 };
287
288 let buf = fs::read(source)?;
290
291 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 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 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 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 pub fn add_spine(&mut self, item: SpineItem) -> &mut Self {
381 self.spine.push(item);
382 self
383 }
384
385 pub fn remove_last_spine(&mut self) -> &mut Self {
387 self.spine.pop();
388 self
389 }
390
391 pub fn take_last_spine(&mut self) -> Option<SpineItem> {
397 self.spine.pop()
398 }
399
400 pub fn clear_spines(&mut self) -> &mut Self {
402 self.spine.clear();
403 self
404 }
405
406 pub fn set_catalog_title(&mut self, title: &str) -> &mut Self {
411 self.catalog_title = title.to_string();
412 self
413 }
414
415 pub fn add_catalog_item(&mut self, item: NavPoint) -> &mut Self {
422 self.catalog.push(item);
423 self
424 }
425
426 pub fn remove_last_catalog_item(&mut self) -> &mut Self {
428 self.catalog.pop();
429 self
430 }
431
432 pub fn take_last_catalog_item(&mut self) -> Option<NavPoint> {
438 self.catalog.pop()
439 }
440
441 pub fn set_catalog(&mut self, catalog: Vec<NavPoint>) -> &mut Self {
448 self.catalog = catalog;
449 self
450 }
451
452 pub fn clear_catalog(&mut self) -> &mut Self {
454 self.catalog.clear();
455 self
456 }
457
458 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 pub fn make<P: AsRef<Path>>(mut self, output_path: P) -> Result<(), EpubError> {
485 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 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 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 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 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 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 manifest.path = PathBuf::from("/").join(manifest.path);
595
596 let (buf, _) = doc.get_manifest_item(&manifest.id)?; 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 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 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 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 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 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 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 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 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 Ok(())
901 }
902 }
903
904 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 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 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 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 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 fn drop(&mut self) {
1020 if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
1021 warn!("{}", err);
1022 };
1023 }
1024}
1025
1026fn 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 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); 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}