1use dictx_core::{DictEntry, DictxError, Result};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::path::Path;
5use std::sync::Mutex;
6
7pub const ENTRY_PACK_FILE: &str = "entries.dxp";
8const MAGIC: &[u8; 8] = b"DXPCK001";
9
10#[derive(Debug, Clone, Copy)]
11pub struct EntryLocator {
12 pub offset: u64,
13 pub len: u64,
14}
15
16pub struct EntryPackWriter {
17 file: File,
18}
19
20impl EntryPackWriter {
21 pub fn create(path: &Path) -> Result<Self> {
22 let mut file = File::create(path)?;
23 file.write_all(MAGIC)?;
24 Ok(Self { file })
25 }
26
27 pub fn append(&mut self, entry: &DictEntry) -> Result<EntryLocator> {
28 let bytes = rmp_serde::to_vec_named(entry)
29 .map_err(|err| DictxError::InvalidData(format!("词条二进制序列化失败: {err}")))?;
30 let offset = self.file.stream_position()?;
31 self.file.write_all(&(bytes.len() as u32).to_le_bytes())?;
32 self.file.write_all(&bytes)?;
33 Ok(EntryLocator {
34 offset,
35 len: bytes.len() as u64,
36 })
37 }
38
39 pub fn finish(mut self) -> Result<()> {
40 self.file.flush()?;
41 Ok(())
42 }
43}
44
45pub struct EntryPackReader {
46 file: Mutex<File>,
47}
48
49impl EntryPackReader {
50 pub fn open(path: &Path) -> Result<Self> {
51 let mut file = File::open(path)?;
52 let mut magic = [0u8; 8];
53 file.read_exact(&mut magic)?;
54 if &magic != MAGIC {
55 return Err(DictxError::InvalidData(format!(
56 "词条 pack 文件格式不正确: {}",
57 path.display()
58 )));
59 }
60 Ok(Self {
61 file: Mutex::new(file),
62 })
63 }
64
65 pub fn read(&self, locator: EntryLocator) -> Result<DictEntry> {
66 let mut file = self
67 .file
68 .lock()
69 .map_err(|_| DictxError::Message("词条 pack 读取锁已损坏".to_string()))?;
70 file.seek(SeekFrom::Start(locator.offset))?;
71
72 let mut len_buf = [0u8; 4];
73 file.read_exact(&mut len_buf)?;
74 let len = u32::from_le_bytes(len_buf) as u64;
75 if len != locator.len {
76 return Err(DictxError::InvalidData(format!(
77 "词条 pack 长度不一致: index={} pack={}",
78 locator.len, len
79 )));
80 }
81
82 let mut bytes = vec![0u8; len as usize];
83 file.read_exact(&mut bytes)?;
84 rmp_serde::from_slice(&bytes)
85 .map_err(|err| DictxError::InvalidData(format!("词条二进制反序列化失败: {err}")))
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use dictx_core::{Definition, DictSource};
93
94 #[test]
95 fn roundtrips_entry_pack() {
96 let dir = tempfile::tempdir().unwrap();
97 let path = dir.path().join(ENTRY_PACK_FILE);
98 let mut entry = DictEntry::new(DictSource::Custom { name: "t".into() }, "apple");
99 entry
100 .definitions
101 .push(Definition::new("fruit", "苹果", Some("n".into())));
102
103 let mut writer = EntryPackWriter::create(&path).unwrap();
104 let locator = writer.append(&entry).unwrap();
105 writer.finish().unwrap();
106
107 let reader = EntryPackReader::open(&path).unwrap();
108 let decoded = reader.read(locator).unwrap();
109 assert_eq!(decoded.word, "apple");
110 assert_eq!(decoded.definitions[0].zh, "苹果");
111 }
112}