1use std::borrow::Cow;
4use std::fmt;
5use std::str::FromStr;
6
7use super::reader::Reader;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ResourceType {
13 Integer,
14 String,
15 StrArray,
16 IntArray,
17 Boolean,
18 Color,
19 Id,
20 Theme,
21 Plural,
22 Float,
23 Media,
24 Profile,
25 Pattern,
26 Symbol,
27 Other(u32),
29}
30
31struct TypeRow {
32 kind: ResourceType,
33 code: u32,
35 name: &'static str,
36 object_code: Option<u32>,
39}
40
41const fn row(kind: ResourceType, code: u32, name: &'static str, object_code: Option<u32>) -> TypeRow {
42 TypeRow { kind, code, name, object_code }
43}
44
45const TYPES: &[TypeRow] = &[
46 row(ResourceType::Integer, 8, "integer", Some(10007)),
47 row(ResourceType::String, 9, "string", Some(10003)),
48 row(ResourceType::StrArray, 10, "strarray", Some(10009)),
49 row(ResourceType::IntArray, 11, "intarray", Some(10006)),
50 row(ResourceType::Boolean, 12, "boolean", Some(10005)),
51 row(ResourceType::Color, 14, "color", Some(10001)),
52 row(ResourceType::Id, 15, "id", None),
53 row(ResourceType::Theme, 16, "theme", None),
54 row(ResourceType::Plural, 17, "plural", Some(10004)),
55 row(ResourceType::Float, 18, "float", Some(10002)),
56 row(ResourceType::Media, 19, "media", Some(20000)),
57 row(ResourceType::Profile, 20, "profile", None),
58 row(ResourceType::Pattern, 22, "pattern", Some(10008)),
59 row(ResourceType::Symbol, 23, "symbol", Some(40000)),
60];
61
62impl ResourceType {
63 fn row(self) -> Option<&'static TypeRow> {
64 TYPES.iter().find(|row| row.kind == self)
65 }
66
67 pub fn from_code(code: u32) -> ResourceType {
69 TYPES.iter().find(|row| row.code == code).map_or(ResourceType::Other(code), |row| row.kind)
70 }
71
72 pub fn code(self) -> u32 {
74 match self {
75 ResourceType::Other(code) => code,
76 named => named.row().map_or(0, |row| row.code),
77 }
78 }
79
80 pub fn from_object_code(code: u32) -> Option<ResourceType> {
86 TYPES.iter().find(|row| row.object_code == Some(code)).map(|row| row.kind)
87 }
88
89 pub fn object_code(self) -> Option<u32> {
91 self.row().and_then(|row| row.object_code)
92 }
93}
94
95impl fmt::Display for ResourceType {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self.row() {
100 Some(row) => f.write_str(row.name),
101 None => write!(f, "type{}", self.code()),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
108#[error("unknown resource type {0:?}")]
109pub struct UnknownResourceType(String);
110
111impl FromStr for ResourceType {
112 type Err = UnknownResourceType;
113
114 fn from_str(s: &str) -> Result<ResourceType, UnknownResourceType> {
117 if let Some(row) = TYPES.iter().find(|row| row.name == s) {
118 return Ok(row.kind);
119 }
120 s.strip_prefix("type")
121 .and_then(|code| code.parse().ok())
122 .map(ResourceType::from_code)
123 .ok_or_else(|| UnknownResourceType(s.to_owned()))
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct ResourceRef<'s> {
131 pub kind: ResourceType,
132 pub name: &'s str,
137}
138
139impl<'s> ResourceRef<'s> {
140 pub fn parse(text: &'s str) -> Option<ResourceRef<'s>> {
144 let (kind, name) = match text.strip_prefix('$') {
145 Some(rest) => rest.split_once(':')?,
146 None => text.strip_prefix("app.")?.split_once('.')?,
147 };
148 if name.is_empty() {
149 return None;
150 }
151 Some(ResourceRef { kind: kind.parse().ok()?, name })
152 }
153}
154
155impl fmt::Display for ResourceRef<'_> {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write!(f, "${}:{}", self.kind, self.name)
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Attr<'a> {
166 pub name: Cow<'a, str>,
167 pub value: Cow<'a, str>,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum ResourceValue<'a> {
174 Text(Cow<'a, str>),
178 File(Cow<'a, str>),
181 Array(Vec<Cow<'a, str>>),
183 Plural(Vec<Attr<'a>>),
185 Map {
187 parent: Option<Cow<'a, str>>,
189 attrs: Vec<Attr<'a>>,
190 },
191 Raw(&'a [u8]),
194}
195
196impl<'a> ResourceValue<'a> {
197 pub(super) fn decode(kind: ResourceType, bytes: &'a [u8], terminated: bool) -> ResourceValue<'a> {
200 let bytes = match bytes.split_last() {
201 Some((0, rest)) if terminated => rest,
202 _ => bytes,
203 };
204 let text = || String::from_utf8_lossy(bytes);
205 let raw = ResourceValue::Raw(bytes);
206 match kind {
207 ResourceType::Integer
208 | ResourceType::String
209 | ResourceType::Boolean
210 | ResourceType::Color
211 | ResourceType::Id
212 | ResourceType::Float
213 | ResourceType::Symbol => ResourceValue::Text(text()),
214 ResourceType::Media | ResourceType::Profile => ResourceValue::File(text()),
215 ResourceType::StrArray | ResourceType::IntArray => strings(bytes).map_or(raw, ResourceValue::Array),
216 ResourceType::Plural => match strings(bytes) {
217 Some(items) if items.len() % 2 == 0 => ResourceValue::Plural(pairs(items)),
218 _ => raw,
219 },
220 ResourceType::Theme | ResourceType::Pattern => match strings(bytes) {
221 Some(mut items) if items.len() % 2 == 1 => {
223 let parent = items.remove(0);
224 ResourceValue::Map { parent: Some(parent), attrs: pairs(items) }
225 }
226 Some(items) => ResourceValue::Map { parent: None, attrs: pairs(items) },
227 None => raw,
228 },
229 ResourceType::Other(_) => raw,
230 }
231 }
232
233 pub fn archive_path(&self) -> Option<&str> {
236 match self {
237 ResourceValue::File(path) => Some(path.split_once('/').map_or(path.as_ref(), |(_, rest)| rest)),
238 ResourceValue::Text(_)
239 | ResourceValue::Array(_)
240 | ResourceValue::Plural(_)
241 | ResourceValue::Map { .. }
242 | ResourceValue::Raw(_) => None,
243 }
244 }
245}
246
247fn strings(bytes: &[u8]) -> Option<Vec<Cow<'_, str>>> {
250 let mut reader = Reader::new(bytes);
251 let mut items = Vec::new();
252 while reader.remaining() > 0 {
253 items.push(String::from_utf8_lossy(reader.sized16("list item").ok()?));
254 if reader.take(1, "list item terminator").ok()? != [0] {
255 return None;
256 }
257 }
258 Some(items)
259}
260
261fn pairs(items: Vec<Cow<'_, str>>) -> Vec<Attr<'_>> {
262 let mut items = items.into_iter();
263 std::iter::from_fn(|| Some(Attr { name: items.next()?, value: items.next()? })).collect()
264}
265
266struct Quoted<'s>(&'s str);
269
270impl fmt::Display for Quoted<'_> {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 f.write_str("\"")?;
273 for c in self.0.chars() {
274 match c {
275 '"' => f.write_str("\\\"")?,
276 '\\' => f.write_str("\\\\")?,
277 '\n' => f.write_str("\\n")?,
278 '\t' => f.write_str("\\t")?,
279 '\r' => f.write_str("\\r")?,
280 c if c.is_control() => write!(f, "\\u{:04x}", u32::from(c))?,
281 c => write!(f, "{c}")?,
282 }
283 }
284 f.write_str("\"")
285 }
286}
287
288fn write_attrs(f: &mut fmt::Formatter<'_>, attrs: &[Attr<'_>], mut first: bool) -> fmt::Result {
289 for attr in attrs {
290 if !first {
291 f.write_str(", ")?;
292 }
293 first = false;
294 write!(f, "{}: {}", Quoted(&attr.name), Quoted(&attr.value))?;
295 }
296 Ok(())
297}
298
299impl fmt::Display for ResourceValue<'_> {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self {
307 ResourceValue::Text(text) => write!(f, "{}", Quoted(text)),
308 ResourceValue::File(path) => f.write_str(path),
309 ResourceValue::Array(items) => {
310 f.write_str("[")?;
311 for (i, item) in items.iter().enumerate() {
312 if i > 0 {
313 f.write_str(", ")?;
314 }
315 write!(f, "{}", Quoted(item))?;
316 }
317 f.write_str("]")
318 }
319 ResourceValue::Plural(attrs) => {
320 f.write_str("{")?;
321 write_attrs(f, attrs, true)?;
322 f.write_str("}")
323 }
324 ResourceValue::Map { parent, attrs } => {
325 f.write_str("{")?;
326 if let Some(parent) = parent {
327 write!(f, "parent: {}", Quoted(parent))?;
328 }
329 write_attrs(f, attrs, parent.is_none())?;
330 f.write_str("}")
331 }
332 ResourceValue::Raw(bytes) => write!(f, "<{} bytes>", bytes.len()),
333 }
334 }
335}