pub mod catalog;
pub mod ccel;
pub mod door43;
pub mod ebible;
pub mod fetch;
pub mod license;
pub mod sword;
pub mod update;
use logoxide::{
LogoxideError, Result,
catalog::{CatalogEntry, CatalogSource, Download},
manifest::{Format, FormatEntry, Keys, MANIFEST, Manifest, Package as Info, Upstream},
package::{Features, Package},
storage::{MemoryStorage, Storage},
};
pub use update::{
ImportUpdate, apply_import, compare_import, compare_publisher_versions, import_is_current,
import_removals, set_version,
};
pub const PREFIXES: [&str; 5] = ["ebible", "fetch", "ccel", "sword", "door43"];
pub const IMPORT_VERSION: semver::Version = semver::Version::new(1, 0, 0);
pub fn unzip(bytes: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
Ok(logoxide_text::archive::unzip(bytes)?)
}
pub fn downloads(entry: &CatalogEntry) -> Result<Vec<Download>> {
match &entry.source {
CatalogSource::External { downloads, .. } => Ok(downloads.clone()),
CatalogSource::Git { .. } | CatalogSource::Registry { .. } => Err(LogoxideError::Import(
format!("{} isn't imported: add it", entry.id),
)),
}
}
pub fn build(entry: &CatalogEntry, files: &[(String, Vec<u8>)], dest: &dyn Storage) -> Result<()> {
let named = |name: &str| {
(files.iter())
.find(|(n, _)| n == name)
.map(|(_, bytes)| bytes.as_slice())
.ok_or_else(|| LogoxideError::Import(format!("{}: {name} wasn't downloaded", entry.id)))
};
match entry.source.prefix() {
Some("ebible") => ebible::import(entry, named("zip")?, dest),
Some("fetch") => fetch::import(entry, files, dest),
Some("ccel") => ccel::import(entry, named("thml")?, dest),
Some("sword") => sword::import(entry, named("zip")?, dest),
Some("door43") => door43::import(entry, named("archive")?, dest),
Some(other) => Err(LogoxideError::Import(format!(
"{}: no import reads `{other}:` downloads",
entry.id
))),
None => Err(LogoxideError::Import(format!(
"{} isn't imported: add it",
entry.id
))),
}
}
pub fn upstream(entry: &CatalogEntry) -> Option<Upstream> {
let CatalogSource::External {
import, upstream, ..
} = &entry.source
else {
return None;
};
Some(Upstream {
url: upstream.clone(),
import: Some(import.clone()),
version: entry.version.clone(),
converted: entry.source.prefix() == Some("sword"),
note: None,
})
}
pub(crate) fn external(
prefix: &str,
id: &str,
tracks_versions: bool,
downloads: Vec<Download>,
) -> CatalogSource {
CatalogSource::External {
import: format!("{prefix}:{id}"),
upstream: (downloads.first()).and_then(|d| d.urls.first().cloned()),
tracks_versions,
downloads,
}
}
pub(crate) fn package_id(id: &str) -> String {
id.to_lowercase()
.chars()
.map(|c| match c {
'a'..='z' | '0'..='9' | '-' | '_' => c,
_ => '-',
})
.collect()
}
pub(crate) fn import_id<'a>(
entry: &'a CatalogEntry,
prefix: &str,
publisher: &str,
) -> Result<&'a str> {
match entry.source.prefix() == Some(prefix) {
true => entry
.source
.import_id()
.ok_or_else(|| LogoxideError::Import(format!("{}: no import id", entry.id))),
false => Err(LogoxideError::Import(format!(
"{} isn't from {publisher}",
entry.id
))),
}
}
pub(crate) struct Made {
pub(crate) format: &'static str,
pub(crate) license_file: Option<String>,
pub(crate) publisher: &'static str,
pub(crate) homepage: String,
pub(crate) versification: Option<String>,
pub(crate) keys: Option<Keys>,
}
pub(crate) fn finish(
entry: &CatalogEntry,
made: Made,
staged: MemoryStorage,
dest: &dyn Storage,
) -> Result<()> {
use logoxide::language::{script_direction, script_of_tag};
let script = (entry.script.clone()).or_else(|| script_of_tag(&entry.language));
let implied = script_direction(script.as_deref());
let info = Info {
id: entry.id.clone(),
name: entry.name.clone(),
abbreviation: entry.abbreviation.clone(),
authors: vec![],
description: entry.description.clone(),
aliases: entry.aliases.clone(),
version: IMPORT_VERSION,
kind: entry.kind,
language: entry.language.clone(),
language_name: entry.language_name.clone(),
script: entry.script.clone(),
direction: (entry.direction != implied).then_some(entry.direction),
license: (entry.license.clone()).map(|license| {
match logoxide::license::check_license(&license) {
Ok(()) => license,
Err(_) => format!("LicenseRef-{}", made.publisher.replace(' ', "-")),
}
}),
rights: (!entry.rights.is_empty()).then_some(entry.rights),
license_file: made.license_file,
publisher: Some(made.publisher.to_string()),
homepage: Some(made.homepage),
repository: None,
documentation: None,
readme: None,
keywords: entry.keywords.clone(),
categories: entry.categories.clone(),
about_books: entry.about_books.clone(),
work: (entry.work.clone()).or_else(|| logoxide::works::of_source(&entry.source)),
edition: entry.edition.clone(),
edition_year: entry.edition_year,
upstream: upstream(entry),
versification: made.versification.unwrap_or_else(|| String::from("own")),
maps_to: None,
keys: made.keys,
include: vec![],
};
let folder = match Format::from_name(made.format) {
Some(Format::Dictionary) => "entries",
_ => made.format,
};
let manifest = Manifest {
package: Some(info),
formats: [(
made.format.to_string(),
FormatEntry {
path: format!("{folder}/"),
feature: None,
},
)]
.into(),
..Manifest::default()
};
let text = toml::to_string(&manifest).map_err(|e| LogoxideError::Import(e.to_string()))?;
let header = format!("# Imported from {} by logoxide\n", made.publisher);
staged.write(MANIFEST, format!("{header}{text}").as_bytes())?;
let staged = std::sync::Arc::new(staged);
let package = Package::open(staged.clone(), Features::default())?;
let verses = (package.text_format()).is_some_and(|(f, _)| f.is_verse_text());
let book = entry.kind == logoxide::manifest::PackageKind::Book;
if verses && package.books().is_empty() {
return Err(LogoxideError::Import(format!("{}: no books", entry.id)));
}
if !verses && !book && package.source().keys().is_empty() {
return Err(LogoxideError::Import(format!("{}: no entries", entry.id)));
}
for path in staged.walk("")? {
dest.write(&path, &staged.read(&path)?)?;
}
Ok(())
}
#[cfg(test)]
pub(crate) mod tests {
use std::io::Write;
pub(crate) fn zip(files: &[(&str, &str)]) -> Vec<u8> {
zip_bytes(
&(files.iter())
.map(|(path, text)| (*path, text.as_bytes()))
.collect::<Vec<_>>(),
)
}
pub(crate) fn zip_bytes(files: &[(&str, &[u8])]) -> Vec<u8> {
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
for (path, bytes) in files {
writer
.start_file(*path, zip::write::SimpleFileOptions::default())
.unwrap();
writer.write_all(bytes).unwrap();
}
writer.finish().unwrap().into_inner()
}
pub(crate) fn build_twice(
entry: &logoxide::catalog::CatalogEntry,
files: &[(String, Vec<u8>)],
dest: &dyn logoxide::storage::Storage,
) {
super::build(entry, files, dest).unwrap();
use logoxide::storage::Storage;
let again = super::MemoryStorage::new();
super::build(entry, files, &again).unwrap();
assert_eq!(dest.walk("").unwrap(), again.walk("").unwrap());
for path in again.walk("").unwrap() {
assert_eq!(
dest.read(&path).unwrap(),
again.read(&path).unwrap(),
"{path}"
);
}
}
#[test]
fn what_isnt_imported_is_said() {
let catalog = logoxide::catalog::Catalog::parse_toml(
"[[package]]\nid = \"kjv\"\nname = \"KJV\"\nabbreviation = \"KJV\"\nlanguage = \"en\"\ngit = \"https://example.org/kjv\"\n",
)
.unwrap();
let entry = &catalog.entries[0];
assert!(super::downloads(entry).is_err());
assert!(super::build(entry, &[], &super::MemoryStorage::new()).is_err());
assert_eq!(super::upstream(entry), None);
assert_eq!(super::package_id("eng-ASV 1901"), "eng-asv-1901");
}
}