Skip to main content

asc_hap/resource/
value.rs

1//! Resource types and the values stored for them.
2
3use std::borrow::Cow;
4use std::fmt;
5use std::str::FromStr;
6
7use super::reader::Reader;
8
9/// The type of a resource: the `string` of `$string:app_name` and of
10/// `$r('app.string.app_name')`.
11#[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    /// A type number this crate has no name for.
28    Other(u32),
29}
30
31struct TypeRow {
32    kind: ResourceType,
33    /// The number stored in the index.
34    code: u32,
35    name: &'static str,
36    /// The `type` of the object `$r(...)` compiles to, for the types that
37    /// have one.
38    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    /// From the number stored in the index.
68    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    /// The number stored in the index.
73    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    /// From the `type` of a compiled `$r(...)`: the compiler turns
81    /// `$r('app.string.x')` into the object literal
82    /// `{ id: 16777236, type: 10003, params: [], bundleName, moduleName }`,
83    /// where 10003 says string. `None` for a number that names no index
84    /// type, such as 30000 (`$rawfile`, which has no id).
85    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    /// The `type` a compiled `$r(...)` of this type carries.
90    pub fn object_code(self) -> Option<u32> {
91        self.row().and_then(|row| row.object_code)
92    }
93}
94
95/// The name used in references: `string`, `media`, `profile`. An unnamed
96/// type is `type<number>`.
97impl 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/// The text was not the name of a resource type.
107#[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    /// A type name (`string`), or `type<number>` as `Display` writes an
115    /// unnamed one.
116    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/// A reference to a resource by type and name, as `module.json` writes
128/// it (`$string:app_name`) or as source code does (`app.string.app_name`).
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct ResourceRef<'s> {
131    pub kind: ResourceType,
132    /// The resource name. Values inside the index refer by decimal id
133    /// instead (`$color:16777245`); [`ResourceIndex::resolve`] takes both.
134    ///
135    /// [`ResourceIndex::resolve`]: super::ResourceIndex::resolve
136    pub name: &'s str,
137}
138
139impl<'s> ResourceRef<'s> {
140    /// `$type:name` or `app.type.name`. `None` for anything else,
141    /// including system resources (`$ohos:string:x`, `sys.string.x`),
142    /// which no package's index holds.
143    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
155/// `$type:name`.
156impl 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/// A name with its value: a plural form, or an attribute of a theme or
163/// pattern.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Attr<'a> {
166    pub name: Cow<'a, str>,
167    pub value: Cow<'a, str>,
168}
169
170/// What the index stores for one resource under one qualifier set. Every
171/// value is text in the file; text that is not UTF-8 is read lossily.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum ResourceValue<'a> {
174    /// string, integer, boolean, color, float, symbol, id: the text as
175    /// written in the source (`#FFFFFF`, `50fp`, `true`), or a reference
176    /// to another resource by id (`$color:16777245`).
177    Text(Cow<'a, str>),
178    /// media, profile: where the file is, as the index spells it:
179    /// `entry/resources/base/media/icon.png`.
180    File(Cow<'a, str>),
181    /// strarray, intarray.
182    Array(Vec<Cow<'a, str>>),
183    /// plural: one text per quantity (`one`, `other`).
184    Plural(Vec<Attr<'a>>),
185    /// theme, pattern.
186    Map {
187        /// The theme or pattern this one extends, as a reference.
188        parent: Option<Cow<'a, str>>,
189        attrs: Vec<Attr<'a>>,
190    },
191    /// The bytes of a value of an unnamed type, or of a list that does
192    /// not read as one.
193    Raw(&'a [u8]),
194}
195
196impl<'a> ResourceValue<'a> {
197    /// `terminated` says the format ends every value with a NUL that its
198    /// length counts (the first index format does).
199    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                // An odd count is how the format says the first string is the parent.
222                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    /// The zip entry of a [`File`](ResourceValue::File) value. The index
234    /// prefixes the path with the module's name; the package does not.
235    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
247/// A list value: strings one after another, each a `u16` length, the
248/// bytes and a NUL the length does not count.
249fn 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
266/// A string in JSON quotes on one line: `\"`, `\\`, `\n`, `\t`, `\r`
267/// escaped and every other control character as `\u001b`.
268struct 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
299/// One line: text quoted as JSON (`"Bridge"`), a file path bare
300/// (`entry/resources/base/media/icon.png`), an array `["a", "b"]`, a
301/// plural `{"one": "%d file", "other": "%d files"}`, a theme or pattern
302/// the same with its parent first (`{parent: "$theme:16777220", ...}`),
303/// raw bytes as `<12 bytes>`.
304impl 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}