es_fluent_shared/
path_utils.rs1use crate::CanonicalLanguageIdentifierError;
4use crate::error::{EsFluentError, EsFluentResult};
5use std::path::Path;
6
7pub fn parse_language_entry(
11 entry: std::fs::DirEntry,
12) -> EsFluentResult<Option<unic_langid::LanguageIdentifier>> {
13 if !entry.file_type()?.is_dir() {
14 return Ok(None);
15 }
16
17 let raw_name = entry.file_name();
18 let name = raw_name.into_string().map_err(|raw| {
19 EsFluentError::IoError(std::io::Error::new(
20 std::io::ErrorKind::InvalidData,
21 format!("Assets directory contains a non UTF-8 entry: {:?}", raw),
22 ))
23 })?;
24
25 let lang = crate::parse_canonical_language_identifier(&name).map_err(|err| match err {
26 CanonicalLanguageIdentifierError::Invalid { source, .. } => {
27 EsFluentError::invalid_language_identifier(&name, format!("Parse error: {}", source))
28 },
29 CanonicalLanguageIdentifierError::IcuInvalid { details, .. } => {
30 EsFluentError::invalid_language_identifier(&name, format!("ICU parse error: {details}"))
31 },
32 CanonicalLanguageIdentifierError::NonCanonical { canonical, .. } => {
33 EsFluentError::invalid_language_identifier(
34 &name,
35 format!(
36 "Locale directory must use canonical BCP-47 casing '{}'",
37 canonical
38 ),
39 )
40 },
41 })?;
42
43 Ok(Some(lang))
44}
45
46pub fn validate_assets_dir(assets_dir: &Path) -> EsFluentResult<()> {
48 if !assets_dir.exists() {
49 return Err(EsFluentError::assets_not_found(assets_dir));
50 }
51
52 if !assets_dir.is_dir() {
53 return Err(EsFluentError::IoError(std::io::Error::new(
54 std::io::ErrorKind::InvalidInput,
55 format!("Assets path '{}' is not a directory", assets_dir.display()),
56 )));
57 }
58
59 Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 fn read_dir_entry_by_name(parent: &Path, name: &str) -> std::fs::DirEntry {
67 std::fs::read_dir(parent)
68 .expect("read_dir")
69 .filter_map(|entry| entry.ok())
70 .find(|entry| entry.file_name() == name)
71 .expect("entry not found")
72 }
73
74 #[test]
75 fn parse_language_entry_handles_non_directory_and_valid_directory() {
76 let temp = tempfile::tempdir().expect("tempdir");
77 std::fs::write(temp.path().join("file.txt"), "not a directory").expect("write");
78 std::fs::create_dir_all(temp.path().join("en-US")).expect("create lang");
79
80 let file_entry = read_dir_entry_by_name(temp.path(), "file.txt");
81 assert_eq!(parse_language_entry(file_entry).expect("parse file"), None);
82
83 let dir_entry = read_dir_entry_by_name(temp.path(), "en-US");
84 let parsed = parse_language_entry(dir_entry)
85 .expect("parse dir")
86 .expect("language id");
87 assert_eq!(
88 parsed,
89 "en-US"
90 .parse::<unic_langid::LanguageIdentifier>()
91 .expect("language")
92 );
93 }
94
95 #[test]
96 fn parse_language_entry_rejects_invalid_identifiers_and_accepts_variants() {
97 let temp = tempfile::tempdir().expect("tempdir");
98 std::fs::create_dir_all(temp.path().join("not-a-lang!")).expect("create invalid");
99 std::fs::create_dir_all(temp.path().join("de-DE-1901")).expect("create variant");
100
101 let invalid_entry = read_dir_entry_by_name(temp.path(), "not-a-lang!");
102 let invalid_err = parse_language_entry(invalid_entry).expect_err("should fail");
103 assert!(matches!(
104 invalid_err,
105 EsFluentError::InvalidLanguageIdentifier { .. }
106 ));
107
108 let variant_entry = read_dir_entry_by_name(temp.path(), "de-DE-1901");
109 let variant_lang = parse_language_entry(variant_entry)
110 .expect("variant locale should parse")
111 .expect("language id");
112 assert_eq!(
113 variant_lang,
114 "de-DE-1901"
115 .parse::<unic_langid::LanguageIdentifier>()
116 .expect("language")
117 );
118 }
119
120 #[test]
121 fn parse_language_entry_rejects_noncanonical_locale_casing() {
122 let temp = tempfile::tempdir().expect("tempdir");
123 std::fs::create_dir_all(temp.path().join("en-us")).expect("create noncanonical");
124
125 let entry = read_dir_entry_by_name(temp.path(), "en-us");
126 let err = parse_language_entry(entry).expect_err("noncanonical locale should fail");
127 assert!(matches!(
128 err,
129 EsFluentError::InvalidLanguageIdentifier { .. }
130 ));
131 assert!(err.to_string().contains("en-US"));
132 }
133
134 #[test]
135 fn validate_assets_dir_checks_missing_file_and_directory() {
136 let temp = tempfile::tempdir().expect("tempdir");
137 let missing = temp.path().join("missing");
138 let file = temp.path().join("file.txt");
139 let dir = temp.path().join("assets");
140 std::fs::write(&file, "x").expect("write");
141 std::fs::create_dir_all(&dir).expect("mkdir");
142
143 let missing_err = validate_assets_dir(&missing).expect_err("missing should fail");
144 assert!(matches!(missing_err, EsFluentError::AssetsNotFound { .. }));
145
146 let file_err = validate_assets_dir(&file).expect_err("file should fail");
147 assert!(matches!(file_err, EsFluentError::IoError(_)));
148
149 validate_assets_dir(&dir).expect("directory should validate");
150 }
151}