1pub mod catalog;
32pub mod ccel;
33pub mod door43;
34pub mod ebible;
35pub mod fetch;
36pub mod license;
37pub mod sword;
38pub mod update;
39
40use logoxide::{
41 LogoxideError, Result,
42 catalog::{CatalogEntry, CatalogSource, Download},
43 manifest::{Format, FormatEntry, Keys, MANIFEST, Manifest, Package as Info, Upstream},
44 package::{Features, Package},
45 storage::{MemoryStorage, Storage},
46};
47
48pub use update::{
49 ImportUpdate, apply_import, compare_import, compare_publisher_versions, import_is_current,
50 import_removals, set_version,
51};
52
53pub const PREFIXES: [&str; 5] = ["ebible", "fetch", "ccel", "sword", "door43"];
55
56pub const IMPORT_VERSION: semver::Version = semver::Version::new(1, 0, 0);
58
59pub fn unzip(bytes: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
61 Ok(logoxide_text::archive::unzip(bytes)?)
62}
63
64pub fn downloads(entry: &CatalogEntry) -> Result<Vec<Download>> {
70 match &entry.source {
71 CatalogSource::External { downloads, .. } => Ok(downloads.clone()),
72 CatalogSource::Git { .. } | CatalogSource::Registry { .. } => Err(LogoxideError::Import(
73 format!("{} isn't imported: add it", entry.id),
74 )),
75 }
76}
77
78pub fn build(entry: &CatalogEntry, files: &[(String, Vec<u8>)], dest: &dyn Storage) -> Result<()> {
81 let named = |name: &str| {
82 (files.iter())
83 .find(|(n, _)| n == name)
84 .map(|(_, bytes)| bytes.as_slice())
85 .ok_or_else(|| LogoxideError::Import(format!("{}: {name} wasn't downloaded", entry.id)))
86 };
87 match entry.source.prefix() {
88 Some("ebible") => ebible::import(entry, named("zip")?, dest),
89 Some("fetch") => fetch::import(entry, files, dest),
90 Some("ccel") => ccel::import(entry, named("thml")?, dest),
91 Some("sword") => sword::import(entry, named("zip")?, dest),
92 Some("door43") => door43::import(entry, named("archive")?, dest),
93 Some(other) => Err(LogoxideError::Import(format!(
94 "{}: no import reads `{other}:` downloads",
95 entry.id
96 ))),
97 None => Err(LogoxideError::Import(format!(
98 "{} isn't imported: add it",
99 entry.id
100 ))),
101 }
102}
103
104pub fn upstream(entry: &CatalogEntry) -> Option<Upstream> {
111 let CatalogSource::External {
112 import, upstream, ..
113 } = &entry.source
114 else {
115 return None;
116 };
117 Some(Upstream {
118 url: upstream.clone(),
119 import: Some(import.clone()),
120 version: entry.version.clone(),
121 converted: entry.source.prefix() == Some("sword"),
124 note: None,
125 })
126}
127
128pub(crate) fn external(
131 prefix: &str,
132 id: &str,
133 tracks_versions: bool,
134 downloads: Vec<Download>,
135) -> CatalogSource {
136 CatalogSource::External {
137 import: format!("{prefix}:{id}"),
138 upstream: (downloads.first()).and_then(|d| d.urls.first().cloned()),
139 tracks_versions,
140 downloads,
141 }
142}
143
144pub(crate) fn package_id(id: &str) -> String {
147 id.to_lowercase()
148 .chars()
149 .map(|c| match c {
150 'a'..='z' | '0'..='9' | '-' | '_' => c,
151 _ => '-',
152 })
153 .collect()
154}
155
156pub(crate) fn import_id<'a>(
159 entry: &'a CatalogEntry,
160 prefix: &str,
161 publisher: &str,
162) -> Result<&'a str> {
163 match entry.source.prefix() == Some(prefix) {
164 true => entry
165 .source
166 .import_id()
167 .ok_or_else(|| LogoxideError::Import(format!("{}: no import id", entry.id))),
168 false => Err(LogoxideError::Import(format!(
169 "{} isn't from {publisher}",
170 entry.id
171 ))),
172 }
173}
174
175pub(crate) struct Made {
177 pub(crate) format: &'static str,
179 pub(crate) license_file: Option<String>,
180 pub(crate) publisher: &'static str,
181 pub(crate) homepage: String,
182 pub(crate) versification: Option<String>,
184 pub(crate) keys: Option<Keys>,
186}
187
188pub(crate) fn finish(
191 entry: &CatalogEntry,
192 made: Made,
193 staged: MemoryStorage,
194 dest: &dyn Storage,
195) -> Result<()> {
196 use logoxide::language::{script_direction, script_of_tag};
197 let script = (entry.script.clone()).or_else(|| script_of_tag(&entry.language));
199 let implied = script_direction(script.as_deref());
200 let info = Info {
201 id: entry.id.clone(),
202 name: entry.name.clone(),
203 abbreviation: entry.abbreviation.clone(),
204 authors: vec![],
205 description: entry.description.clone(),
206 aliases: entry.aliases.clone(),
207 version: IMPORT_VERSION,
208 kind: entry.kind,
209 language: entry.language.clone(),
210 language_name: entry.language_name.clone(),
211 script: entry.script.clone(),
212 direction: (entry.direction != implied).then_some(entry.direction),
213 license: (entry.license.clone()).map(|license| {
215 match logoxide::license::check_license(&license) {
216 Ok(()) => license,
217 Err(_) => format!("LicenseRef-{}", made.publisher.replace(' ', "-")),
218 }
219 }),
220 rights: (!entry.rights.is_empty()).then_some(entry.rights),
221 license_file: made.license_file,
222 publisher: Some(made.publisher.to_string()),
223 homepage: Some(made.homepage),
224 repository: None,
225 documentation: None,
226 readme: None,
227 keywords: entry.keywords.clone(),
228 categories: entry.categories.clone(),
229 about_books: entry.about_books.clone(),
230 work: (entry.work.clone()).or_else(|| logoxide::works::of_source(&entry.source)),
232 edition: entry.edition.clone(),
233 edition_year: entry.edition_year,
234 upstream: upstream(entry),
235 versification: made.versification.unwrap_or_else(|| String::from("own")),
236 maps_to: None,
237 keys: made.keys,
238 include: vec![],
239 };
240 let folder = match Format::from_name(made.format) {
242 Some(Format::Dictionary) => "entries",
243 _ => made.format,
244 };
245 let manifest = Manifest {
246 package: Some(info),
247 formats: [(
248 made.format.to_string(),
249 FormatEntry {
250 path: format!("{folder}/"),
251 feature: None,
252 },
253 )]
254 .into(),
255 ..Manifest::default()
256 };
257 let text = toml::to_string(&manifest).map_err(|e| LogoxideError::Import(e.to_string()))?;
258 let header = format!("# Imported from {} by logoxide\n", made.publisher);
259 staged.write(MANIFEST, format!("{header}{text}").as_bytes())?;
260 let staged = std::sync::Arc::new(staged);
261 let package = Package::open(staged.clone(), Features::default())?;
262 let verses = (package.text_format()).is_some_and(|(f, _)| f.is_verse_text());
263 let book = entry.kind == logoxide::manifest::PackageKind::Book;
265 if verses && package.books().is_empty() {
266 return Err(LogoxideError::Import(format!("{}: no books", entry.id)));
267 }
268 if !verses && !book && package.source().keys().is_empty() {
269 return Err(LogoxideError::Import(format!("{}: no entries", entry.id)));
270 }
271 for path in staged.walk("")? {
272 dest.write(&path, &staged.read(&path)?)?;
273 }
274 Ok(())
275}
276
277#[cfg(test)]
278pub(crate) mod tests {
279 use std::io::Write;
280
281 pub(crate) fn zip(files: &[(&str, &str)]) -> Vec<u8> {
283 zip_bytes(
284 &(files.iter())
285 .map(|(path, text)| (*path, text.as_bytes()))
286 .collect::<Vec<_>>(),
287 )
288 }
289
290 pub(crate) fn zip_bytes(files: &[(&str, &[u8])]) -> Vec<u8> {
291 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
292 for (path, bytes) in files {
293 writer
294 .start_file(*path, zip::write::SimpleFileOptions::default())
295 .unwrap();
296 writer.write_all(bytes).unwrap();
297 }
298 writer.finish().unwrap().into_inner()
299 }
300
301 pub(crate) fn build_twice(
306 entry: &logoxide::catalog::CatalogEntry,
307 files: &[(String, Vec<u8>)],
308 dest: &dyn logoxide::storage::Storage,
309 ) {
310 super::build(entry, files, dest).unwrap();
311 use logoxide::storage::Storage;
312 let again = super::MemoryStorage::new();
313 super::build(entry, files, &again).unwrap();
314 assert_eq!(dest.walk("").unwrap(), again.walk("").unwrap());
315 for path in again.walk("").unwrap() {
316 assert_eq!(
317 dest.read(&path).unwrap(),
318 again.read(&path).unwrap(),
319 "{path}"
320 );
321 }
322 }
323
324 #[test]
325 fn what_isnt_imported_is_said() {
326 let catalog = logoxide::catalog::Catalog::parse_toml(
327 "[[package]]\nid = \"kjv\"\nname = \"KJV\"\nabbreviation = \"KJV\"\nlanguage = \"en\"\ngit = \"https://example.org/kjv\"\n",
328 )
329 .unwrap();
330 let entry = &catalog.entries[0];
331 assert!(super::downloads(entry).is_err());
332 assert!(super::build(entry, &[], &super::MemoryStorage::new()).is_err());
333 assert_eq!(super::upstream(entry), None);
334 assert_eq!(super::package_id("eng-ASV 1901"), "eng-asv-1901");
335 }
336}