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
57#[cfg(feature = "content_builder")]
58use crate::builder::content::ContentBuilder;
59use crate::{
60 epub::EpubDoc,
61 error::{EpubBuilderError, EpubError},
62 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
63 utils::{
64 ELEMENT_IN_DC_NAMESPACE, check_realtive_link_leakage, local_time, remove_leading_slash,
65 },
66};
67
68#[cfg(feature = "content_builder")]
69pub mod content;
70
71type XmlWriter = Writer<Cursor<Vec<u8>>>;
72
73#[cfg_attr(test, derive(Debug))]
75pub struct EpubVersion3;
76
77#[cfg_attr(test, derive(Debug))]
122pub struct EpubBuilder<Version> {
123 epub_version: PhantomData<Version>,
125
126 temp_dir: PathBuf,
128
129 rootfiles: Vec<String>,
131
132 metadata: Vec<MetadataItem>,
134
135 manifest: HashMap<String, ManifestItem>,
137
138 spine: Vec<SpineItem>,
140
141 catalog_title: String,
142
143 catalog: Vec<NavPoint>,
145
146 #[cfg(feature = "content_builder")]
148 content: Vec<(PathBuf, ContentBuilder)>,
149}
150
151impl EpubBuilder<EpubVersion3> {
152 pub fn new() -> Result<Self, EpubError> {
158 let temp_dir = env::temp_dir().join(local_time());
159 fs::create_dir(&temp_dir)?;
160 fs::create_dir(temp_dir.join("META-INF"))?;
161
162 let mime_file = temp_dir.join("mimetype");
163 fs::write(mime_file, "application/epub+zip")?;
164
165 Ok(EpubBuilder {
166 epub_version: PhantomData,
167 temp_dir,
168
169 rootfiles: vec![],
170 metadata: vec![],
171 manifest: HashMap::new(),
172 spine: vec![],
173
174 catalog_title: String::new(),
175 catalog: vec![],
176
177 #[cfg(feature = "content_builder")]
178 content: vec![],
179 })
180 }
181
182 pub fn add_rootfile(&mut self, rootfile: &str) -> Result<&mut Self, EpubError> {
194 let rootfile = if rootfile.starts_with("/") || rootfile.starts_with("../") {
195 return Err(EpubBuilderError::IllegalRootfilePath.into());
196 } else if let Some(rootfile) = rootfile.strip_prefix("./") {
197 rootfile
198 } else {
199 rootfile
200 };
201
202 self.rootfiles.push(rootfile.to_string());
203
204 Ok(self)
205 }
206
207 pub fn remove_last_rootfile(&mut self) -> &mut Self {
209 self.rootfiles.pop();
210 self
211 }
212
213 pub fn take_last_rootfile(&mut self) -> Option<String> {
219 self.rootfiles.pop()
220 }
221
222 pub fn clear_rootfiles(&mut self) -> &mut Self {
224 self.rootfiles.clear();
225 self
226 }
227
228 pub fn add_metadata(&mut self, item: MetadataItem) -> &mut Self {
236 self.metadata.push(item);
237 self
238 }
239
240 pub fn remove_last_metadata(&mut self) -> &mut Self {
242 self.metadata.pop();
243 self
244 }
245
246 pub fn take_last_metadata(&mut self) -> Option<MetadataItem> {
252 self.metadata.pop()
253 }
254
255 pub fn clear_metadatas(&mut self) -> &mut Self {
257 self.metadata.clear();
258 self
259 }
260
261 pub fn add_manifest(
278 &mut self,
279 manifest_source: &str,
280 manifest_item: ManifestItem,
281 ) -> Result<&mut Self, EpubError> {
282 if self.rootfiles.is_empty() {
283 return Err(EpubBuilderError::MissingRootfile.into());
284 }
285
286 let source = PathBuf::from(manifest_source);
288 if !source.is_file() {
289 return Err(EpubBuilderError::TargetIsNotFile {
290 target_path: manifest_source.to_string(),
291 }
292 .into());
293 }
294
295 let extension = match source.extension() {
297 Some(ext) => ext.to_string_lossy().to_lowercase(),
298 None => String::new(),
299 };
300
301 let buf = fs::read(source)?;
303
304 let real_mime = match Infer::new().get(&buf) {
306 Some(infer_mime) => refine_mime_type(infer_mime.mime_type(), &extension),
307 None => {
308 return Err(EpubBuilderError::UnknownFileFormat {
309 file_path: manifest_source.to_string(),
310 }
311 .into());
312 }
313 };
314
315 let target_path = self.normalize_manifest_path(&manifest_item.path, &manifest_item.id)?;
316 if let Some(parent_dir) = target_path.parent() {
317 if !parent_dir.exists() {
318 fs::create_dir_all(parent_dir)?
319 }
320 }
321
322 match fs::write(target_path, buf) {
323 Ok(_) => {
324 self.manifest
325 .insert(manifest_item.id.clone(), manifest_item.set_mime(&real_mime));
326 Ok(self)
327 }
328 Err(err) => Err(err.into()),
329 }
330 }
331
332 pub fn remove_manifest(&mut self, id: &str) -> Result<&mut Self, EpubError> {
344 if let Some(manifest) = self.manifest.remove(id) {
345 let target_path = self.normalize_manifest_path(&manifest.path, &manifest.id)?;
346 fs::remove_file(target_path)?;
347 }
348
349 Ok(self)
350 }
351
352 pub fn take_manifest(&mut self, id: &str) -> Option<ManifestItem> {
361 if let Some(manifest) = self.manifest.remove(id) {
362 let target_path = self
363 .normalize_manifest_path(&manifest.path, &manifest.id)
364 .ok()?;
365 fs::remove_file(target_path).ok()?;
366
367 return Some(manifest);
368 }
369
370 None
371 }
372
373 pub fn clear_manifests(&mut self) -> Result<&mut Self, EpubError> {
379 let keys = self.manifest.keys().cloned().collect::<Vec<String>>();
380 for id in keys {
381 self.remove_manifest(&id)?;
382 }
383
384 Ok(self)
385 }
386
387 pub fn add_spine(&mut self, item: SpineItem) -> &mut Self {
394 self.spine.push(item);
395 self
396 }
397
398 pub fn remove_last_spine(&mut self) -> &mut Self {
400 self.spine.pop();
401 self
402 }
403
404 pub fn take_last_spine(&mut self) -> Option<SpineItem> {
410 self.spine.pop()
411 }
412
413 pub fn clear_spines(&mut self) -> &mut Self {
415 self.spine.clear();
416 self
417 }
418
419 pub fn set_catalog_title(&mut self, title: &str) -> &mut Self {
424 self.catalog_title = title.to_string();
425 self
426 }
427
428 pub fn add_catalog_item(&mut self, item: NavPoint) -> &mut Self {
435 self.catalog.push(item);
436 self
437 }
438
439 pub fn remove_last_catalog_item(&mut self) -> &mut Self {
441 self.catalog.pop();
442 self
443 }
444
445 pub fn take_last_catalog_item(&mut self) -> Option<NavPoint> {
451 self.catalog.pop()
452 }
453
454 pub fn set_catalog(&mut self, catalog: Vec<NavPoint>) -> &mut Self {
461 self.catalog = catalog;
462 self
463 }
464
465 pub fn clear_catalog(&mut self) -> &mut Self {
467 self.catalog.clear();
468 self
469 }
470
471 #[cfg(feature = "content_builder")]
480 pub fn add_content(&mut self, target_path: &str, content: ContentBuilder) -> &mut Self {
481 self.content.push((PathBuf::from(target_path), content));
482 self
483 }
484
485 #[cfg(feature = "content_builder")]
487 pub fn remove_last_content(&mut self) -> &mut Self {
488 self.content.pop();
489 self
490 }
491
492 #[cfg(feature = "content_builder")]
498 pub fn take_last_content(&mut self) -> Option<(PathBuf, ContentBuilder)> {
499 self.content.pop()
500 }
501
502 #[cfg(feature = "content_builder")]
504 pub fn clear_contents(&mut self) -> &mut Self {
505 self.content.clear();
506 self
507 }
508
509 pub fn clear_all(&mut self) -> Result<&mut Self, EpubError> {
518 self.catalog_title = String::new();
519 self.clear_metadatas()
520 .clear_manifests()?
521 .clear_spines()
522 .clear_catalog();
523
524 #[cfg(feature = "content_builder")]
525 self.clear_contents();
526
527 Ok(self)
528 }
529
530 pub fn make<P: AsRef<Path>>(mut self, output_path: P) -> Result<(), EpubError> {
539 self.make_container_xml()?;
543 self.make_navigation_document()?;
544 #[cfg(feature = "content_builder")]
545 self.make_contents()?;
546 self.make_opf_file()?;
547 self.remove_empty_dirs()?;
548
549 if let Some(parent) = output_path.as_ref().parent() {
550 if !parent.exists() {
551 fs::create_dir_all(parent)?;
552 }
553 }
554
555 let file = File::create(output_path)?;
557 let mut zip = ZipWriter::new(file);
558 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
559
560 for entry in WalkDir::new(&self.temp_dir) {
561 let entry = entry?;
562 let path = entry.path();
563
564 let relative_path = path.strip_prefix(&self.temp_dir).unwrap();
567 let target_path = relative_path.to_string_lossy().replace("\\", "/");
568
569 if path.is_file() {
570 zip.start_file(target_path, options)?;
571
572 let mut buf = Vec::new();
573 File::open(path)?.read_to_end(&mut buf)?;
574
575 zip.write_all(&buf)?;
576 } else if path.is_dir() {
577 zip.add_directory(target_path, options)?;
578 }
579 }
580
581 zip.finish()?;
582 Ok(())
583 }
584
585 pub fn build<P: AsRef<Path>>(
596 self,
597 output_path: P,
598 ) -> Result<EpubDoc<BufReader<File>>, EpubError> {
599 self.make(&output_path)?;
600
601 EpubDoc::new(output_path)
602 }
603
604 pub fn from<R: Read + Seek>(doc: &mut EpubDoc<R>) -> Result<Self, EpubError> {
631 let mut builder = Self::new()?;
632
633 builder.add_rootfile(&doc.package_path.clone().to_string_lossy())?;
634 builder.metadata = doc.metadata.clone();
635 builder.spine = doc.spine.clone();
636 builder.catalog = doc.catalog.clone();
637 builder.catalog_title = doc.catalog_title.clone();
638
639 for (_, mut manifest) in doc.manifest.clone().into_iter() {
641 if let Some(properties) = &manifest.properties {
642 if properties.contains("nav") {
643 continue;
644 }
645 }
646
647 manifest.path = PathBuf::from("/").join(manifest.path);
651
652 let (buf, _) = doc.get_manifest_item(&manifest.id)?; let target_path = builder.normalize_manifest_path(&manifest.path, &manifest.id)?;
654 if let Some(parent_dir) = target_path.parent() {
655 if !parent_dir.exists() {
656 fs::create_dir_all(parent_dir)?
657 }
658 }
659
660 fs::write(target_path, buf)?;
661 builder.manifest.insert(manifest.id.clone(), manifest);
662 }
663
664 Ok(builder)
665 }
666
667 fn make_container_xml(&self) -> Result<(), EpubError> {
671 if self.rootfiles.is_empty() {
672 return Err(EpubBuilderError::MissingRootfile.into());
673 }
674
675 let mut writer = Writer::new(Cursor::new(Vec::new()));
676
677 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
678
679 writer.write_event(Event::Start(BytesStart::new("container").with_attributes(
680 [
681 ("version", "1.0"),
682 ("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container"),
683 ],
684 )))?;
685 writer.write_event(Event::Start(BytesStart::new("rootfiles")))?;
686
687 for rootfile in &self.rootfiles {
688 writer.write_event(Event::Empty(BytesStart::new("rootfile").with_attributes([
689 ("full-path", rootfile.as_str()),
690 ("media-type", "application/oebps-package+xml"),
691 ])))?;
692 }
693
694 writer.write_event(Event::End(BytesEnd::new("rootfiles")))?;
695 writer.write_event(Event::End(BytesEnd::new("container")))?;
696
697 let file_path = self.temp_dir.join("META-INF").join("container.xml");
698 let file_data = writer.into_inner().into_inner();
699 fs::write(file_path, file_data)?;
700
701 Ok(())
702 }
703
704 #[cfg(feature = "content_builder")]
706 fn make_contents(&mut self) -> Result<(), EpubError> {
707 let mut buf = vec![0; 512];
708 let contents = std::mem::take(&mut self.content);
709
710 for (target, mut content) in contents.into_iter() {
711 let manifest_id = content.id.clone();
712
713 let absolute_target = self.normalize_manifest_path(&target, &manifest_id)?;
715 let mut resources = content.make(&absolute_target)?;
716
717 let to_container_path = |p: &PathBuf| -> PathBuf {
719 match p.strip_prefix(&self.temp_dir) {
720 Ok(rel) => PathBuf::from("/").join(rel.to_string_lossy().replace("\\", "/")),
721 Err(_) => unreachable!("path MUST under temp directory"),
722 }
723 };
724
725 let path = resources.swap_remove(0);
727 let mut file = std::fs::File::open(&path)?;
728 let _ = file.read(&mut buf)?;
729 let extension = path
730 .extension()
731 .map(|e| e.to_string_lossy().to_lowercase())
732 .unwrap_or_default();
733 let mime = match Infer::new().get(&buf) {
734 Some(infer) => refine_mime_type(infer.mime_type(), &extension),
735 None => {
736 return Err(EpubBuilderError::UnknownFileFormat {
737 file_path: path.to_string_lossy().to_string(),
738 }
739 .into());
740 }
741 };
742
743 self.manifest.insert(
744 manifest_id.clone(),
745 ManifestItem {
746 id: manifest_id.clone(),
747 path: to_container_path(&path),
748 mime,
749 properties: None,
750 fallback: None,
751 },
752 );
753
754 for res in resources {
756 let mut file = fs::File::open(&res)?;
757 let _ = file.read(&mut buf)?;
758 let extension = res
759 .extension()
760 .map(|e| e.to_string_lossy().to_lowercase())
761 .unwrap_or_default();
762 let mime = match Infer::new().get(&buf) {
763 Some(ft) => refine_mime_type(ft.mime_type(), &extension),
764 None => {
765 return Err(EpubBuilderError::UnknownFileFormat {
766 file_path: path.to_string_lossy().to_string(),
767 }
768 .into());
769 }
770 };
771
772 let file_name = res
773 .file_name()
774 .map(|s| s.to_string_lossy().to_string())
775 .unwrap_or_default();
776 let res_id = format!("{}-{}", manifest_id, file_name);
777
778 self.manifest.insert(
779 res_id.clone(),
780 ManifestItem {
781 id: res_id,
782 path: to_container_path(&res),
783 mime,
784 properties: None,
785 fallback: None,
786 },
787 );
788 }
789 }
790
791 Ok(())
792 }
793
794 fn make_navigation_document(&mut self) -> Result<(), EpubError> {
798 if self.catalog.is_empty() {
799 return Err(EpubBuilderError::NavigationInfoUninitalized.into());
800 }
801
802 let mut writer = Writer::new(Cursor::new(Vec::new()));
803
804 writer.write_event(Event::Start(BytesStart::new("html").with_attributes([
805 ("xmlns", "http://www.w3.org/1999/xhtml"),
806 ("xmlns:epub", "http://www.idpf.org/2007/ops"),
807 ])))?;
808
809 writer.write_event(Event::Start(BytesStart::new("head")))?;
811 writer.write_event(Event::Start(BytesStart::new("title")))?;
812 writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
813 writer.write_event(Event::End(BytesEnd::new("title")))?;
814 writer.write_event(Event::End(BytesEnd::new("head")))?;
815
816 writer.write_event(Event::Start(BytesStart::new("body")))?;
818 writer.write_event(Event::Start(
819 BytesStart::new("nav").with_attributes([("epub:type", "toc")]),
820 ))?;
821
822 if !self.catalog_title.is_empty() {
823 writer.write_event(Event::Start(BytesStart::new("h1")))?;
824 writer.write_event(Event::Text(BytesText::new(&self.catalog_title)))?;
825 writer.write_event(Event::End(BytesEnd::new("h1")))?;
826 }
827
828 Self::make_nav(&mut writer, &self.catalog)?;
829
830 writer.write_event(Event::End(BytesEnd::new("nav")))?;
831 writer.write_event(Event::End(BytesEnd::new("body")))?;
832
833 writer.write_event(Event::End(BytesEnd::new("html")))?;
834
835 let file_path = self.temp_dir.join("nav.xhtml");
836 let file_data = writer.into_inner().into_inner();
837 fs::write(file_path, file_data)?;
838
839 self.manifest.insert(
840 "nav".to_string(),
841 ManifestItem {
842 id: "nav".to_string(),
843 path: PathBuf::from("/nav.xhtml"),
844 mime: "application/xhtml+xml".to_string(),
845 properties: Some("nav".to_string()),
846 fallback: None,
847 },
848 );
849
850 Ok(())
851 }
852
853 fn make_opf_file(&mut self) -> Result<(), EpubError> {
860 if !self.validate_metadata() {
861 return Err(EpubBuilderError::MissingNecessaryMetadata.into());
862 }
863 self.validate_manifest_fallback_chains()?;
864 self.validate_manifest_nav()?;
865
866 let mut writer = Writer::new(Cursor::new(Vec::new()));
867
868 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
869
870 writer.write_event(Event::Start(BytesStart::new("package").with_attributes([
871 ("xmlns", "http://www.idpf.org/2007/opf"),
872 ("xmlns:dc", "http://purl.org/dc/elements/1.1/"),
873 ("unique-identifier", "pub-id"),
874 ("version", "3.0"),
875 ])))?;
876
877 self.make_opf_metadata(&mut writer)?;
878 self.make_opf_manifest(&mut writer)?;
879 self.make_opf_spine(&mut writer)?;
880
881 writer.write_event(Event::End(BytesEnd::new("package")))?;
882
883 let file_path = self.temp_dir.join(&self.rootfiles[0]);
884 let file_data = writer.into_inner().into_inner();
885 fs::write(file_path, file_data)?;
886
887 Ok(())
888 }
889
890 fn make_opf_metadata(&mut self, writer: &mut XmlWriter) -> Result<(), EpubError> {
891 self.metadata.push(MetadataItem {
892 id: None,
893 property: "dcterms:modified".to_string(),
894 value: Utc::now().to_rfc3339_opts(SecondsFormat::AutoSi, true),
895 lang: None,
896 refined: vec![],
897 });
898
899 writer.write_event(Event::Start(BytesStart::new("metadata")))?;
900
901 for metadata in &self.metadata {
902 let tag_name = if ELEMENT_IN_DC_NAMESPACE.contains(&metadata.property.as_str()) {
903 format!("dc:{}", metadata.property)
904 } else {
905 "meta".to_string()
906 };
907
908 writer.write_event(Event::Start(
909 BytesStart::new(tag_name.as_str()).with_attributes(metadata.attributes()),
910 ))?;
911 writer.write_event(Event::Text(BytesText::new(metadata.value.as_str())))?;
912 writer.write_event(Event::End(BytesEnd::new(tag_name.as_str())))?;
913
914 for refinement in &metadata.refined {
915 writer.write_event(Event::Start(
916 BytesStart::new("meta").with_attributes(refinement.attributes()),
917 ))?;
918 writer.write_event(Event::Text(BytesText::new(refinement.value.as_str())))?;
919 writer.write_event(Event::End(BytesEnd::new("meta")))?;
920 }
921 }
922
923 writer.write_event(Event::End(BytesEnd::new("metadata")))?;
924
925 Ok(())
926 }
927
928 fn make_opf_manifest(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
929 writer.write_event(Event::Start(BytesStart::new("manifest")))?;
930
931 for manifest in self.manifest.values() {
932 writer.write_event(Event::Empty(
933 BytesStart::new("item").with_attributes(manifest.attributes()),
934 ))?;
935 }
936
937 writer.write_event(Event::End(BytesEnd::new("manifest")))?;
938
939 Ok(())
940 }
941
942 fn make_opf_spine(&self, writer: &mut XmlWriter) -> Result<(), EpubError> {
943 writer.write_event(Event::Start(BytesStart::new("spine")))?;
944
945 for spine in &self.spine {
946 writer.write_event(Event::Empty(
947 BytesStart::new("itemref").with_attributes(spine.attributes()),
948 ))?;
949 }
950
951 writer.write_event(Event::End(BytesEnd::new("spine")))?;
952
953 Ok(())
954 }
955
956 fn make_nav(writer: &mut XmlWriter, navgations: &Vec<NavPoint>) -> Result<(), EpubError> {
957 writer.write_event(Event::Start(BytesStart::new("ol")))?;
958
959 for nav in navgations {
960 writer.write_event(Event::Start(BytesStart::new("li")))?;
961
962 if let Some(path) = &nav.content {
963 writer.write_event(Event::Start(
964 BytesStart::new("a").with_attributes([("href", path.to_string_lossy())]),
965 ))?;
966 writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
967 writer.write_event(Event::End(BytesEnd::new("a")))?;
968 } else {
969 writer.write_event(Event::Start(BytesStart::new("span")))?;
970 writer.write_event(Event::Text(BytesText::new(nav.label.as_str())))?;
971 writer.write_event(Event::End(BytesEnd::new("span")))?;
972 }
973
974 if !nav.children.is_empty() {
975 Self::make_nav(writer, &nav.children)?;
976 }
977
978 writer.write_event(Event::End(BytesEnd::new("li")))?;
979 }
980
981 writer.write_event(Event::End(BytesEnd::new("ol")))?;
982
983 Ok(())
984 }
985
986 fn validate_metadata(&self) -> bool {
990 let has_title = self.metadata.iter().any(|item| item.property == "title");
991 let has_language = self.metadata.iter().any(|item| item.property == "language");
992 let has_identifier = self.metadata.iter().any(|item| {
993 item.property == "identifier" && item.id.as_ref().is_some_and(|id| id == "pub-id")
994 });
995
996 has_title && has_identifier && has_language
997 }
998
999 fn validate_manifest_fallback_chains(&self) -> Result<(), EpubError> {
1000 for (id, item) in &self.manifest {
1001 if item.fallback.is_none() {
1002 continue;
1003 }
1004
1005 let mut fallback_chain = Vec::new();
1006 self.validate_fallback_chain(id, &mut fallback_chain)?;
1007 }
1008
1009 Ok(())
1010 }
1011
1012 fn validate_fallback_chain(
1018 &self,
1019 manifest_id: &str,
1020 fallback_chain: &mut Vec<String>,
1021 ) -> Result<(), EpubError> {
1022 if fallback_chain.contains(&manifest_id.to_string()) {
1023 fallback_chain.push(manifest_id.to_string());
1024
1025 return Err(EpubBuilderError::ManifestCircularReference {
1026 fallback_chain: fallback_chain.join("->"),
1027 }
1028 .into());
1029 }
1030
1031 let item = self.manifest.get(manifest_id).unwrap();
1033
1034 if let Some(fallback_id) = &item.fallback {
1035 if !self.manifest.contains_key(fallback_id) {
1036 return Err(EpubBuilderError::ManifestNotFound {
1037 manifest_id: fallback_id.to_owned(),
1038 }
1039 .into());
1040 }
1041
1042 fallback_chain.push(manifest_id.to_string());
1043 self.validate_fallback_chain(fallback_id, fallback_chain)
1044 } else {
1045 Ok(())
1047 }
1048 }
1049
1050 fn validate_manifest_nav(&self) -> Result<(), EpubError> {
1054 if self
1055 .manifest
1056 .values()
1057 .filter(|&item| {
1058 if let Some(properties) = &item.properties {
1059 properties
1060 .clone()
1061 .split(" ")
1062 .collect::<Vec<&str>>()
1063 .contains(&"nav")
1064 } else {
1065 false
1066 }
1067 })
1068 .count()
1069 == 1
1070 {
1071 Ok(())
1072 } else {
1073 Err(EpubBuilderError::TooManyNavFlags.into())
1074 }
1075 }
1076
1077 fn normalize_manifest_path<P: AsRef<Path>>(
1097 &self,
1098 path: P,
1099 id: &str,
1100 ) -> Result<PathBuf, EpubError> {
1101 let opf_path = PathBuf::from(&self.rootfiles[0]);
1102 let basic_path = remove_leading_slash(opf_path.parent().unwrap());
1103
1104 let mut target_path = if path.as_ref().starts_with("../") {
1106 check_realtive_link_leakage(
1107 self.temp_dir.clone(),
1108 basic_path.to_path_buf(),
1109 &path.as_ref().to_string_lossy(),
1110 )
1111 .map(PathBuf::from)
1112 .ok_or_else(|| EpubError::RealtiveLinkLeakage {
1113 path: path.as_ref().to_string_lossy().to_string(),
1114 })?
1115 } else if let Ok(path) = path.as_ref().strip_prefix("/") {
1116 self.temp_dir.join(path)
1117 } else if path.as_ref().starts_with("./") {
1118 Err(EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() })?
1120 } else {
1121 self.temp_dir.join(basic_path).join(path)
1122 };
1123
1124 #[cfg(windows)]
1125 {
1126 target_path = PathBuf::from(target_path.to_string_lossy().replace('\\', "/"));
1127 }
1128
1129 Ok(target_path)
1130 }
1131
1132 fn remove_empty_dirs(&self) -> Result<(), EpubError> {
1143 let mut dirs = WalkDir::new(self.temp_dir.as_path())
1144 .min_depth(1)
1145 .into_iter()
1146 .filter_map(|entry| entry.ok())
1147 .filter(|entry| entry.file_type().is_dir())
1148 .map(|entry| entry.into_path())
1149 .collect::<Vec<PathBuf>>();
1150
1151 dirs.sort_by_key(|p| Reverse(p.components().count()));
1152
1153 for dir in dirs {
1154 if fs::read_dir(&dir)?.next().is_none() {
1155 fs::remove_dir(dir)?;
1156 }
1157 }
1158
1159 Ok(())
1160 }
1161}
1162
1163impl<Version> Drop for EpubBuilder<Version> {
1164 fn drop(&mut self) {
1166 if let Err(err) = fs::remove_dir_all(&self.temp_dir) {
1167 warn!("{}", err);
1168 };
1169 }
1170}
1171
1172fn refine_mime_type(infer_mime: &str, extension: &str) -> String {
1176 match (infer_mime, extension) {
1177 ("text/xml", "xhtml")
1178 | ("application/xml", "xhtml")
1179 | ("text/xml", "xht")
1180 | ("application/xml", "xht") => "application/xhtml+xml".to_string(),
1181
1182 ("text/xml", "opf") | ("application/xml", "opf") => {
1183 "application/oebps-package+xml".to_string()
1184 }
1185
1186 ("text/xml", "ncx") | ("application/xml", "ncx") => "application/x-dtbncx+xml".to_string(),
1187
1188 ("application/zip", "epub") => "application/epub+zip".to_string(),
1189
1190 ("text/plain", "css") => "text/css".to_string(),
1191 ("text/plain", "js") => "application/javascript".to_string(),
1192 ("text/plain", "json") => "application/json".to_string(),
1193 ("text/plain", "svg") => "image/svg+xml".to_string(),
1194
1195 _ => infer_mime.to_string(),
1196 }
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use std::{env, fs, path::PathBuf};
1202
1203 use crate::{
1204 builder::{EpubBuilder, EpubVersion3, refine_mime_type},
1205 epub::EpubDoc,
1206 error::{EpubBuilderError, EpubError},
1207 types::{ManifestItem, MetadataItem, NavPoint, SpineItem},
1208 utils::local_time,
1209 };
1210
1211 #[test]
1212 fn test_epub_builder_new() {
1213 let builder = EpubBuilder::<EpubVersion3>::new();
1214 assert!(builder.is_ok());
1215
1216 let builder = builder.unwrap();
1217 assert!(builder.temp_dir.exists());
1218 assert!(builder.rootfiles.is_empty());
1219 assert!(builder.metadata.is_empty());
1220 assert!(builder.manifest.is_empty());
1221 assert!(builder.spine.is_empty());
1222 assert!(builder.catalog_title.is_empty());
1223 assert!(builder.catalog.is_empty());
1224 }
1225
1226 #[test]
1227 fn test_add_rootfile() {
1228 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1229 assert!(builder.add_rootfile("content.opf").is_ok());
1230
1231 assert_eq!(builder.rootfiles.len(), 1);
1232 assert_eq!(builder.rootfiles[0], "content.opf");
1233
1234 assert!(builder.add_rootfile("./another.opf").is_ok());
1235 assert_eq!(builder.rootfiles.len(), 2);
1236 assert_eq!(builder.rootfiles, vec!["content.opf", "another.opf"]);
1237 }
1238
1239 #[test]
1240 fn test_add_rootfile_fail() {
1241 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1242
1243 let result = builder.add_rootfile("/rootfile.opf");
1244 assert!(result.is_err());
1245 assert_eq!(
1246 result.unwrap_err(),
1247 EpubBuilderError::IllegalRootfilePath.into()
1248 );
1249
1250 let result = builder.add_rootfile("../rootfile.opf");
1251 assert!(result.is_err());
1252 assert_eq!(
1253 result.unwrap_err(),
1254 EpubBuilderError::IllegalRootfilePath.into()
1255 );
1256 }
1257
1258 #[test]
1259 fn test_remove_last_rootfile() {
1260 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1261
1262 assert!(builder.add_rootfile("first.opf").is_ok());
1263 assert!(builder.add_rootfile("second.opf").is_ok());
1264 assert!(builder.add_rootfile("third.opf").is_ok());
1265 assert_eq!(builder.rootfiles.len(), 3);
1266
1267 let result = builder.remove_last_rootfile();
1268 assert_eq!(result.rootfiles.len(), 2);
1269 assert_eq!(builder.rootfiles, vec!["first.opf", "second.opf"]);
1270
1271 builder.remove_last_rootfile();
1272 assert_eq!(builder.rootfiles.len(), 1);
1273 assert_eq!(builder.rootfiles[0], "first.opf");
1274
1275 builder.remove_last_rootfile();
1276 assert!(builder.rootfiles.is_empty());
1277
1278 builder.remove_last_rootfile();
1279 assert!(builder.rootfiles.is_empty());
1280 }
1281
1282 #[test]
1283 fn test_take_last_rootfile() {
1284 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1285
1286 let result = builder.take_last_rootfile();
1287 assert!(result.is_none());
1288
1289 builder.add_rootfile("first.opf").unwrap();
1290 builder.add_rootfile("second.opf").unwrap();
1291 builder.add_rootfile("third.opf").unwrap();
1292 assert_eq!(builder.rootfiles.len(), 3);
1293
1294 let result = builder.take_last_rootfile();
1295 assert!(result.is_some());
1296 assert_eq!(result.unwrap(), "third.opf");
1297 assert_eq!(builder.rootfiles.len(), 2);
1298
1299 let result = builder.take_last_rootfile();
1300 assert_eq!(result.unwrap(), "second.opf");
1301 assert_eq!(builder.rootfiles.len(), 1);
1302
1303 let result = builder.take_last_rootfile();
1304 assert_eq!(result.unwrap(), "first.opf");
1305 assert!(builder.rootfiles.is_empty());
1306
1307 let result = builder.take_last_rootfile();
1308 assert!(result.is_none());
1309 }
1310
1311 #[test]
1312 fn test_clear_rootfiles() {
1313 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1314
1315 builder.clear_rootfiles();
1316 assert!(builder.rootfiles.is_empty());
1317
1318 builder.add_rootfile("first.opf").unwrap();
1319 builder.add_rootfile("second.opf").unwrap();
1320 builder.add_rootfile("third.opf").unwrap();
1321 assert_eq!(builder.rootfiles.len(), 3);
1322
1323 builder.clear_rootfiles();
1324 assert!(builder.rootfiles.is_empty());
1325 assert_eq!(builder.rootfiles.len(), 0);
1326
1327 builder.add_rootfile("new.opf").unwrap();
1328 assert_eq!(builder.rootfiles.len(), 1);
1329 assert_eq!(builder.rootfiles[0], "new.opf");
1330 }
1331
1332 #[test]
1333 fn test_add_metadata() {
1334 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1335 let metadata_item = MetadataItem::new("title", "Test Book");
1336
1337 builder.add_metadata(metadata_item);
1338
1339 assert_eq!(builder.metadata.len(), 1);
1340 assert_eq!(builder.metadata[0].property, "title");
1341 assert_eq!(builder.metadata[0].value, "Test Book");
1342 }
1343
1344 #[test]
1345 fn test_remove_last_metadata() {
1346 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1347 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1348 builder.add_metadata(MetadataItem::new("author", "Test Author"));
1349
1350 assert_eq!(builder.metadata.len(), 2);
1351
1352 builder.remove_last_metadata();
1353
1354 assert_eq!(builder.metadata.len(), 1);
1355 assert_eq!(builder.metadata[0].property, "title");
1356
1357 builder.remove_last_metadata();
1358 builder.remove_last_metadata();
1359 assert_eq!(builder.metadata.len(), 0);
1360 }
1361
1362 #[test]
1363 fn test_take_last_metadata() {
1364 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1365 let metadata1 = MetadataItem::new("title", "Test Book");
1366 let metadata2 = MetadataItem::new("author", "Test Author");
1367
1368 builder.add_metadata(metadata1);
1369 builder.add_metadata(metadata2);
1370 assert_eq!(builder.metadata.len(), 2);
1371
1372 let taken = builder.take_last_metadata();
1373 assert!(taken.is_some());
1374 assert_eq!(taken.unwrap().property, "author");
1375 assert_eq!(builder.metadata.len(), 1);
1376
1377 let _ = builder.take_last_metadata();
1378 let result = builder.take_last_metadata();
1379 assert!(result.is_none());
1380 assert_eq!(builder.metadata.len(), 0);
1381 }
1382
1383 #[test]
1384 fn test_clear_metadatas() {
1385 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1386 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1387 builder.add_metadata(MetadataItem::new("author", "Test Author"));
1388 builder.add_metadata(MetadataItem::new("language", "en"));
1389
1390 assert_eq!(builder.metadata.len(), 3);
1391
1392 builder.clear_metadatas();
1393
1394 assert_eq!(builder.metadata.len(), 0);
1395
1396 builder.clear_metadatas();
1397 assert_eq!(builder.metadata.len(), 0);
1398 }
1399
1400 #[test]
1401 fn test_add_manifest_success() {
1402 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1403 assert!(builder.add_rootfile("content.opf").is_ok());
1404
1405 let temp_dir = env::temp_dir().join(local_time());
1407 fs::create_dir_all(&temp_dir).unwrap();
1408 let test_file = temp_dir.join("test.xhtml");
1409 fs::write(&test_file, "<html><body>Hello World</body></html>").unwrap();
1410
1411 let manifest_item = ManifestItem::new("test", "/epub/test.xhtml").unwrap();
1412 let result = builder.add_manifest(test_file.to_str().unwrap(), manifest_item);
1413
1414 assert!(result.is_ok());
1415 assert_eq!(builder.manifest.len(), 1);
1416 assert!(builder.manifest.contains_key("test"));
1417
1418 fs::remove_dir_all(temp_dir).unwrap();
1419 }
1420
1421 #[test]
1422 fn test_add_manifest_no_rootfile() {
1423 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1424
1425 let manifest_item = ManifestItem {
1426 id: "main".to_string(),
1427 path: PathBuf::from("/Overview.xhtml"),
1428 mime: String::new(),
1429 properties: None,
1430 fallback: None,
1431 };
1432
1433 let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item.clone());
1434 assert!(result.is_err());
1435 assert_eq!(
1436 result.unwrap_err(),
1437 EpubBuilderError::MissingRootfile.into()
1438 );
1439
1440 let result = builder.add_rootfile("package.opf");
1441 assert!(result.is_ok());
1442
1443 let result = builder.add_manifest("./test_case/Overview.xhtml", manifest_item);
1444 assert!(result.is_ok());
1445 }
1446
1447 #[test]
1448 fn test_add_manifest_nonexistent_file() {
1449 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1450 assert!(builder.add_rootfile("content.opf").is_ok());
1451
1452 let manifest_item = ManifestItem::new("test", "nonexistent.xhtml").unwrap();
1453 let result = builder.add_manifest("nonexistent.xhtml", manifest_item);
1454
1455 assert!(result.is_err());
1456 assert_eq!(
1457 result.unwrap_err(),
1458 EpubBuilderError::TargetIsNotFile {
1459 target_path: "nonexistent.xhtml".to_string()
1460 }
1461 .into()
1462 );
1463 }
1464
1465 #[test]
1466 fn test_add_manifest_unknow_file_format() {
1467 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1468 let result = builder.add_rootfile("package.opf");
1469 assert!(result.is_ok());
1470
1471 let result = builder.add_manifest(
1472 "./test_case/unknown_file_format.xhtml",
1473 ManifestItem {
1474 id: "file".to_string(),
1475 path: PathBuf::from("unknown_file_format.xhtml"),
1476 mime: String::new(),
1477 properties: None,
1478 fallback: None,
1479 },
1480 );
1481
1482 assert!(result.is_err());
1483 assert_eq!(
1484 result.unwrap_err(),
1485 EpubBuilderError::UnknownFileFormat {
1486 file_path: "./test_case/unknown_file_format.xhtml".to_string(),
1487 }
1488 .into()
1489 )
1490 }
1491
1492 #[test]
1493 fn test_remove_manifest() {
1494 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1495 builder.add_rootfile("package.opf").unwrap();
1496
1497 builder
1498 .add_manifest(
1499 "./test_case/Overview.xhtml",
1500 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1501 )
1502 .unwrap();
1503 builder
1504 .add_manifest(
1505 "./test_case/Overview.xhtml",
1506 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1507 )
1508 .unwrap();
1509 builder
1510 .add_manifest(
1511 "./test_case/Overview.xhtml",
1512 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1513 )
1514 .unwrap();
1515
1516 assert_eq!(builder.manifest.len(), 3);
1517
1518 let result = builder.remove_manifest("item2");
1519 assert!(result.is_ok());
1520 assert_eq!(builder.manifest.len(), 2);
1521 assert!(!builder.manifest.contains_key("item2"));
1522 assert!(builder.manifest.contains_key("item1"));
1523 assert!(builder.manifest.contains_key("item3"));
1524
1525 builder.remove_manifest("item1").unwrap();
1526 assert_eq!(builder.manifest.len(), 1);
1527 assert!(builder.manifest.contains_key("item3"));
1528
1529 let result = builder.remove_manifest("nonexistent");
1530 assert!(result.is_ok());
1531 assert_eq!(builder.manifest.len(), 1);
1532 }
1533
1534 #[test]
1535 fn test_take_manifest() {
1536 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1537 builder.add_rootfile("package.opf").unwrap();
1538
1539 builder
1540 .add_manifest(
1541 "./test_case/Overview.xhtml",
1542 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1543 )
1544 .unwrap();
1545 builder
1546 .add_manifest(
1547 "./test_case/Overview.xhtml",
1548 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1549 )
1550 .unwrap();
1551
1552 assert_eq!(builder.manifest.len(), 2);
1553
1554 let taken = builder.take_manifest("item1");
1555 assert!(taken.is_some());
1556 assert_eq!(taken.unwrap().id, "item1");
1557 assert_eq!(builder.manifest.len(), 1);
1558 assert!(!builder.manifest.contains_key("item1"));
1559
1560 let taken = builder.take_manifest("item2");
1561 assert!(taken.is_some());
1562 assert_eq!(taken.unwrap().id, "item2");
1563 assert!(builder.manifest.is_empty());
1564
1565 let taken = builder.take_manifest("item1");
1566 assert!(taken.is_none());
1567
1568 builder
1569 .add_manifest(
1570 "./test_case/Overview.xhtml",
1571 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1572 )
1573 .unwrap();
1574 let taken = builder.take_manifest("nonexistent");
1575 assert!(taken.is_none());
1576 assert_eq!(builder.manifest.len(), 1);
1577 }
1578
1579 #[test]
1580 fn test_clear_manifests() {
1581 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1582 builder.add_rootfile("package.opf").unwrap();
1583
1584 let result = builder.clear_manifests();
1585 assert!(result.is_ok());
1586 assert!(builder.manifest.is_empty());
1587
1588 builder
1589 .add_manifest(
1590 "./test_case/Overview.xhtml",
1591 ManifestItem::new("item1", "content1.xhtml").unwrap(),
1592 )
1593 .unwrap();
1594 builder
1595 .add_manifest(
1596 "./test_case/Overview.xhtml",
1597 ManifestItem::new("item2", "content2.xhtml").unwrap(),
1598 )
1599 .unwrap();
1600 builder
1601 .add_manifest(
1602 "./test_case/Overview.xhtml",
1603 ManifestItem::new("item3", "content3.xhtml").unwrap(),
1604 )
1605 .unwrap();
1606
1607 assert_eq!(builder.manifest.len(), 3);
1608
1609 let result = builder.clear_manifests();
1610 assert!(result.is_ok());
1611 assert!(builder.manifest.is_empty());
1612
1613 builder
1614 .add_manifest(
1615 "./test_case/Overview.xhtml",
1616 ManifestItem::new("new_item", "new_content.xhtml").unwrap(),
1617 )
1618 .unwrap();
1619 assert_eq!(builder.manifest.len(), 1);
1620 assert_eq!(builder.manifest.get("new_item").unwrap().id, "new_item");
1621 }
1622
1623 #[test]
1624 fn test_add_spine() {
1625 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1626 let spine_item = SpineItem::new("test_item");
1627
1628 builder.add_spine(spine_item.clone());
1629
1630 assert_eq!(builder.spine.len(), 1);
1631 assert_eq!(builder.spine[0].idref, "test_item");
1632 }
1633
1634 #[test]
1635 fn test_remove_last_spine() {
1636 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1637
1638 builder.add_spine(SpineItem::new("chapter1"));
1639 builder.add_spine(SpineItem::new("chapter2"));
1640 builder.add_spine(SpineItem::new("chapter3"));
1641 assert_eq!(builder.spine.len(), 3);
1642
1643 builder.remove_last_spine();
1644 assert_eq!(builder.spine.len(), 2);
1645 assert_eq!(builder.spine[0].idref, "chapter1");
1646 assert_eq!(builder.spine[1].idref, "chapter2");
1647
1648 builder.remove_last_spine();
1649 assert_eq!(builder.spine.len(), 1);
1650 assert_eq!(builder.spine[0].idref, "chapter1");
1651
1652 builder.remove_last_spine();
1653 assert!(builder.spine.is_empty());
1654
1655 builder.remove_last_spine();
1656 assert!(builder.spine.is_empty());
1657 }
1658
1659 #[test]
1660 fn test_take_last_spine() {
1661 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1662
1663 let result = builder.take_last_spine();
1664 assert!(result.is_none());
1665
1666 builder.add_spine(SpineItem::new("chapter1"));
1667 builder.add_spine(SpineItem::new("chapter2"));
1668 builder.add_spine(SpineItem::new("chapter3"));
1669 assert_eq!(builder.spine.len(), 3);
1670
1671 let result = builder.take_last_spine();
1672 assert!(result.is_some());
1673 assert_eq!(result.unwrap().idref, "chapter3");
1674 assert_eq!(builder.spine.len(), 2);
1675
1676 let result = builder.take_last_spine();
1677 assert_eq!(result.unwrap().idref, "chapter2");
1678 assert_eq!(builder.spine.len(), 1);
1679
1680 let result = builder.take_last_spine();
1681 assert_eq!(result.unwrap().idref, "chapter1");
1682 assert!(builder.spine.is_empty());
1683
1684 let result = builder.take_last_spine();
1685 assert!(result.is_none());
1686 }
1687
1688 #[test]
1689 fn test_clear_spines() {
1690 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1691
1692 builder.clear_spines();
1693 assert!(builder.spine.is_empty());
1694
1695 builder.add_spine(SpineItem::new("chapter1"));
1696 builder.add_spine(SpineItem::new("chapter2"));
1697 builder.add_spine(SpineItem::new("chapter3"));
1698 assert_eq!(builder.spine.len(), 3);
1699
1700 builder.clear_spines();
1701 assert!(builder.spine.is_empty());
1702 assert_eq!(builder.spine.len(), 0);
1703
1704 builder.add_spine(SpineItem::new("new_chapter"));
1705 assert_eq!(builder.spine.len(), 1);
1706 assert_eq!(builder.spine[0].idref, "new_chapter");
1707 }
1708
1709 #[test]
1710 fn test_set_catalog_title() {
1711 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1712 let title = "Test Catalog Title";
1713
1714 builder.set_catalog_title(title);
1715
1716 assert_eq!(builder.catalog_title, title);
1717 }
1718
1719 #[test]
1720 fn test_add_catalog_item() {
1721 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1722 let nav_point = NavPoint::new("Chapter 1");
1723
1724 builder.add_catalog_item(nav_point.clone());
1725
1726 assert_eq!(builder.catalog.len(), 1);
1727 assert_eq!(builder.catalog[0].label, "Chapter 1");
1728 }
1729
1730 #[test]
1731 fn test_remove_last_catalog_item() {
1732 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1733
1734 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1735 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1736 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1737 assert_eq!(builder.catalog.len(), 3);
1738
1739 builder.remove_last_catalog_item();
1740 assert_eq!(builder.catalog.len(), 2);
1741 assert_eq!(builder.catalog[0].label, "Chapter 1");
1742 assert_eq!(builder.catalog[1].label, "Chapter 2");
1743
1744 builder.remove_last_catalog_item();
1745 assert_eq!(builder.catalog.len(), 1);
1746 assert_eq!(builder.catalog[0].label, "Chapter 1");
1747
1748 builder.remove_last_catalog_item();
1749 assert!(builder.catalog.is_empty());
1750
1751 builder.remove_last_catalog_item();
1752 assert!(builder.catalog.is_empty());
1753 }
1754
1755 #[test]
1756 fn test_take_last_catalog_item() {
1757 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1758
1759 let result = builder.take_last_catalog_item();
1760 assert!(result.is_none());
1761
1762 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1763 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1764 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1765 assert_eq!(builder.catalog.len(), 3);
1766
1767 let result = builder.take_last_catalog_item();
1768 assert!(result.is_some());
1769 assert_eq!(result.unwrap().label, "Chapter 3");
1770 assert_eq!(builder.catalog.len(), 2);
1771
1772 let result = builder.take_last_catalog_item();
1773 assert_eq!(result.unwrap().label, "Chapter 2");
1774 assert_eq!(builder.catalog.len(), 1);
1775
1776 let result = builder.take_last_catalog_item();
1777 assert_eq!(result.unwrap().label, "Chapter 1");
1778 assert!(builder.catalog.is_empty());
1779
1780 let result = builder.take_last_catalog_item();
1781 assert!(result.is_none());
1782 }
1783
1784 #[test]
1785 fn test_set_catalog() {
1786 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1787 let nav_points = vec![NavPoint::new("Chapter 1"), NavPoint::new("Chapter 2")];
1788
1789 builder.set_catalog(nav_points.clone());
1790
1791 assert_eq!(builder.catalog.len(), 2);
1792 assert_eq!(builder.catalog[0].label, "Chapter 1");
1793 assert_eq!(builder.catalog[1].label, "Chapter 2");
1794 }
1795
1796 #[test]
1797 fn test_clear_catalog() {
1798 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1799
1800 builder.clear_catalog();
1801 assert!(builder.catalog.is_empty());
1802
1803 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1804 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1805 builder.add_catalog_item(NavPoint::new("Chapter 3"));
1806 assert_eq!(builder.catalog.len(), 3);
1807
1808 builder.clear_catalog();
1809 assert!(builder.catalog.is_empty());
1810 assert_eq!(builder.catalog.len(), 0);
1811
1812 builder.add_catalog_item(NavPoint::new("New Chapter"));
1813 assert_eq!(builder.catalog.len(), 1);
1814 assert_eq!(builder.catalog[0].label, "New Chapter");
1815 }
1816
1817 #[test]
1818 fn test_clear_all() {
1819 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1820
1821 builder.add_rootfile("content.opf").unwrap();
1822 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1823 builder.add_metadata(MetadataItem::new("language", "en"));
1824 builder.add_spine(SpineItem::new("chapter1"));
1825 builder.add_spine(SpineItem::new("chapter2"));
1826 builder.add_catalog_item(NavPoint::new("Chapter 1"));
1827 builder.add_catalog_item(NavPoint::new("Chapter 2"));
1828 builder.set_catalog_title("Table of Contents");
1829
1830 assert_eq!(builder.metadata.len(), 2);
1831 assert_eq!(builder.spine.len(), 2);
1832 assert_eq!(builder.catalog.len(), 2);
1833 assert_eq!(builder.catalog_title, "Table of Contents");
1834
1835 let result = builder.clear_all();
1836 assert!(result.is_ok());
1837
1838 assert!(builder.metadata.is_empty());
1839 assert!(builder.spine.is_empty());
1840 assert!(builder.catalog.is_empty());
1841 assert!(builder.catalog_title.is_empty());
1842 assert!(builder.manifest.is_empty());
1843
1844 builder.add_metadata(MetadataItem::new("title", "New Book"));
1845 builder.add_spine(SpineItem::new("new_chapter"));
1846 builder.add_catalog_item(NavPoint::new("New Chapter"));
1847
1848 assert_eq!(builder.metadata.len(), 1);
1849 assert_eq!(builder.spine.len(), 1);
1850 assert_eq!(builder.catalog.len(), 1);
1851 }
1852
1853 #[test]
1854 fn test_make_container_file() {
1855 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1856
1857 let result = builder.make_container_xml();
1858 assert!(result.is_err());
1859 assert_eq!(
1860 result.unwrap_err(),
1861 EpubBuilderError::MissingRootfile.into()
1862 );
1863
1864 assert!(builder.add_rootfile("content.opf").is_ok());
1865 let result = builder.make_container_xml();
1866 assert!(result.is_ok());
1867 }
1868
1869 #[test]
1870 fn test_make_navigation_document() {
1871 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1872
1873 let result = builder.make_navigation_document();
1874 assert!(result.is_err());
1875 assert_eq!(
1876 result.unwrap_err(),
1877 EpubBuilderError::NavigationInfoUninitalized.into()
1878 );
1879
1880 builder.set_catalog(vec![NavPoint::new("test")]);
1881 assert!(builder.make_navigation_document().is_ok());
1882 }
1883
1884 #[test]
1885 fn test_validate_metadata_success() {
1886 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1887
1888 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1889 builder.add_metadata(MetadataItem::new("language", "en"));
1890 builder.add_metadata(
1891 MetadataItem::new("identifier", "urn:isbn:1234567890")
1892 .with_id("pub-id")
1893 .build(),
1894 );
1895
1896 assert!(builder.validate_metadata());
1897 }
1898
1899 #[test]
1900 fn test_validate_metadata_missing_required() {
1901 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1902
1903 builder.add_metadata(MetadataItem::new("title", "Test Book"));
1904 builder.add_metadata(MetadataItem::new("language", "en"));
1905
1906 assert!(!builder.validate_metadata());
1907 }
1908
1909 #[test]
1910 fn test_validate_fallback_chain_valid() {
1911 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1912
1913 let item3 = ManifestItem::new("item3", "path3");
1914 assert!(item3.is_ok());
1915
1916 let item3 = item3.unwrap();
1917 let item2 = ManifestItem::new("item2", "path2")
1918 .unwrap()
1919 .with_fallback("item3")
1920 .build();
1921 let item1 = ManifestItem::new("item1", "path1")
1922 .unwrap()
1923 .with_fallback("item2")
1924 .build();
1925
1926 builder.manifest.insert("item3".to_string(), item3);
1927 builder.manifest.insert("item2".to_string(), item2);
1928 builder.manifest.insert("item1".to_string(), item1);
1929
1930 let result = builder.validate_manifest_fallback_chains();
1931 assert!(result.is_ok());
1932 }
1933
1934 #[test]
1935 fn test_validate_fallback_chain_circular_reference() {
1936 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1937
1938 let item2 = ManifestItem::new("item2", "path2")
1939 .unwrap()
1940 .with_fallback("item1")
1941 .build();
1942 let item1 = ManifestItem::new("item1", "path1")
1943 .unwrap()
1944 .with_fallback("item2")
1945 .build();
1946
1947 builder.manifest.insert("item1".to_string(), item1);
1948 builder.manifest.insert("item2".to_string(), item2);
1949
1950 let result = builder.validate_manifest_fallback_chains();
1951 assert!(result.is_err());
1952 assert!(
1953 result.unwrap_err().to_string().starts_with(
1954 "Epub builder error: Circular reference detected in fallback chain for"
1955 ),
1956 );
1957 }
1958
1959 #[test]
1960 fn test_validate_fallback_chain_not_found() {
1961 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1962
1963 let item1 = ManifestItem::new("item1", "path1")
1964 .unwrap()
1965 .with_fallback("nonexistent")
1966 .build();
1967
1968 builder.manifest.insert("item1".to_string(), item1);
1969
1970 let result = builder.validate_manifest_fallback_chains();
1971 assert!(result.is_err());
1972 assert_eq!(
1973 result.unwrap_err().to_string(),
1974 "Epub builder error: Fallback resource 'nonexistent' does not exist in manifest."
1975 );
1976 }
1977
1978 #[test]
1979 fn test_validate_manifest_nav_single() {
1980 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1981
1982 let nav_item = ManifestItem::new("nav", "nav.xhtml")
1983 .unwrap()
1984 .append_property("nav")
1985 .build();
1986 builder.manifest.insert("nav".to_string(), nav_item);
1987
1988 let result = builder.validate_manifest_nav();
1989 assert!(result.is_ok());
1990 }
1991
1992 #[test]
1993 fn test_validate_manifest_nav_multiple() {
1994 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
1995
1996 let nav_item1 = ManifestItem::new("nav1", "nav1.xhtml")
1997 .unwrap()
1998 .append_property("nav")
1999 .build();
2000 let nav_item2 = ManifestItem::new("nav2", "nav2.xhtml")
2001 .unwrap()
2002 .append_property("nav")
2003 .build();
2004
2005 builder.manifest.insert("nav1".to_string(), nav_item1);
2006 builder.manifest.insert("nav2".to_string(), nav_item2);
2007
2008 let result = builder.validate_manifest_nav();
2009 assert!(result.is_err());
2010 assert_eq!(
2011 result.unwrap_err().to_string(),
2012 "Epub builder error: There are too many items with 'nav' property in the manifest."
2013 );
2014 }
2015
2016 #[test]
2017 fn test_make_opf_file_success() {
2018 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2019
2020 assert!(builder.add_rootfile("content.opf").is_ok());
2021 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2022 builder.add_metadata(MetadataItem::new("language", "en"));
2023 builder.add_metadata(
2024 MetadataItem::new("identifier", "urn:isbn:1234567890")
2025 .with_id("pub-id")
2026 .build(),
2027 );
2028
2029 let temp_dir = env::temp_dir().join(local_time());
2030 fs::create_dir_all(&temp_dir).unwrap();
2031
2032 let test_file = temp_dir.join("test.xhtml");
2033 fs::write(&test_file, "<html></html>").unwrap();
2034
2035 let manifest_result = builder.add_manifest(
2036 test_file.to_str().unwrap(),
2037 ManifestItem::new("test", "test.xhtml").unwrap(),
2038 );
2039 assert!(manifest_result.is_ok());
2040
2041 builder.add_catalog_item(NavPoint::new("Chapter"));
2042 builder.add_spine(SpineItem::new("test"));
2043
2044 let result = builder.make_navigation_document();
2045 assert!(result.is_ok());
2046
2047 let result = builder.make_opf_file();
2048 assert!(result.is_ok());
2049
2050 let opf_path = builder.temp_dir.join("content.opf");
2051 assert!(opf_path.exists());
2052
2053 fs::remove_dir_all(temp_dir).unwrap();
2054 }
2055
2056 #[test]
2057 fn test_make_opf_file_missing_metadata() {
2058 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2059 assert!(builder.add_rootfile("content.opf").is_ok());
2060
2061 let result = builder.make_opf_file();
2062 assert!(result.is_err());
2063 assert_eq!(
2064 result.unwrap_err().to_string(),
2065 "Epub builder error: Requires at least one 'title', 'language', and 'identifier' with id 'pub-id'."
2066 );
2067 }
2068
2069 #[test]
2070 fn test_make() {
2071 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2072
2073 assert!(builder.add_rootfile("content.opf").is_ok());
2074 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2075 builder.add_metadata(MetadataItem::new("language", "en"));
2076 builder.add_metadata(
2077 MetadataItem::new("identifier", "test_identifier")
2078 .with_id("pub-id")
2079 .build(),
2080 );
2081
2082 assert!(
2083 builder
2084 .add_manifest(
2085 "./test_case/Overview.xhtml",
2086 ManifestItem {
2087 id: "test".to_string(),
2088 path: PathBuf::from("test.xhtml"),
2089 mime: String::new(),
2090 properties: None,
2091 fallback: None,
2092 },
2093 )
2094 .is_ok()
2095 );
2096
2097 builder.add_catalog_item(NavPoint::new("Chapter"));
2098 builder.add_spine(SpineItem::new("test"));
2099
2100 let file = env::temp_dir()
2101 .join("temp_dir")
2102 .join(format!("{}.epub", local_time()));
2103 assert!(builder.make(&file).is_ok());
2104 assert!(EpubDoc::new(&file).is_ok());
2105 }
2106
2107 #[test]
2108 fn test_build() {
2109 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2110
2111 assert!(builder.add_rootfile("content.opf").is_ok());
2112 builder.add_metadata(MetadataItem::new("title", "Test Book"));
2113 builder.add_metadata(MetadataItem::new("language", "en"));
2114 builder.add_metadata(
2115 MetadataItem::new("identifier", "test_identifier")
2116 .with_id("pub-id")
2117 .build(),
2118 );
2119
2120 assert!(
2121 builder
2122 .add_manifest(
2123 "./test_case/Overview.xhtml",
2124 ManifestItem {
2125 id: "test".to_string(),
2126 path: PathBuf::from("test.xhtml"),
2127 mime: String::new(),
2128 properties: None,
2129 fallback: None,
2130 },
2131 )
2132 .is_ok()
2133 );
2134
2135 builder.add_catalog_item(NavPoint::new("Chapter"));
2136 builder.add_spine(SpineItem::new("test"));
2137
2138 let file = env::temp_dir().join(format!("{}.epub", local_time()));
2139 assert!(builder.build(&file).is_ok());
2140 }
2141
2142 #[test]
2143 fn test_from() {
2144 let builder = EpubBuilder::<EpubVersion3>::new();
2145 assert!(builder.is_ok());
2146
2147 let metadata = vec![
2148 MetadataItem {
2149 id: None,
2150 property: "title".to_string(),
2151 value: "Test Book".to_string(),
2152 lang: None,
2153 refined: vec![],
2154 },
2155 MetadataItem {
2156 id: None,
2157 property: "language".to_string(),
2158 value: "en".to_string(),
2159 lang: None,
2160 refined: vec![],
2161 },
2162 MetadataItem {
2163 id: Some("pub-id".to_string()),
2164 property: "identifier".to_string(),
2165 value: "test-book".to_string(),
2166 lang: None,
2167 refined: vec![],
2168 },
2169 ];
2170 let spine = vec![SpineItem {
2171 id: None,
2172 idref: "main".to_string(),
2173 linear: true,
2174 properties: None,
2175 }];
2176 let catalog = vec![
2177 NavPoint {
2178 label: "Nav".to_string(),
2179 content: None,
2180 children: vec![],
2181 play_order: None,
2182 },
2183 NavPoint {
2184 label: "Overview".to_string(),
2185 content: None,
2186 children: vec![],
2187 play_order: None,
2188 },
2189 ];
2190
2191 let mut builder = builder.unwrap();
2192 assert!(builder.add_rootfile("content.opf").is_ok());
2193 builder.metadata = metadata.clone();
2194 builder.spine = spine.clone();
2195 builder.catalog = catalog.clone();
2196 builder.set_catalog_title("catalog title");
2197 let result = builder.add_manifest(
2198 "./test_case/Overview.xhtml",
2199 ManifestItem {
2200 id: "main".to_string(),
2201 path: PathBuf::from("Overview.xhtml"),
2202 mime: String::new(),
2203 properties: None,
2204 fallback: None,
2205 },
2206 );
2207 assert!(result.is_ok());
2208
2209 let epub_file = env::temp_dir().join(format!("{}.epub", local_time()));
2210 let result = builder.make(&epub_file);
2211 assert!(result.is_ok());
2212
2213 let doc = EpubDoc::new(&epub_file);
2214 assert!(doc.is_ok());
2215
2216 let mut doc = doc.unwrap();
2217 let builder = EpubBuilder::from(&mut doc);
2218 assert!(builder.is_ok());
2219 let builder = builder.unwrap();
2220
2221 assert_eq!(builder.metadata.len(), metadata.len() + 1);
2222 assert_eq!(builder.manifest.len(), 1); assert_eq!(builder.spine.len(), spine.len());
2224 assert_eq!(builder.catalog, catalog);
2225 assert_eq!(builder.catalog_title, "catalog title");
2226
2227 fs::remove_file(epub_file).unwrap();
2228 }
2229
2230 #[test]
2231 fn test_normalize_manifest_path() {
2232 let mut builder = EpubBuilder::<EpubVersion3>::new().unwrap();
2233
2234 assert!(builder.add_rootfile("content.opf").is_ok());
2235
2236 let result = builder.normalize_manifest_path("../../test.xhtml", "id");
2237 assert!(result.is_err());
2238 assert_eq!(
2239 result.unwrap_err(),
2240 EpubError::RealtiveLinkLeakage { path: "../../test.xhtml".to_string() }
2241 );
2242
2243 let result = builder.normalize_manifest_path("/test.xhtml", "id");
2244 assert!(result.is_ok());
2245 assert_eq!(result.unwrap(), builder.temp_dir.join("test.xhtml"));
2246
2247 let result = builder.normalize_manifest_path("./test.xhtml", "manifest_id");
2248 assert!(result.is_err());
2249 assert_eq!(
2250 result.unwrap_err(),
2251 EpubBuilderError::IllegalManifestPath { manifest_id: "manifest_id".to_string() }.into(),
2252 );
2253 }
2254
2255 #[test]
2256 fn test_refine_mime_type() {
2257 assert_eq!(
2258 refine_mime_type("text/xml", "xhtml"),
2259 "application/xhtml+xml"
2260 );
2261 assert_eq!(refine_mime_type("text/xml", "xht"), "application/xhtml+xml");
2262 assert_eq!(
2263 refine_mime_type("application/xml", "opf"),
2264 "application/oebps-package+xml"
2265 );
2266 assert_eq!(
2267 refine_mime_type("text/xml", "ncx"),
2268 "application/x-dtbncx+xml"
2269 );
2270 assert_eq!(refine_mime_type("text/plain", "css"), "text/css");
2271 assert_eq!(refine_mime_type("text/plain", "unknown"), "text/plain");
2272 }
2273}