use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::entry::{
mo_metadata_entry_to_string, MOEntry, MsgidEotMsgctxt,
};
use crate::errors::IOError;
use crate::file::{
metadata_hashmap_to_msgstr, pofile::POFile, AsBytes, FileOptions,
Save, SaveAsMOFile, SaveAsPOFile,
};
use crate::moparser::{MOFileParser, MAGIC, MAGIC_SWAPPED};
fn empty_msgctxt_predicate(_: &MOEntry, _: &str) -> bool {
true
}
fn msgctxt_predicate(entry: &MOEntry, msgctxt: &str) -> bool {
entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == msgctxt
}
fn by_msgid_predicate(entry: &MOEntry, value: &str) -> bool {
entry.msgid == value
}
fn by_msgstr_predicate(entry: &MOEntry, value: &str) -> bool {
entry.msgstr.as_ref().unwrap_or(&"".to_string()) == value
}
fn by_msgctxt_predicate(entry: &MOEntry, value: &str) -> bool {
entry.msgctxt.as_ref().unwrap_or(&"".to_string()) == value
}
fn by_msgid_plural_predicate(entry: &MOEntry, value: &str) -> bool {
entry.msgid_plural.as_ref().unwrap_or(&"".to_string()) == value
}
pub fn mofile<Opt>(options: Opt) -> Result<MOFile, IOError>
where
Opt: Into<FileOptions>,
{
let mut parser = MOFileParser::new(options.into());
parser.parse()?;
Ok(parser.file)
}
#[derive(Clone, Debug, PartialEq)]
pub struct MOFile {
pub magic_number: Option<u32>,
pub version: Option<u32>,
pub metadata: HashMap<String, String>,
pub entries: Vec<MOEntry>,
pub options: FileOptions,
}
impl MOFile {
pub fn new(options: FileOptions) -> Self {
Self {
options,
magic_number: None,
version: None,
metadata: HashMap::new(),
entries: Vec::new(),
}
}
pub fn metadata_as_entry(&self) -> MOEntry {
let mut entry =
MOEntry::new("".to_string(), None, None, vec![], None);
if !self.metadata.is_empty() {
entry.msgstr =
Some(metadata_hashmap_to_msgstr(&self.metadata))
}
entry
}
pub fn find(
&self,
value: &str,
by: &str,
msgctxt: Option<&str>,
) -> Vec<&MOEntry> {
let mut entries: Vec<&MOEntry> = Vec::new();
let msgctxt_predicate: &dyn Fn(&MOEntry, &str) -> bool =
match msgctxt {
Some(_) => &msgctxt_predicate,
None => &empty_msgctxt_predicate,
};
let by_predicate: &dyn Fn(&MOEntry, &str) -> bool = match by {
"msgid" => &by_msgid_predicate,
"msgstr" => &by_msgstr_predicate,
"msgctxt" => &by_msgctxt_predicate,
"msgid_plural" => &by_msgid_plural_predicate,
_ => &|_: &MOEntry, _: &str| false,
};
for entry in &self.entries {
if by_predicate(entry, value)
&& msgctxt_predicate(entry, msgctxt.unwrap_or(""))
{
entries.push(entry);
}
}
entries
}
pub fn find_by_msgid(&self, msgid: &str) -> Option<&MOEntry> {
self.entries.iter().find(|e| e.msgid == msgid)
}
pub fn find_by_msgid_msgctxt(
&self,
msgid: &str,
msgctxt: &str,
) -> Option<&MOEntry> {
self.entries.iter().find(|e| {
e.msgid == msgid && e.msgctxt == Some(msgctxt.to_string())
})
}
pub fn remove(&mut self, entry: &MOEntry) {
self.entries.retain(|e| e != entry);
}
pub fn remove_by_msgid(&mut self, msgid: &str) {
self.entries.retain(|e| e.msgid != msgid);
}
pub fn remove_by_msgid_msgctxt(
&mut self,
msgid: &str,
msgctxt: &str,
) {
self.entries.retain(|e| {
e.msgid != msgid
|| e.msgctxt.as_ref().unwrap_or(&"".to_string())
!= msgctxt
});
}
pub fn as_bytes_with(
&self,
magic_number: u32,
revision_number: u32,
) -> Cow<[u8]> {
let metadata_entry = self.metadata_as_entry();
let bytes_reader: fn(u32) -> [u8; 4] = match magic_number {
MAGIC_SWAPPED => u32::to_be_bytes,
_ => u32::to_le_bytes,
};
let mut entries: Vec<&MOEntry> = vec![&metadata_entry];
entries.extend(&self.entries);
entries.sort_unstable_by(|a, b| {
a.msgid_eot_msgctxt().cmp(&b.msgid_eot_msgctxt())
});
let entries_length = entries.len();
let mut offsets: Vec<(usize, usize, usize, usize)> = vec![];
let mut ids = "".to_string();
let mut strs = "".to_string();
for e in entries {
let mut msgid = "".to_string();
let mut msgstr = "".to_string();
if let Some(msgctxt) = &e.msgctxt {
msgid.push_str(msgctxt);
msgid.push('\u{4}');
}
if let Some(msgid_plural) = &e.msgid_plural {
msgid.push_str(&e.msgid);
msgid.push('\u{0}');
msgid.push_str(msgid_plural);
let msgstr_plural_length = &e.msgstr_plural.len();
for (i, v) in e.msgstr_plural.iter().enumerate() {
msgstr.push_str(v);
if i < msgstr_plural_length - 1 {
msgstr.push('\u{0}');
}
}
} else {
msgid.push_str(&e.msgid);
if let Some(m) = &e.msgstr {
msgstr.push_str(m);
}
}
offsets.push((
ids.len(),
msgid.len(),
strs.len(),
msgstr.len(),
));
ids.push_str(&msgid);
ids.push('\u{0}');
strs.push_str(&msgstr);
strs.push('\u{0}');
}
let keystart = 7 * 4 + 16 * entries_length;
let valuestart = keystart + ids.len();
let mut koffsets: Vec<(usize, usize)> = vec![];
let mut voffsets: Vec<(usize, usize)> = vec![];
for (o1, l1, o2, l2) in offsets {
koffsets.push((l1, o1 + keystart));
voffsets.push((l2, o2 + valuestart));
}
let mut final_offsets: Vec<u8> = vec![];
for (l, o) in koffsets {
final_offsets.extend(bytes_reader(l as u32));
final_offsets.extend(bytes_reader(o as u32));
}
for (l, o) in voffsets {
final_offsets.extend(bytes_reader(l as u32));
final_offsets.extend(bytes_reader(o as u32));
}
let mut output: Vec<u8> = Vec::with_capacity(
7 * 4 + 8 * entries_length + ids.len() + strs.len(),
);
output.extend(bytes_reader(MAGIC));
output.extend(bytes_reader(revision_number));
output.extend(bytes_reader(entries_length as u32));
output.extend(bytes_reader(7 * 4));
output.extend(bytes_reader(
7 * 4 + (entries_length as u32) * 8,
));
output.extend([0, 0, 0, 0]);
output.extend(bytes_reader(keystart as u32));
output.extend(final_offsets);
output.extend(ids.as_bytes());
output.extend(strs.as_bytes());
output.into()
}
}
impl fmt::Display for MOFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ret = String::from("#\n");
ret.push_str(&mo_metadata_entry_to_string(
&self.metadata_as_entry(),
));
ret.push('\n');
for entry in &self.entries {
ret.push_str(&entry.to_string());
ret.push('\n');
}
ret.remove(ret.len() - 1);
write!(f, "{}", ret)
}
}
impl SaveAsPOFile for MOFile {}
impl Save for MOFile {
fn save(&self, path: &str) {
let mut file = File::create(path).unwrap();
file.write_all(&self.as_bytes()).ok();
}
}
impl SaveAsMOFile for MOFile {
fn save_as_mofile(&self, path: &str) {
self.save(path);
}
}
impl AsBytes for MOFile {
fn as_bytes(&self) -> Cow<[u8]> {
self.as_bytes_with(MAGIC, 0)
}
fn as_bytes_le(&self) -> Cow<[u8]> {
self.as_bytes_with(MAGIC, 0)
}
fn as_bytes_be(&self) -> Cow<[u8]> {
self.as_bytes_with(MAGIC_SWAPPED, 0)
}
}
impl From<&POFile> for MOFile {
fn from(file: &POFile) -> MOFile {
let mut new_file = MOFile::new(file.options.clone());
new_file.metadata = file.metadata.clone();
new_file.entries = file
.translated_entries()
.iter()
.map(|e| MOEntry::from(*e))
.collect();
new_file
}
}
impl From<Vec<&MOEntry>> for MOFile {
fn from(entries: Vec<&MOEntry>) -> Self {
let mut file = MOFile::new("".into());
for entry in entries {
file.entries.push(entry.clone());
}
file
}
}
impl From<&Path> for MOFile {
fn from(path: &Path) -> Self {
MOFile::new(path.to_str().unwrap().into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pofile;
use std::fs;
use std::io::Read;
use std::path::Path;
use unicode_width::UnicodeWidthStr;
#[test]
fn mofile_test() {
let path = "tests-data/all.mo";
let file = mofile(path).unwrap();
assert_eq!(file.entries.len(), 7);
}
#[test]
fn mofile_metadata_as_entry() {
let path = "tests-data/all.mo";
let file = mofile(path).unwrap();
let entry = file.metadata_as_entry();
let msgstr = entry.msgstr.unwrap();
assert_eq!(entry.msgid, "");
assert_eq!(msgstr.lines().count(), 10);
let path = "tests-data/empty-metadata.mo";
let file = mofile(path).unwrap();
let entry = file.metadata_as_entry();
assert_eq!(entry.msgid, "");
assert_eq!(entry.msgstr.is_none(), true);
}
#[test]
fn mofile_from_pofile() {
let path = "tests-data/all.po";
let po_file = pofile(path).unwrap();
let mo_file = MOFile::from(&po_file);
assert_eq!(
mo_file.entries.len(),
po_file.translated_entries().len(),
);
assert_eq!(mo_file.metadata.len(), po_file.metadata.len());
}
#[test]
fn mofile_from_std_path() {
let file = MOFile::from(Path::new("tests-data/all.mo"));
assert_eq!(file.options.path_or_content, "tests-data/all.mo");
}
#[test]
fn mofile_to_string() {
let mo_path = "tests-data/all.mo";
let file = mofile(mo_path).unwrap();
let file_as_string = file.to_string();
for line in file_as_string.lines() {
let width = UnicodeWidthStr::width(line);
assert!(width <= file.options.wrapwidth + 2);
}
}
#[test]
fn mofile_as_bytes() {
let path = "tests-data/all.mo";
let file = mofile(path).unwrap();
let file_as_bytes = file.as_bytes();
let polib_path = "tests-data/all-polib.mo";
let polib_file = mofile(polib_path).unwrap();
let polib_file_as_bytes = polib_file.as_bytes();
assert_eq!(file_as_bytes.len(), polib_file_as_bytes.len());
for (rspolib_byte, polib_byte) in
file_as_bytes.iter().zip(polib_file_as_bytes.iter())
{
assert_eq!(rspolib_byte, polib_byte);
}
let buffer: Vec<u8> = fs::read(path).unwrap();
assert_ne!(file_as_bytes, buffer);
assert_ne!(polib_file_as_bytes, buffer);
assert!(file_as_bytes.len() < buffer.len());
assert!(polib_file_as_bytes.len() < buffer.len());
}
#[test]
fn mofile_save_as_pofile() {
let tmpdir = "tests-data/tests";
let path = "tests-data/all.mo";
let file = mofile(path).unwrap();
let file_as_string = file.to_string();
let tmp_path = Path::new(&tmpdir).join("all.po");
let tmp_path_str = tmp_path.to_str().unwrap();
file.save_as_pofile(tmp_path_str);
assert_eq!(
file_as_string,
fs::read_to_string(tmp_path_str).unwrap()
);
fs::remove_file(tmp_path_str).unwrap();
}
fn mofile_save_test(
basename: &str,
read_bytes_from_file: bool,
save_method_name: &str,
) {
let tmpdir = "tests-data/tests";
let path = "tests-data/all.mo";
let file = mofile(path).unwrap();
let tmp_path =
Path::new(&tmpdir).join(format!("{}.mo", basename));
let tmp_path_str = tmp_path.to_str().unwrap();
if save_method_name == "save" {
file.save(tmp_path_str);
} else {
file.save_as_mofile(tmp_path_str);
}
assert!(tmp_path.is_file());
let file_bytes = match read_bytes_from_file {
true => fs::read(tmp_path_str).unwrap(),
false => file.as_bytes().into_owned(),
};
let mut file_bytes = file_bytes.as_slice();
let mut buf: [u8; 4] = [0, 0, 0, 0];
file_bytes.read_exact(&mut buf).unwrap();
let magic_number = u32::from_le_bytes(buf);
assert_eq!(magic_number, MAGIC);
file_bytes.read_exact(&mut buf).unwrap();
let revision_number = u32::from_le_bytes(buf);
assert_eq!(revision_number, 0);
file_bytes.read_exact(&mut buf).unwrap();
let number_of_entries = u32::from_le_bytes(buf);
assert_eq!(
number_of_entries,
file.entries.len() as u32 + 1,
);
}
#[test]
fn mofile_save_as_mofile() {
mofile_save_test(
"mofile_save_as_mofile-file",
true,
"save_as_mofile",
);
mofile_save_test(
"mofile_save_as_mofile-struct",
false,
"save_as_mofile",
);
}
#[test]
fn mofile_save() {
mofile_save_test("mofile_save-file", true, "save");
mofile_save_test("mofile_save-struct", false, "save");
}
#[test]
fn remove() {
let mut entry_1 = MOEntry::from("msgid 1");
entry_1.msgstr = Some("msgstr 1".to_string());
let mut entry_2 = MOEntry::from("msgid 2");
entry_2.msgstr = Some("msgstr 2".to_string());
let mut file = MOFile::from(vec![&entry_1, &entry_2]);
assert_eq!(file.entries.len(), 2);
file.remove(&entry_1);
assert_eq!(file.entries.len(), 1);
assert_eq!(file.entries[0].msgid, "msgid 2");
file.entries.push(entry_1);
assert_eq!(file.entries.len(), 2);
file.remove_by_msgid("msgid 2");
assert_eq!(file.entries.len(), 1);
assert_eq!(file.entries[0].msgid, "msgid 1");
entry_2.msgctxt = Some("msgctxt 2".to_string());
entry_2.msgid = "msgid 1".to_string();
file.entries.push(entry_2);
assert_eq!(file.entries.len(), 2);
file.remove_by_msgid_msgctxt("msgid 1", "msgctxt 2");
assert_eq!(file.entries.len(), 1);
assert_eq!(file.entries[0].msgid, "msgid 1");
assert_eq!(
file.entries[0].msgstr.as_ref().unwrap(),
"msgstr 1",
);
}
#[test]
fn find() {
let mut entry_1 = MOEntry::from("msgid 1");
entry_1.msgstr = Some("msgstr 1".to_string());
let mut entry_2 = MOEntry::from("msgid 2");
entry_2.msgstr = Some("msgstr 2".to_string());
let mut file = MOFile::from(vec![&entry_1, &entry_2]);
assert_eq!(file.entries.len(), 2);
assert_eq!(
file.find_by_msgid("msgid 2").unwrap().msgid,
"msgid 2"
);
entry_2.msgctxt = Some("msgctxt 2".to_string());
entry_2.msgid = "msgid 1".to_string();
file.entries.push(entry_2);
assert_eq!(file.entries.len(), 3);
assert_eq!(
file.find_by_msgid_msgctxt("msgid 1", "msgctxt 2")
.unwrap()
.msgstr
.as_ref()
.unwrap(),
"msgstr 2",
);
}
}