1use std::{
41 cmp::Reverse,
42 env,
43 fs::{self, File},
44 io::{BufReader, Cursor, Read, Seek},
45 marker::PhantomData,
46 path::{Path, PathBuf},
47};
48
49use log::warn;
50use quick_xml::{
51 Writer,
52 events::{BytesDecl, BytesEnd, BytesStart, Event},
53};
54use walkdir::WalkDir;
55use zip::{CompressionMethod, ZipWriter, write::FileOptions};
56
57#[cfg(feature = "content-builder")]
58use crate::builder::content::ContentBuilder;
59use crate::{
60 epub::EpubDoc,
61 error::{EpubBuilderError, EpubError},
62 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
63 utils::{check_realtive_link_leakage, local_time, remove_leading_slash},
64};
65
66#[cfg(feature = "content-builder")]
67pub mod content;
68
69pub use components::CatalogBuilder;
70#[cfg(feature = "content-builder")]
71pub use components::DocumentBuilder;
72pub use components::ManifestBuilder;
73pub use components::MetadataBuilder;
74pub use components::RootfileBuilder;
75pub use components::SpineBuilder;
76
77pub(crate) mod components;
78
79type XmlWriter = Writer<Cursor<Vec<u8>>>;
80
81#[cfg_attr(test, derive(Debug))]
83pub struct EpubVersion3;
84
85#[cfg_attr(test, derive(Debug))]
144pub struct EpubBuilder<Version> {
145 epub_version: PhantomData<Version>,
147
148 pub(crate) temp_dir: PathBuf,
150
151 pub(crate) rootfiles: RootfileBuilder,
152 pub(crate) metadata: MetadataBuilder,
153 pub(crate) manifest: ManifestBuilder,
154 pub(crate) spine: SpineBuilder,
155 pub(crate) catalog: CatalogBuilder,
156
157 #[cfg(feature = "content-builder")]
158 pub(crate) content: DocumentBuilder,
159}
160
161impl EpubBuilder<EpubVersion3> {
162 pub fn new() -> Result<Self, EpubError> {
168 let temp_dir = env::temp_dir().join(local_time());
169 fs::create_dir(&temp_dir)?;
170 fs::create_dir(temp_dir.join("META-INF"))?;
171
172 let mime_file = temp_dir.join("mimetype");
173 fs::write(mime_file, "application/epub+zip")?;
174
175 Ok(EpubBuilder {
176 epub_version: PhantomData,
177 temp_dir: temp_dir.clone(),
178
179 rootfiles: RootfileBuilder::new(),
180 metadata: MetadataBuilder::new(),
181 manifest: ManifestBuilder::new(temp_dir),
182 spine: SpineBuilder::new(),
183 catalog: CatalogBuilder::new(),
184
185 #[cfg(feature = "content-builder")]
186 content: DocumentBuilder::new(),
187 })
188 }
189
190 pub fn add_rootfile(&mut self, rootfile: impl AsRef<str>) -> Result<&mut Self, EpubError> {
202 match self.rootfiles.add(rootfile) {
203 Ok(_) => Ok(self),
204 Err(err) => Err(err),
205 }
206 }
207
208 pub fn add_metadata(&mut self, item: MetadataItem) -> &mut Self {
216 let _ = self.metadata.add(item);
217 self
218 }
219
220 pub fn add_manifest(
237 &mut self,
238 manifest_source: impl Into<String>,
239 manifest_item: ManifestItem,
240 ) -> Result<&mut Self, EpubError> {
241 if self.rootfiles.is_empty() {
242 return Err(EpubBuilderError::MissingRootfile.into());
243 } else {
244 self.manifest
245 .set_rootfile(self.rootfiles.first().expect("Unreachable"));
246 }
247
248 match self.manifest.add(manifest_source, manifest_item) {
249 Ok(_) => Ok(self),
250 Err(err) => Err(err),
251 }
252 }
253
254 pub fn add_spine(&mut self, item: SpineItem) -> &mut Self {
261 self.spine.add(item);
262 self
263 }
264
265 pub fn set_catalog_title(&mut self, title: impl Into<String>) -> &mut Self {
270 let _ = self.catalog.set_title(title);
271 self
272 }
273
274 pub fn add_catalog_item(&mut self, item: NavPoint) -> &mut Self {
281 let _ = self.catalog.add(item);
282 self
283 }
284
285 #[cfg(feature = "content-builder")]
294 pub fn add_content(
295 &mut self,
296 target_path: impl AsRef<str>,
297 content: ContentBuilder,
298 ) -> &mut Self {
299 self.content.add(target_path, content);
300 self
301 }
302
303 pub fn clear_all(&mut self) -> &mut Self {
312 self.rootfiles.clear();
313 self.metadata.clear();
314 self.manifest.clear();
315 self.spine.clear();
316 self.catalog.clear();
317 #[cfg(feature = "content-builder")]
318 self.content.clear();
319
320 self
321 }
322
323 pub fn rootfile(&mut self) -> &mut RootfileBuilder {
330 &mut self.rootfiles
331 }
332
333 pub fn metadata(&mut self) -> &mut MetadataBuilder {
340 &mut self.metadata
341 }
342
343 pub fn manifest(&mut self) -> &mut ManifestBuilder {
350 &mut self.manifest
351 }
352
353 pub fn spine(&mut self) -> &mut SpineBuilder {
360 &mut self.spine
361 }
362
363 pub fn catalog(&mut self) -> &mut CatalogBuilder {
370 &mut self.catalog
371 }
372
373 #[cfg(feature = "content-builder")]
380 pub fn content(&mut self) -> &mut DocumentBuilder {
381 &mut self.content
382 }
383
384 pub fn make(mut self, output_path: impl AsRef<Path>) -> Result<(), EpubError> {
393 self.make_container_xml()?;
397 self.make_navigation_document()?;
398 #[cfg(feature = "content-builder")]
399 self.make_contents()?;
400 self.make_opf_file()?;
401 self.remove_empty_dirs()?;
402
403 if let Some(parent) = output_path.as_ref().parent() {
404 if !parent.exists() {
405 fs::create_dir_all(parent)?;
406 }
407 }
408
409 let file = File::create(output_path)?;
411 let mut zip = ZipWriter::new(file);
412 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
413
414 for entry in WalkDir::new(&self.temp_dir) {
415 let entry = entry?;
416 let path = entry.path();
417
418 let relative_path = path.strip_prefix(&self.temp_dir).unwrap();
421 let target_path = relative_path.to_string_lossy().replace("\\", "/");
422
423 if path.is_file() {
424 zip.start_file(target_path, options)?;
425
426 let mut file = File::open(path)?;
427 std::io::copy(&mut file, &mut zip)?;
428 } else if path.is_dir() {
429 zip.add_directory(target_path, options)?;
430 }
431 }
432
433 zip.finish()?;
434 Ok(())
435 }
436
437 pub fn build(
448 self,
449 output_path: impl AsRef<Path>,
450 ) -> Result<EpubDoc<BufReader<File>>, EpubError> {
451 self.make(&output_path)?;
452
453 EpubDoc::new(output_path)
454 }
455
456 pub fn from<R: Read + Seek + Send>(doc: &mut EpubDoc<R>) -> Result<Self, EpubError> {
483 let mut builder = Self::new()?;
484
485 builder.add_rootfile(doc.package_path.clone().to_string_lossy())?;
486 builder.metadata.metadata = doc.metadata.clone();
487 builder.spine.spine = doc.spine.clone();
488 builder.catalog.catalog = doc.catalog.clone();
489 builder.catalog.title = doc.catalog_title.clone();
490
491 for (_, mut manifest) in doc.manifest.clone().into_iter() {
493 if let Some(properties) = &manifest.properties {
494 if properties.contains("nav") {
495 continue;
496 }
497 }
498
499 manifest.path = PathBuf::from("/").join(manifest.path);
503
504 let (buf, _) = doc.get_manifest_item(&manifest.id)?; let target_path = normalize_manifest_path(
506 &builder.temp_dir,
507 builder.rootfiles.first().expect("Unreachable"),
508 &manifest.path,
509 &manifest.id,
510 )?;
511 if let Some(parent_dir) = target_path.parent() {
512 if !parent_dir.exists() {
513 fs::create_dir_all(parent_dir)?
514 }
515 }
516
517 fs::write(target_path, buf)?;
518 builder
519 .manifest
520 .manifest
521 .insert(manifest.id.clone(), manifest);
522 }
523
524 Ok(builder)
525 }
526
527 fn make_container_xml(&self) -> Result<(), EpubError> {
531 if self.rootfiles.is_empty() {
532 return Err(EpubBuilderError::MissingRootfile.into());
533 }
534
535 let mut writer = Writer::new(Cursor::new(Vec::new()));
536 self.rootfiles.make(&mut writer)?;
537
538 let file_path = self.temp_dir.join("META-INF").join("container.xml");
539 let file_data = writer.into_inner().into_inner();
540 fs::write(file_path, file_data)?;
541
542 Ok(())
543 }
544
545 #[cfg(feature = "content-builder")]
547 fn make_contents(&mut self) -> Result<(), EpubError> {
548 let manifest_list = self.content.make(
549 self.temp_dir.clone(),
550 self.rootfiles.first().expect("Unreachable"),
551 )?;
552
553 for item in manifest_list.into_iter() {
554 self.manifest.insert(item.id.clone(), item);
555 }
556
557 Ok(())
558 }
559
560 fn make_navigation_document(&mut self) -> Result<(), EpubError> {
568 for (_, manifest_item) in &self.manifest().manifest {
569 if let Some(properties) = &manifest_item.properties
570 && properties.split(' ').any(|property| property == "nav")
571 {
572 return Ok(());
573 }
574 }
575
576 if self.catalog.is_empty() {
577 return Err(EpubBuilderError::NavigationInfoUninitalized.into());
578 }
579
580 let mut writer = Writer::new(Cursor::new(Vec::new()));
581 self.catalog.make(&mut writer)?;
582
583 let file_path = self.temp_dir.join("nav.xhtml");
584 let file_data = writer.into_inner().into_inner();
585 fs::write(file_path, file_data)?;
586
587 self.manifest.insert(
588 "nav".to_string(),
589 ManifestItem {
590 id: "nav".to_string(),
591 path: PathBuf::from("/nav.xhtml"),
592 mime: "application/xhtml+xml".to_string(),
593 properties: Some("nav".to_string()),
594 fallback: None,
595 },
596 );
597
598 Ok(())
599 }
600
601 fn make_opf_file(&mut self) -> Result<(), EpubError> {
608 self.metadata.validate()?;
609 self.manifest.validate()?;
610 self.spine.validate(self.manifest.keys())?;
611
612 let mut writer = Writer::new(Cursor::new(Vec::new()));
613
614 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
615
616 writer.write_event(Event::Start(BytesStart::new("package").with_attributes([
617 ("xmlns", "http://www.idpf.org/2007/opf"),
618 ("xmlns:dc", "http://purl.org/dc/elements/1.1/"),
619 ("unique-identifier", "pub-id"),
620 ("version", "3.0"),
621 ])))?;
622
623 self.metadata.make(&mut writer)?;
624 self.manifest.make(&mut writer)?;
625 self.spine.make(&mut writer)?;
626
627 writer.write_event(Event::End(BytesEnd::new("package")))?;
628
629 let file_path = self
630 .temp_dir
631 .join(self.rootfiles.first().expect("Unreachable"));
632 let file_data = writer.into_inner().into_inner();
633 fs::write(file_path, file_data)?;
634
635 Ok(())
636 }
637
638 fn remove_empty_dirs(&self) -> Result<(), EpubError> {
649 let mut dirs = WalkDir::new(self.temp_dir.as_path())
650 .min_depth(1)
651 .into_iter()
652 .filter_map(|entry| entry.ok())
653 .filter(|entry| entry.file_type().is_dir())
654 .map(|entry| entry.into_path())
655 .collect::<Vec<PathBuf>>();
656
657 dirs.sort_by_key(|p| Reverse(p.components().count()));
658
659 for dir in dirs {
660 if fs::read_dir(&dir)?.next().is_none() {
661 fs::remove_dir(dir)?;
662 }
663 }
664
665 Ok(())
666 }
667}
668
669impl<Version> Drop for EpubBuilder<Version> {
670 fn drop(&mut self) {
672 if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
673 warn!("{}", err);
674 };
675 }
676}
677
678fn refine_mime_type<'a>(infer_mime: &'a str, extension: &'a str) -> &'a str {
684 match (infer_mime, extension) {
685 ("text/xml", "xhtml")
686 | ("application/xml", "xhtml")
687 | ("text/xml", "xht")
688 | ("application/xml", "xht") => "application/xhtml+xml",
689
690 ("text/xml", "opf") | ("application/xml", "opf") => "application/oebps-package+xml",
691
692 ("text/xml", "ncx") | ("application/xml", "ncx") => "application/x-dtbncx+xml",
693
694 ("application/zip", "epub") => "application/epub+zip",
695
696 ("text/plain", "css") => "text/css",
697 ("text/plain", "js") => "application/javascript",
698 ("text/plain", "json") => "application/json",
699 ("text/plain", "svg") => "image/svg+xml",
700
701 _ => infer_mime,
702 }
703}
704
705fn normalize_manifest_path<TempD: AsRef<Path>, S: AsRef<str>, P: AsRef<Path>>(
727 temp_dir: TempD,
728 rootfile: S,
729 path: P,
730 id: &str,
731) -> Result<PathBuf, EpubError> {
732 let opf_path = PathBuf::from(rootfile.as_ref());
733 let basic_path = remove_leading_slash(opf_path.parent().unwrap());
734
735 let target_path = if path.as_ref().starts_with("../") {
737 check_realtive_link_leakage(
738 temp_dir.as_ref().to_path_buf(),
739 basic_path.to_path_buf(),
740 &path.as_ref().to_string_lossy(),
741 )
742 .map(PathBuf::from)
743 .ok_or_else(|| EpubError::RelativeLinkLeakage {
744 path: path.as_ref().to_string_lossy().to_string(),
745 })?
746 } else if let Ok(stripped) = path.as_ref().strip_prefix("/") {
747 temp_dir.as_ref().join(stripped)
748 } else if path.as_ref().starts_with("./") {
749 Err(EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() })?
751 } else {
752 temp_dir.as_ref().join(basic_path).join(path)
753 };
754
755 #[cfg(windows)]
756 let target_path = PathBuf::from(target_path.to_string_lossy().replace('\\', "/"));
757
758 Ok(target_path)
759}
760
761#[cfg(test)]
762mod tests {
763 use std::{env, fs, path::PathBuf};
764
765 use crate::{
766 builder::{EpubBuilder, EpubVersion3, normalize_manifest_path, refine_mime_type},
767 epub::EpubDoc,
768 error::{EpubBuilderError, EpubError},
769 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
770 utils::local_time,
771 };
772
773 mod test_helpers {
774 use super::*;
775
776 pub(super) fn create_basic_builder() -> EpubBuilder<EpubVersion3> {
777 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
778 builder.add_rootfile("content.opf").unwrap();
779 builder.add_metadata(MetadataItem::new("title", "Test Book"));
780 builder.add_metadata(MetadataItem::new("language", "en"));
781 builder.add_metadata(
782 MetadataItem::new("identifier", "urn:isbn:1234567890")
783 .with_id("pub-id")
784 .build(),
785 );
786 builder
787 }
788
789 pub(super) fn create_full_builder() -> EpubBuilder<EpubVersion3> {
790 let mut builder = create_basic_builder();
791 builder.add_catalog_item(NavPoint::new("Chapter"));
792 builder.add_spine(SpineItem::new("test"));
793 builder
794 }
795 }
796
797 mod epub_builder_tests {
798 use super::*;
799
800 #[test]
801 fn test_epub_builder_new() {
802 let builder = EpubBuilder::<EpubVersion3>::new().expect("Failed to create builder");
803 assert!(builder.temp_dir.exists());
804 assert!(builder.rootfiles.is_empty());
805 assert!(builder.metadata.metadata.is_empty());
806 assert!(builder.manifest.manifest.is_empty());
807 assert!(builder.spine.spine.is_empty());
808 assert!(builder.catalog.title.is_empty());
809 assert!(builder.catalog.is_empty());
810 }
811
812 #[test]
813 fn test_add_rootfile() {
814 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
815
816 builder
817 .add_rootfile("content.opf")
818 .expect("Failed to add rootfile");
819 assert_eq!(builder.rootfiles.rootfiles.len(), 1);
820 assert_eq!(builder.rootfiles.rootfiles[0], "content.opf");
821
822 builder
823 .add_rootfile("./another.opf")
824 .expect("Failed to add another rootfile");
825 assert_eq!(builder.rootfiles.rootfiles.len(), 2);
826 assert_eq!(
827 builder.rootfiles.rootfiles,
828 vec!["content.opf", "another.opf"]
829 );
830 }
831
832 #[test]
833 fn test_add_rootfile_fail() {
834 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
835
836 let result = builder.add_rootfile("/rootfile.opf");
837 assert!(result.is_err());
838 assert_eq!(
839 result.unwrap_err(),
840 EpubBuilderError::IllegalRootfilePath.into()
841 );
842
843 let result = builder.add_rootfile("../rootfile.opf");
844 assert!(result.is_err());
845 assert_eq!(
846 result.unwrap_err(),
847 EpubBuilderError::IllegalRootfilePath.into()
848 );
849 }
850
851 #[test]
852 fn test_add_metadata() {
853 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
854 let metadata_item = MetadataItem::new("title", "Test Book");
855
856 builder.add_metadata(metadata_item);
857
858 assert_eq!(builder.metadata.metadata.len(), 1);
859 assert_eq!(builder.metadata.metadata[0].property, "title");
860 assert_eq!(builder.metadata.metadata[0].value, "Test Book");
861 }
862
863 #[test]
864 fn test_add_spine() {
865 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
866 let spine_item = SpineItem::new("test_item");
867
868 builder.add_spine(spine_item);
869
870 assert_eq!(builder.spine.spine.len(), 1);
871 assert_eq!(builder.spine.spine[0].idref, "test_item");
872 }
873
874 #[test]
875 fn test_set_catalog_title() {
876 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
877 let title = "Test Catalog Title";
878
879 builder.set_catalog_title(title);
880
881 assert_eq!(builder.catalog.title, title);
882 }
883
884 #[test]
885 fn test_add_catalog_item() {
886 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
887 let nav_point = NavPoint::new("Chapter 1");
888
889 builder.add_catalog_item(nav_point);
890
891 assert_eq!(builder.catalog.catalog.len(), 1);
892 assert_eq!(builder.catalog.catalog[0].label, "Chapter 1");
893 }
894
895 #[test]
896 fn test_clear_all() {
897 let mut builder = test_helpers::create_full_builder();
898
899 assert_eq!(builder.metadata.metadata.len(), 3);
900 assert_eq!(builder.spine.spine.len(), 1);
901 assert_eq!(builder.catalog.catalog.len(), 1);
902
903 builder.clear_all();
904
905 assert!(builder.metadata.metadata.is_empty());
906 assert!(builder.spine.spine.is_empty());
907 assert!(builder.catalog.catalog.is_empty());
908 assert!(builder.catalog.title.is_empty());
909 assert!(builder.manifest.manifest.is_empty());
910
911 builder.add_metadata(MetadataItem::new("title", "New Book"));
912 builder.add_spine(SpineItem::new("new_chapter"));
913 builder.add_catalog_item(NavPoint::new("New Chapter"));
914
915 assert_eq!(builder.metadata.metadata.len(), 1);
916 assert_eq!(builder.spine.spine.len(), 1);
917 assert_eq!(builder.catalog.catalog.len(), 1);
918 }
919
920 #[test]
921 fn test_make() {
922 let mut builder = test_helpers::create_full_builder();
923
924 builder
925 .add_manifest(
926 "./test_case/Overview.xhtml",
927 ManifestItem {
928 id: "test".to_string(),
929 path: PathBuf::from("test.xhtml"),
930 mime: String::new(),
931 properties: None,
932 fallback: None,
933 },
934 )
935 .unwrap();
936
937 let file = env::temp_dir().join(format!("{}.epub", local_time()));
938 assert!(builder.make(&file).is_ok());
939 assert!(EpubDoc::new(&file).is_ok());
940 }
941
942 #[test]
943 fn test_build() {
944 let mut builder = test_helpers::create_full_builder();
945
946 builder
947 .add_manifest(
948 "./test_case/Overview.xhtml",
949 ManifestItem {
950 id: "test".to_string(),
951 path: PathBuf::from("test.xhtml"),
952 mime: String::new(),
953 properties: None,
954 fallback: None,
955 },
956 )
957 .unwrap();
958
959 let file = env::temp_dir().join(format!("{}.epub", local_time()));
960 assert!(builder.build(&file).is_ok());
961 }
962
963 #[test]
964 fn test_from() {
965 let metadata = vec![
966 MetadataItem {
967 id: None,
968 property: "title".to_string(),
969 value: "Test Book".to_string(),
970 lang: None,
971 refined: vec![],
972 },
973 MetadataItem {
974 id: None,
975 property: "language".to_string(),
976 value: "en".to_string(),
977 lang: None,
978 refined: vec![],
979 },
980 MetadataItem {
981 id: Some("pub-id".to_string()),
982 property: "identifier".to_string(),
983 value: "test-book".to_string(),
984 lang: None,
985 refined: vec![],
986 },
987 ];
988 let spine = vec![SpineItem {
989 id: None,
990 idref: "main".to_string(),
991 linear: true,
992 properties: None,
993 }];
994 let catalog = vec![
995 NavPoint {
996 label: "Nav".to_string(),
997 content: None,
998 children: vec![],
999 play_order: None,
1000 },
1001 NavPoint {
1002 label: "Overview".to_string(),
1003 content: None,
1004 children: vec![],
1005 play_order: None,
1006 },
1007 ];
1008
1009 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1010 builder.add_rootfile("content.opf").unwrap();
1011 builder.metadata.metadata = metadata.clone();
1012 builder.spine.spine = spine.clone();
1013 builder.catalog.catalog = catalog.clone();
1014 builder.set_catalog_title("catalog title");
1015 builder
1016 .add_manifest(
1017 "./test_case/Overview.xhtml",
1018 ManifestItem {
1019 id: "main".to_string(),
1020 path: PathBuf::from("Overview.xhtml"),
1021 mime: String::new(),
1022 properties: None,
1023 fallback: None,
1024 },
1025 )
1026 .unwrap();
1027
1028 let epub_file = env::temp_dir().join(format!("{}.epub", local_time()));
1029 builder.make(&epub_file).unwrap();
1030
1031 let mut doc = EpubDoc::new(&epub_file).unwrap();
1032 let builder = EpubBuilder::from(&mut doc).unwrap();
1033
1034 assert_eq!(builder.metadata.metadata.len(), metadata.len() + 1);
1035 assert_eq!(builder.manifest.manifest.len(), 1);
1036 assert_eq!(builder.spine.spine.len(), spine.len());
1037 assert_eq!(builder.catalog.catalog, catalog);
1038 assert_eq!(builder.catalog.title, "catalog title");
1039 }
1040
1041 #[test]
1042 fn test_make_container_file() {
1043 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1044
1045 let result = builder.make_container_xml();
1046 assert!(result.is_err());
1047 assert_eq!(
1048 result.unwrap_err(),
1049 EpubBuilderError::MissingRootfile.into()
1050 );
1051
1052 builder.add_rootfile("content.opf").unwrap();
1053 assert!(builder.make_container_xml().is_ok());
1054 }
1055
1056 #[test]
1057 fn test_make_navigation_document() {
1058 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1059
1060 let result = builder.make_navigation_document();
1061 assert!(result.is_err());
1062 assert_eq!(
1063 result.unwrap_err(),
1064 EpubBuilderError::NavigationInfoUninitalized.into()
1065 );
1066
1067 builder.add_catalog_item(NavPoint::new("test"));
1068 assert!(builder.make_navigation_document().is_ok());
1069 }
1070
1071 #[test]
1072 fn test_make_navigation_document_skips_with_existing_nav() {
1073 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1074
1075 builder.manifest.manifest.insert(
1076 "custom-nav".to_string(),
1077 ManifestItem::new("custom-nav", "nav.xhtml")
1078 .unwrap()
1079 .append_property("nav")
1080 .build(),
1081 );
1082
1083 assert!(builder.make_navigation_document().is_ok());
1084
1085 assert!(!builder.temp_dir.join("nav.xhtml").exists());
1086 assert!(!builder.manifest.manifest.contains_key("nav"));
1087 assert_eq!(builder.manifest.manifest.len(), 1);
1088 assert_eq!(
1089 builder
1090 .manifest
1091 .manifest
1092 .get("custom-nav")
1093 .unwrap()
1094 .properties,
1095 Some("nav".to_string())
1096 );
1097 }
1098
1099 #[test]
1100 fn test_make_navigation_document_skips_with_multiple_properties() {
1101 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1102
1103 builder.manifest.manifest.insert(
1104 "custom-nav".to_string(),
1105 ManifestItem::new("custom-nav", "nav.xhtml")
1106 .unwrap()
1107 .append_property("cover-image")
1108 .append_property("nav")
1109 .build(),
1110 );
1111
1112 assert!(builder.make_navigation_document().is_ok());
1113 assert!(!builder.temp_dir.join("nav.xhtml").exists());
1114 }
1115
1116 #[test]
1117 fn test_make_navigation_document_does_not_match_substring() {
1118 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1119
1120 builder.manifest.manifest.insert(
1121 "custom-nav".to_string(),
1122 ManifestItem::new("custom-nav", "nav.xhtml")
1123 .unwrap()
1124 .append_property("navigation")
1125 .build(),
1126 );
1127
1128 assert_eq!(
1129 builder.make_navigation_document().unwrap_err(),
1130 EpubBuilderError::NavigationInfoUninitalized.into()
1131 );
1132
1133 builder.add_catalog_item(NavPoint::new("test"));
1134 assert!(builder.make_navigation_document().is_ok());
1135 assert!(builder.temp_dir.join("nav.xhtml").exists());
1136 assert!(builder.manifest.manifest.contains_key("nav"));
1137 }
1138
1139 #[test]
1140 fn test_make_navigation_document_multiple_nav_items_deferred_to_validation() {
1141 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1142
1143 builder.manifest.manifest.insert(
1144 "nav1".to_string(),
1145 ManifestItem::new("nav1", "nav1.xhtml")
1146 .unwrap()
1147 .append_property("nav")
1148 .build(),
1149 );
1150 builder.manifest.manifest.insert(
1151 "nav2".to_string(),
1152 ManifestItem::new("nav2", "nav2.xhtml")
1153 .unwrap()
1154 .append_property("nav")
1155 .build(),
1156 );
1157
1158 assert!(builder.make_navigation_document().is_ok());
1159
1160 let result = builder.manifest.validate();
1161 assert!(result.is_err());
1162 assert_eq!(
1163 result.unwrap_err().to_string(),
1164 "Epub builder error: There are too many items with 'nav' property in the manifest."
1165 );
1166 }
1167
1168 #[test]
1169 fn test_make_with_custom_nav_document() {
1170 let mut builder = test_helpers::create_full_builder();
1171
1172 builder
1173 .add_manifest(
1174 "./test_case/Overview.xhtml",
1175 ManifestItem {
1176 id: "test".to_string(),
1177 path: PathBuf::from("test.xhtml"),
1178 mime: String::new(),
1179 properties: None,
1180 fallback: None,
1181 },
1182 )
1183 .unwrap();
1184
1185 builder
1186 .add_manifest(
1187 "./test_case/nav.xhtml",
1188 ManifestItem::new("custom-nav", "nav.xhtml")
1189 .unwrap()
1190 .append_property("nav")
1191 .build(),
1192 )
1193 .unwrap();
1194
1195 let file = env::temp_dir().join(format!("{}.epub", local_time()));
1196 assert!(builder.make(&file).is_ok());
1197
1198 let doc = EpubDoc::new(&file).unwrap();
1199 assert!(doc.manifest.contains_key("custom-nav"));
1200 assert_eq!(
1201 doc.manifest
1202 .get("custom-nav")
1203 .unwrap()
1204 .properties
1205 .as_deref(),
1206 Some("nav")
1207 );
1208 assert!(!doc.manifest.contains_key("nav"));
1209 assert_eq!(doc.manifest.len(), 2);
1210 }
1211
1212 #[test]
1213 fn test_make_opf_file_success() {
1214 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1215
1216 builder.add_rootfile("content.opf").unwrap();
1217 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1218 builder.add_metadata(MetadataItem::new("language", "en"));
1219 builder.add_metadata(
1220 MetadataItem::new("identifier", "urn:isbn:1234567890")
1221 .with_id("pub-id")
1222 .build(),
1223 );
1224
1225 let test_file = builder.temp_dir.join("test.xhtml");
1226 fs::write(&test_file, "<html></html>").unwrap();
1227 builder
1228 .add_manifest(
1229 test_file.to_str().unwrap(),
1230 ManifestItem::new("test", "test.xhtml").unwrap(),
1231 )
1232 .unwrap();
1233
1234 builder.add_catalog_item(NavPoint::new("Chapter"));
1235 builder.add_spine(SpineItem::new("test"));
1236 builder.make_navigation_document().unwrap();
1237
1238 assert!(builder.make_opf_file().is_ok());
1239 assert!(builder.temp_dir.join("content.opf").exists());
1240 }
1241
1242 #[test]
1243 fn test_make_opf_file_missing_metadata() {
1244 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1245 builder.add_rootfile("content.opf").unwrap();
1246
1247 let result = builder.make_opf_file();
1248 assert!(result.is_err());
1249 assert_eq!(
1250 result.unwrap_err().to_string(),
1251 "Epub builder error: Requires at least one 'title', 'language', and 'identifier' with id 'pub-id'."
1252 );
1253 }
1254 }
1255
1256 mod manifest_tests {
1257 use super::*;
1258
1259 #[test]
1260 fn test_add_manifest_success() {
1261 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1262 builder.add_rootfile("content.opf").unwrap();
1263
1264 let test_file = builder.temp_dir.join("test.xhtml");
1265 fs::write(&test_file, "<html><body>Hello World</body></html>").unwrap();
1266
1267 let manifest_item = ManifestItem::new("test", "/epub/test.xhtml").unwrap();
1268 let result = builder.add_manifest(test_file.to_str().unwrap(), manifest_item);
1269
1270 assert!(result.is_ok(), "Failed to add manifest: {:?}", result.err());
1271 assert_eq!(builder.manifest.manifest.len(), 1);
1272 assert!(builder.manifest.manifest.contains_key("test"));
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 builder.add_rootfile("package.opf").unwrap();
1295 let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item);
1296 assert!(result.is_ok());
1297 }
1298
1299 #[test]
1300 fn test_add_manifest_nonexistent_file() {
1301 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1302 builder.add_rootfile("content.opf").unwrap();
1303
1304 let manifest_item = ManifestItem::new("test", "nonexistent.xhtml").unwrap();
1305 let result = builder.add_manifest("nonexistent.xhtml", manifest_item);
1306
1307 assert!(result.is_err());
1308 assert_eq!(
1309 result.unwrap_err(),
1310 EpubBuilderError::TargetIsNotFile {
1311 target_path: "nonexistent.xhtml".to_string()
1312 }
1313 .into()
1314 );
1315 }
1316
1317 #[test]
1318 fn test_add_manifest_unknown_file_format() {
1319 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1320 builder.add_rootfile("package.opf").unwrap();
1321
1322 let result = builder.add_manifest(
1323 "./test_case/unknown_file_format.xhtml",
1324 ManifestItem {
1325 id: "file".to_string(),
1326 path: PathBuf::from("unknown_file_format.xhtml"),
1327 mime: String::new(),
1328 properties: None,
1329 fallback: None,
1330 },
1331 );
1332
1333 assert!(result.is_err());
1334 assert_eq!(
1335 result.unwrap_err(),
1336 EpubBuilderError::UnknownFileFormat {
1337 file_path: "./test_case/unknown_file_format.xhtml".to_string(),
1338 }
1339 .into()
1340 );
1341 }
1342
1343 #[test]
1344 fn test_validate_fallback_chain_valid() {
1345 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1346
1347 let item3 = ManifestItem::new("item3", "path3").unwrap();
1348 let item2 = ManifestItem::new("item2", "path2")
1349 .unwrap()
1350 .with_fallback("item3")
1351 .build();
1352 let item1 = ManifestItem::new("item1", "path1")
1353 .unwrap()
1354 .with_fallback("item2")
1355 .append_property("nav")
1356 .build();
1357
1358 builder.manifest.insert("item3".to_string(), item3);
1359 builder.manifest.insert("item2".to_string(), item2);
1360 builder.manifest.insert("item1".to_string(), item1);
1361
1362 assert!(builder.manifest.validate().is_ok());
1363 }
1364
1365 #[test]
1366 fn test_validate_fallback_chain_circular_reference() {
1367 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1368
1369 let item2 = ManifestItem::new("item2", "path2")
1370 .unwrap()
1371 .with_fallback("item1")
1372 .build();
1373 let item1 = ManifestItem::new("item1", "path1")
1374 .unwrap()
1375 .with_fallback("item2")
1376 .build();
1377
1378 builder.manifest.insert("item1".to_string(), item1);
1379 builder.manifest.insert("item2".to_string(), item2);
1380
1381 let result = builder.manifest.validate();
1382 assert!(result.is_err());
1383 assert!(result.unwrap_err().to_string().starts_with(
1384 "Epub builder error: Circular reference detected in fallback chain for"
1385 ));
1386 }
1387
1388 #[test]
1389 fn test_validate_fallback_chain_not_found() {
1390 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1391
1392 let item1 = ManifestItem::new("item1", "path1")
1393 .unwrap()
1394 .with_fallback("nonexistent")
1395 .build();
1396
1397 builder.manifest.insert("item1".to_string(), item1);
1398
1399 let result = builder.manifest.validate();
1400 assert!(result.is_err());
1401 assert_eq!(
1402 result.unwrap_err().to_string(),
1403 "Epub builder error: Fallback resource 'nonexistent' does not exist in manifest."
1404 );
1405 }
1406
1407 #[test]
1408 fn test_validate_manifest_nav_single() {
1409 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1410
1411 let nav_item = ManifestItem::new("nav", "nav.xhtml")
1412 .unwrap()
1413 .append_property("nav")
1414 .build();
1415 builder
1416 .manifest
1417 .manifest
1418 .insert("nav".to_string(), nav_item);
1419
1420 assert!(builder.manifest.validate().is_ok());
1421 }
1422
1423 #[test]
1424 fn test_validate_manifest_nav_multiple() {
1425 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1426
1427 let nav_item1 = ManifestItem::new("nav1", "nav1.xhtml")
1428 .unwrap()
1429 .append_property("nav")
1430 .build();
1431 let nav_item2 = ManifestItem::new("nav2", "nav2.xhtml")
1432 .unwrap()
1433 .append_property("nav")
1434 .build();
1435
1436 builder
1437 .manifest
1438 .manifest
1439 .insert("nav1".to_string(), nav_item1);
1440 builder
1441 .manifest
1442 .manifest
1443 .insert("nav2".to_string(), nav_item2);
1444
1445 let result = builder.manifest.validate();
1446 assert!(result.is_err());
1447 assert_eq!(
1448 result.unwrap_err().to_string(),
1449 "Epub builder error: There are too many items with 'nav' property in the manifest."
1450 );
1451 }
1452 }
1453
1454 mod metadata_tests {
1455 use super::*;
1456
1457 #[test]
1458 fn test_validate_metadata_success() {
1459 let builder = test_helpers::create_basic_builder();
1460 assert!(builder.metadata.validate().is_ok());
1461 }
1462
1463 #[test]
1464 fn test_validate_metadata_missing_required() {
1465 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1466 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1467 builder.add_metadata(MetadataItem::new("language", "en"));
1468 assert!(builder.metadata.validate().is_err());
1469 }
1470 }
1471
1472 mod utility_tests {
1473 use super::*;
1474
1475 #[test]
1476 fn test_normalize_manifest_path() {
1477 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1478 builder.add_rootfile("content.opf").unwrap();
1479
1480 let result = normalize_manifest_path(
1481 &builder.temp_dir,
1482 builder.rootfiles.first().unwrap(),
1483 "../../test.xhtml",
1484 "id",
1485 );
1486 assert!(result.is_err());
1487 assert_eq!(
1488 result.unwrap_err(),
1489 EpubError::RelativeLinkLeakage { path: "../../test.xhtml".to_string() }
1490 );
1491
1492 let result = normalize_manifest_path(
1493 &builder.temp_dir,
1494 builder.rootfiles.first().unwrap(),
1495 "/test.xhtml",
1496 "id",
1497 );
1498 assert!(result.is_ok());
1499 assert_eq!(result.unwrap(), builder.temp_dir.join("test.xhtml"));
1500
1501 let result = normalize_manifest_path(
1502 &builder.temp_dir,
1503 builder.rootfiles.first().unwrap(),
1504 "./test.xhtml",
1505 "manifest_id",
1506 );
1507 assert!(result.is_err());
1508 assert_eq!(
1509 result.unwrap_err(),
1510 EpubBuilderError::IllegalManifestPath { manifest_id: "manifest_id".to_string() }
1511 .into(),
1512 );
1513 }
1514
1515 #[test]
1516 fn test_refine_mime_type() {
1517 assert_eq!(
1518 refine_mime_type("text/xml", "xhtml"),
1519 "application/xhtml+xml"
1520 );
1521 assert_eq!(refine_mime_type("text/xml", "xht"), "application/xhtml+xml");
1522 assert_eq!(
1523 refine_mime_type("application/xml", "opf"),
1524 "application/oebps-package+xml"
1525 );
1526 assert_eq!(
1527 refine_mime_type("text/xml", "ncx"),
1528 "application/x-dtbncx+xml"
1529 );
1530 assert_eq!(refine_mime_type("text/plain", "css"), "text/css");
1531 assert_eq!(refine_mime_type("text/plain", "unknown"), "text/plain");
1532 }
1533 }
1534
1535 #[cfg(feature = "content-builder")]
1536 mod content_builder_tests {
1537 use crate::builder::{EpubBuilder, EpubVersion3, content::ContentBuilder};
1538
1539 #[test]
1540 fn test_make_contents_basic() {
1541 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1542 builder.add_rootfile("content.opf").unwrap();
1543
1544 let mut content_builder = ContentBuilder::new("chapter1", "en").unwrap();
1545 content_builder
1546 .set_title("Test Chapter")
1547 .add_text_block("This is a test paragraph.", vec![])
1548 .unwrap();
1549
1550 builder.add_content("OEBPS/chapter1.xhtml", content_builder);
1551
1552 assert!(builder.make_contents().is_ok());
1553 assert!(builder.temp_dir.join("OEBPS/chapter1.xhtml").exists());
1554 }
1555
1556 #[test]
1557 fn test_make_contents_multiple_blocks() {
1558 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1559 builder.add_rootfile("content.opf").unwrap();
1560
1561 let mut content_builder = ContentBuilder::new("chapter2", "zh-CN").unwrap();
1562 content_builder
1563 .set_title("多个区块章节")
1564 .add_text_block("第一段文本。", vec![])
1565 .unwrap()
1566 .add_quote_block("这是一个引用。", vec![])
1567 .unwrap()
1568 .add_title_block("子标题", 2, vec![])
1569 .unwrap()
1570 .add_text_block("最后的文本段落。", vec![])
1571 .unwrap();
1572
1573 builder.add_content("OEBPS/chapter2.xhtml", content_builder);
1574
1575 assert!(builder.make_contents().is_ok());
1576 assert!(builder.temp_dir.join("OEBPS/chapter2.xhtml").exists());
1577 }
1578
1579 #[test]
1580 fn test_make_contents_with_media() {
1581 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1582 builder.add_rootfile("content.opf").unwrap();
1583
1584 let mut content_builder = ContentBuilder::new("chapter3", "en").unwrap();
1585 content_builder
1586 .set_title("Chapter with Media")
1587 .add_text_block("Text before image.", vec![])
1588 .unwrap()
1589 .add_image_block(
1590 std::path::PathBuf::from("./test_case/image.jpg"),
1591 Some("Test Image".to_string()),
1592 Some("Figure 1: A test image".to_string()),
1593 vec![],
1594 )
1595 .unwrap()
1596 .add_text_block("Text after image.", vec![])
1597 .unwrap();
1598
1599 builder.add_content("OEBPS/chapter3.xhtml", content_builder);
1600
1601 assert!(builder.make_contents().is_ok());
1602 assert!(builder.temp_dir.join("OEBPS/chapter3.xhtml").exists());
1603 assert!(builder.temp_dir.join("OEBPS/img/image.jpg").exists());
1604 }
1605
1606 #[test]
1607 fn test_make_contents_multiple_documents() {
1608 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1609 builder.add_rootfile("content.opf").unwrap();
1610
1611 for (id, title) in [
1612 ("ch1", "Chapter 1"),
1613 ("ch2", "Chapter 2"),
1614 ("ch3", "Chapter 3"),
1615 ] {
1616 let mut content = ContentBuilder::new(id, "en").unwrap();
1617 content
1618 .set_title(title)
1619 .add_text_block(&format!("Content of {}", title), vec![])
1620 .unwrap();
1621 builder.add_content(format!("OEBPS/{}.xhtml", id), content);
1622 }
1623
1624 assert!(builder.make_contents().is_ok());
1625 assert!(builder.temp_dir.join("OEBPS/ch1.xhtml").exists());
1626 assert!(builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
1627 assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
1628 }
1629
1630 #[test]
1631 fn test_make_contents_different_languages() {
1632 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1633 builder.add_rootfile("content.opf").unwrap();
1634
1635 let langs = [
1636 ("en_ch", "en", "English Chapter"),
1637 ("zh_ch", "zh-CN", "中文章节"),
1638 ("ja_ch", "ja", "日本語の章"),
1639 ];
1640
1641 for (id, lang, title) in langs {
1642 let mut content = ContentBuilder::new(id, lang).unwrap();
1643 content
1644 .set_title(title)
1645 .add_text_block(&format!("Text in {}", lang), vec![])
1646 .unwrap();
1647 builder.add_content(format!("OEBPS/{}_chapter.xhtml", id), content);
1648 }
1649
1650 assert!(builder.make_contents().is_ok());
1651 assert!(builder.temp_dir.join("OEBPS/en_ch_chapter.xhtml").exists());
1652 assert!(builder.temp_dir.join("OEBPS/zh_ch_chapter.xhtml").exists());
1653 assert!(builder.temp_dir.join("OEBPS/ja_ch_chapter.xhtml").exists());
1654 }
1655
1656 #[test]
1657 fn test_make_contents_unique_identifiers() {
1658 use std::path::PathBuf;
1659
1660 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1661 builder.add_rootfile("content.opf").unwrap();
1662
1663 let mut content1 = ContentBuilder::new("unique_id_1", "en").unwrap();
1664 content1.add_text_block("First content", vec![]).unwrap();
1665 builder.add_content("OEBPS/ch1.xhtml", content1);
1666
1667 let mut content2 = ContentBuilder::new("unique_id_2", "en").unwrap();
1668 content2.add_text_block("Second content", vec![]).unwrap();
1669 builder.add_content("OEBPS/ch2.xhtml", content2);
1670
1671 let mut content3 = ContentBuilder::new("unique_id_1", "en").unwrap();
1672 content3
1673 .add_text_block("Duplicate ID content", vec![])
1674 .unwrap();
1675 builder.add_content("OEBPS/ch3.xhtml", content3);
1676
1677 assert!(builder.make_contents().is_ok());
1678 assert!(builder.temp_dir.join("OEBPS/ch1.xhtml").exists());
1679 assert!(builder.temp_dir.join("OEBPS/ch2.xhtml").exists());
1680 assert!(builder.temp_dir.join("OEBPS/ch3.xhtml").exists());
1681
1682 let manifest = builder.manifest.manifest.get("unique_id_1").unwrap();
1683 assert_eq!(manifest.path, PathBuf::from("/OEBPS/ch3.xhtml"));
1684 }
1685
1686 #[test]
1687 fn test_make_contents_complex_structure() {
1688 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1689 builder.add_rootfile("content.opf").unwrap();
1690
1691 let mut content = ContentBuilder::new("complex_ch", "en").unwrap();
1692 content
1693 .set_title("Complex Chapter")
1694 .add_title_block("Section 1", 2, vec![])
1695 .unwrap()
1696 .add_text_block("Introduction text.", vec![])
1697 .unwrap()
1698 .add_quote_block("A wise quote here.", vec![])
1699 .unwrap()
1700 .add_title_block("Section 2", 2, vec![])
1701 .unwrap()
1702 .add_text_block("More content with multiple paragraphs.", vec![])
1703 .unwrap()
1704 .add_text_block("Another paragraph.", vec![])
1705 .unwrap()
1706 .add_title_block("Section 3", 2, vec![])
1707 .unwrap()
1708 .add_quote_block("Another quotation.", vec![])
1709 .unwrap();
1710
1711 builder.add_content("OEBPS/complex_chapter.xhtml", content);
1712
1713 assert!(builder.make_contents().is_ok());
1714 assert!(
1715 builder
1716 .temp_dir
1717 .join("OEBPS/complex_chapter.xhtml")
1718 .exists()
1719 );
1720 }
1721
1722 #[test]
1723 fn test_make_contents_empty_document() {
1724 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1725 builder.add_rootfile("content.opf").unwrap();
1726
1727 let content = ContentBuilder::new("empty_ch", "en").unwrap();
1728 builder.add_content("OEBPS/empty.xhtml", content);
1729
1730 assert!(builder.make_contents().is_ok());
1731 assert!(builder.temp_dir.join("OEBPS/empty.xhtml").exists());
1732 }
1733
1734 #[test]
1735 fn test_make_contents_path_normalization() {
1736 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1737 builder.add_rootfile("OEBPS/content.opf").unwrap();
1738
1739 let mut content = ContentBuilder::new("path_test", "en").unwrap();
1740 content.add_text_block("Path test content", vec![]).unwrap();
1741
1742 builder.add_content("/OEBPS/text/chapter.xhtml", content);
1743
1744 assert!(builder.make_contents().is_ok());
1745 assert!(builder.temp_dir.join("OEBPS/text/chapter.xhtml").exists());
1746 }
1747 }
1748}