1use std::{
2 borrow::Cow,
3 collections::HashMap,
4 fmt,
5 io::Read as _,
6 path::{Path, PathBuf},
7 str::FromStr as _,
8 sync::Arc,
9};
10
11use semver::Version;
12use zng_clone_move::clmv;
13use zng_txt::Txt;
14use zng_var::{ArcEq, Var, WeakVar, const_var, var, weak_var};
15
16use crate::{FluentParserErrors, L10nSource, Lang, LangFilePath, LangMap, LangResourceStatus};
17
18pub struct L10nTar {
25 data: L10nTarData,
26 available_langs: Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
27 available_langs_status: Var<LangResourceStatus>,
28 res: HashMap<(Lang, LangFilePath), L10nEntry>,
29}
30impl L10nTar {
31 pub fn load(data: impl Into<L10nTarData>) -> Self {
33 Self::load_impl(data.into())
34 }
35 fn load_impl(data: L10nTarData) -> Self {
36 let r = Self {
37 data,
38 available_langs: var(Arc::new(LangMap::new())),
39 available_langs_status: var(LangResourceStatus::Loading),
40 res: HashMap::default(),
41 };
42 r.load_available_langs();
43 r
44 }
45 fn load_available_langs(&self) {
46 let status = self.available_langs_status.clone();
47 let map = self.available_langs.clone();
48 let data = self.data.clone();
49 zng_task::spawn_wait(move || {
50 let r = (|| -> std::io::Result<_> {
51 let mut set: LangMap<HashMap<LangFilePath, PathBuf>> = LangMap::new();
52 let mut errors: Vec<Arc<dyn std::error::Error + Send + Sync>> = vec![];
53 let data = data.decode_bytes()?;
55 let data: &[u8] = &data;
56 let mut archive = tar::Archive::new(std::io::Cursor::new(data));
57 let entries = archive.entries_with_seek()?;
58 for entry in entries {
59 let entry = entry?;
60 let ty = entry.header().entry_type();
61 let entry = entry.path()?;
62
63 const EXT: unicase::Ascii<&'static str> = unicase::Ascii::new("ftl");
64
65 let is_ftl = ty.is_file()
66 && entry
67 .file_name()
68 .and_then(|s| s.to_str())
69 .and_then(|n| n.rsplit_once('.'))
70 .map(|(_, ext)| ext.is_ascii() && unicase::Ascii::new(ext) == EXT)
71 .unwrap_or(false);
72
73 if !is_ftl {
74 continue;
75 }
76
77 let utf8_path: Vec<_> = entry.iter().take(6).map(|s| s.to_str().unwrap_or("")).collect();
78 let utf8_path = &utf8_path[1..];
79
80 let (lang, mut file) = match utf8_path.len() {
81 2 => {
83 let lang = utf8_path[0];
84 let file_str = utf8_path[1].rsplit_once('.').unwrap().0;
85 let file = Txt::from_str(if file_str == "_" { "" } else { file_str });
86 (lang, LangFilePath::current_app(file))
87 }
88 5 => {
90 if utf8_path[1] != "deps" {
91 continue;
92 }
93 let lang = utf8_path[0];
94 let pkg_name = Txt::from_str(utf8_path[2]);
95 let pkg_version: Version = match utf8_path[3].parse() {
96 Ok(v) => v,
97 Err(e) => {
98 errors.push(Arc::new(e));
99 continue;
100 }
101 };
102 let file_str = utf8_path[4].rsplit_once('.').unwrap().0;
103 let file = Txt::from_str(if file_str == "_" { "" } else { file_str });
104
105 (lang, LangFilePath::new(pkg_name, pkg_version, file))
106 }
107 _ => {
108 continue;
109 }
110 };
111
112 let lang = match Lang::from_str(lang) {
113 Ok(l) => l,
114 Err(e) => {
115 errors.push(Arc::new(e));
116 continue;
117 }
118 };
119
120 if file.file == "_" {
121 file.file = "".into();
122 }
123
124 set.get_exact_or_insert(lang, Default::default)
125 .insert(file, entry.as_ref().to_owned());
126 }
127 for m in set.values_mut() {
128 m.shrink_to_fit();
129 }
130 set.shrink_to_fit();
131 map.set(set);
132 Ok(errors)
133 })();
134 match r {
135 Ok(e) => {
136 if e.is_empty() {
137 status.set(LangResourceStatus::Loaded)
138 } else {
139 let e = LangResourceStatus::Errors(e);
140 tracing::error!("'loading available' {e}");
141 status.set(e)
142 }
143 }
144 Err(e) => {
145 tracing::error!("failed to load tar, {e}");
146 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]))
147 }
148 }
149 });
150 }
151}
152impl L10nSource for L10nTar {
153 fn available_langs(&mut self) -> Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>> {
154 self.available_langs.read_only()
155 }
156
157 fn available_langs_status(&mut self) -> Var<LangResourceStatus> {
158 self.available_langs_status.read_only()
159 }
160
161 fn lang_resource(&mut self, lang: Lang, file: LangFilePath) -> Var<Option<ArcEq<fluent::FluentResource>>> {
162 match self.res.entry((lang, file)) {
163 std::collections::hash_map::Entry::Occupied(mut e) => {
164 if let Some(out) = e.get().res.upgrade() {
165 out.read_only()
166 } else {
167 let (lang, file) = e.key();
168 let out = resource_var(
169 self.data.clone(),
170 &self.available_langs,
171 e.get().status.clone(),
172 lang.clone(),
173 file.clone(),
174 );
175 e.get_mut().res = out.downgrade();
176 out
177 }
178 }
179 std::collections::hash_map::Entry::Vacant(e) => {
180 let mut f = L10nEntry::new();
181 let (lang, file) = e.key();
182 let out = resource_var(
183 self.data.clone(),
184 &self.available_langs,
185 f.status.clone(),
186 lang.clone(),
187 file.clone(),
188 );
189 f.res = out.downgrade();
190 e.insert(f);
191 out
192 }
193 }
194 }
195
196 fn lang_resource_status(&mut self, lang: Lang, file: LangFilePath) -> Var<LangResourceStatus> {
197 self.res.entry((lang, file)).or_insert_with(L10nEntry::new).status.read_only()
198 }
199}
200
201#[derive(Clone, PartialEq, Eq)]
203pub enum L10nTarData {
204 Static(&'static [u8]),
206 Arc(Arc<Vec<u8>>),
208}
209impl fmt::Debug for L10nTarData {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::Static(_) => f.debug_tuple("Static").finish_non_exhaustive(),
213 Self::Arc(_) => f.debug_tuple("Arc").finish_non_exhaustive(),
214 }
215 }
216}
217impl From<&'static [u8]> for L10nTarData {
218 fn from(value: &'static [u8]) -> Self {
219 L10nTarData::Static(value)
220 }
221}
222impl From<Arc<Vec<u8>>> for L10nTarData {
223 fn from(value: Arc<Vec<u8>>) -> Self {
224 L10nTarData::Arc(value)
225 }
226}
227impl From<Vec<u8>> for L10nTarData {
228 fn from(value: Vec<u8>) -> Self {
229 L10nTarData::Arc(Arc::new(value))
230 }
231}
232impl L10nTarData {
233 pub fn bytes(&self) -> &[u8] {
235 match self {
236 L10nTarData::Static(b) => b,
237 L10nTarData::Arc(b) => b,
238 }
239 }
240
241 pub fn is_gz(&self) -> bool {
245 let bytes = self.bytes();
246 bytes.len() > 2 && bytes[0..2] == [0x1F, 0x8B]
247 }
248
249 pub fn is_zst(&self) -> bool {
253 let bytes = self.bytes();
254 bytes.len() > 4 && bytes[0..4] == [0x28, 0xB5, 0x2F, 0xFD]
255 }
256
257 pub fn decode_bytes(&self) -> std::io::Result<Cow<'_, [u8]>> {
259 if self.is_gz() {
260 let bytes = self.bytes();
261 let mut data = vec![];
262 let mut decoder = flate2::read::GzDecoder::new(bytes);
263 decoder.read_to_end(&mut data)?;
264 Ok(Cow::Owned(data))
265 } else if self.is_zst() {
266 let bytes = self.bytes();
267 let mut data = vec![];
268 let mut decoder = zstd::Decoder::new(bytes)?;
269 decoder.read_to_end(&mut data)?;
270 Ok(Cow::Owned(data))
271 } else {
272 Ok(Cow::Borrowed(self.bytes()))
273 }
274 }
275
276 pub fn extract(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
282 self.extract_impl(path.as_ref())
283 }
284 fn extract_impl(&self, path: &Path) -> std::io::Result<()> {
285 let data = self.decode_bytes()?;
286 let mut data = tar::Archive::new(&*data);
287 data.unpack(path)
288 }
289}
290
291struct L10nEntry {
292 res: WeakVar<Option<ArcEq<fluent::FluentResource>>>,
293 status: Var<LangResourceStatus>,
294}
295impl L10nEntry {
296 fn new() -> Self {
297 Self {
298 res: weak_var(),
299 status: var(LangResourceStatus::Loading),
300 }
301 }
302}
303
304fn resource_var(
305 data: L10nTarData,
306 available_langs: &Var<Arc<LangMap<HashMap<LangFilePath, PathBuf>>>>,
307 status: Var<LangResourceStatus>,
308 lang: Lang,
309 file: LangFilePath,
310) -> Var<Option<ArcEq<fluent::FluentResource>>> {
311 available_langs
312 .map(move |w| w.get_file(&lang, &file).cloned())
313 .flat_map(move |p| match p {
314 Some(p) => {
315 status.set(LangResourceStatus::Loading);
316 let rsp = zng_task::wait_respond(clmv!(p, status, data, || {
317 const E: &str = "already decoded ok once to get entries";
318 let data = data.decode_bytes().expect(E);
319 let data: &[u8] = &data;
320 let mut archive = tar::Archive::new(std::io::Cursor::new(data));
321 for entry in archive.entries_with_seek().expect(E) {
322 let mut entry = entry.expect(E);
323 if entry.path().map(|ep| ep == p).unwrap_or(false) {
324 let mut flt = String::new();
325 if let Err(e) = entry.read_to_string(&mut flt) {
326 tracing::error!("error reading fluent resource, {e}");
327 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
328 } else {
329 match fluent::FluentResource::try_new(flt) {
330 Ok(flt) => {
331 return Some(ArcEq::new(flt));
334 }
335 Err(e) => {
336 let e = FluentParserErrors(e.1);
337 tracing::error!("error parsing fluent resource, {e}");
338 status.set(LangResourceStatus::Errors(vec![Arc::new(e)]));
339 }
340 }
341 }
342 return None;
343 }
344 }
345 status.set(LangResourceStatus::NotAvailable);
346 None
347 }));
348 rsp.bind_filter_map(&status, |r| r.done().and_then(|r| r.as_ref()).map(|_| LangResourceStatus::Loaded))
349 .perm();
350 rsp.map(|r| r.done().cloned().flatten())
351 }
352 None => const_var(None),
353 })
354}