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