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)]
12#[non_exhaustive]
13pub enum ResourceType {
14    /// `integer`: code 8.
15    Integer,
16    /// `string`: code 9.
17    String,
18    /// `strarray`: an array of strings, code 10.
19    StrArray,
20    /// `intarray`: an array of integers, code 11.
21    IntArray,
22    /// `boolean`: code 12.
23    Boolean,
24    /// `color`: code 14.
25    Color,
26    /// `id`: code 15.
27    Id,
28    /// `theme`: a set of attributes, code 16.
29    Theme,
30    /// `plural`: one string per quantity, code 17.
31    Plural,
32    /// `float`: code 18.
33    Float,
34    /// `media`: an image or other file, code 19.
35    Media,
36    /// `profile`: a JSON file under `resources/base/profile`, code 20.
37    Profile,
38    /// `pattern`: a set of style attributes, code 22.
39    Pattern,
40    /// `symbol`: a symbol glyph, code 23.
41    Symbol,
42    /// A type number this crate has no name for.
43    Other(u32),
44}
45
46struct TypeRow {
47    kind: ResourceType,
48    /// The number stored in the index.
49    code: u32,
50    name: &'static str,
51    /// The `type` of the object `$r(...)` compiles to, for the types that
52    /// have one.
53    object_code: Option<u32>,
54}
55
56const fn row(kind: ResourceType, code: u32, name: &'static str, object_code: Option<u32>) -> TypeRow {
57    TypeRow { kind, code, name, object_code }
58}
59
60const TYPES: &[TypeRow] = &[
61    row(ResourceType::Integer, 8, "integer", Some(10007)),
62    row(ResourceType::String, 9, "string", Some(10003)),
63    row(ResourceType::StrArray, 10, "strarray", Some(10009)),
64    row(ResourceType::IntArray, 11, "intarray", Some(10006)),
65    row(ResourceType::Boolean, 12, "boolean", Some(10005)),
66    row(ResourceType::Color, 14, "color", Some(10001)),
67    row(ResourceType::Id, 15, "id", None),
68    row(ResourceType::Theme, 16, "theme", None),
69    row(ResourceType::Plural, 17, "plural", Some(10004)),
70    row(ResourceType::Float, 18, "float", Some(10002)),
71    row(ResourceType::Media, 19, "media", Some(20000)),
72    row(ResourceType::Profile, 20, "profile", None),
73    row(ResourceType::Pattern, 22, "pattern", Some(10008)),
74    row(ResourceType::Symbol, 23, "symbol", Some(40000)),
75];
76
77impl ResourceType {
78    fn row(self) -> Option<&'static TypeRow> {
79        TYPES.iter().find(|row| row.kind == self)
80    }
81
82    /// From the number stored in the index.
83    pub fn from_code(code: u32) -> ResourceType {
84        TYPES.iter().find(|row| row.code == code).map_or(ResourceType::Other(code), |row| row.kind)
85    }
86
87    /// The number stored in the index.
88    pub fn code(self) -> u32 {
89        match self {
90            ResourceType::Other(code) => code,
91            named => named.row().map_or(0, |row| row.code),
92        }
93    }
94
95    /// From the `type` of a compiled `$r(...)`: the compiler turns
96    /// `$r('app.string.x')` into the object literal
97    /// `{ id: 16777236, type: 10003, params: [], bundleName, moduleName }`,
98    /// where 10003 says string. `None` for a number that names no index
99    /// type, such as 30000 (`$rawfile`, which has no id).
100    pub fn from_object_code(code: u32) -> Option<ResourceType> {
101        TYPES.iter().find(|row| row.object_code == Some(code)).map(|row| row.kind)
102    }
103
104    /// The `type` a compiled `$r(...)` of this type carries.
105    pub fn object_code(self) -> Option<u32> {
106        self.row().and_then(|row| row.object_code)
107    }
108}
109
110/// The name used in references: `string`, `media`, `profile`. An unnamed
111/// type is `type<number>`.
112impl fmt::Display for ResourceType {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self.row() {
115            Some(row) => f.write_str(row.name),
116            None => write!(f, "type{}", self.code()),
117        }
118    }
119}
120
121/// The text was not the name of a resource type.
122#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
123#[error("unknown resource type {0:?}")]
124pub struct UnknownResourceType(String);
125
126impl FromStr for ResourceType {
127    type Err = UnknownResourceType;
128
129    /// A type name (`string`), or `type<number>` as `Display` writes an
130    /// unnamed one.
131    fn from_str(s: &str) -> Result<ResourceType, UnknownResourceType> {
132        if let Some(row) = TYPES.iter().find(|row| row.name == s) {
133            return Ok(row.kind);
134        }
135        s.strip_prefix("type")
136            .and_then(|code| code.parse().ok())
137            .map(ResourceType::from_code)
138            .ok_or_else(|| UnknownResourceType(s.to_owned()))
139    }
140}
141
142/// A reference to a resource by type and name, as `module.json` writes
143/// it (`$string:app_name`) or as source code does (`app.string.app_name`).
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct ResourceRef<'s> {
146    /// The resource type.
147    pub kind: ResourceType,
148    /// The resource name. Values inside the index refer by decimal id
149    /// instead (`$color:16777245`); [`ResourceIndex::resolve`] takes both.
150    ///
151    /// [`ResourceIndex::resolve`]: super::ResourceIndex::resolve
152    pub name: &'s str,
153}
154
155impl<'s> ResourceRef<'s> {
156    /// `$type:name` or `app.type.name`. `None` for anything else,
157    /// including system resources (`$ohos:string:x`, `sys.string.x`),
158    /// which no package's index holds.
159    pub fn parse(text: &'s str) -> Option<ResourceRef<'s>> {
160        let (kind, name) = match text.strip_prefix('$') {
161            Some(rest) => rest.split_once(':')?,
162            None => text.strip_prefix("app.")?.split_once('.')?,
163        };
164        if name.is_empty() {
165            return None;
166        }
167        Some(ResourceRef { kind: kind.parse().ok()?, name })
168    }
169}
170
171/// `$type:name`.
172impl fmt::Display for ResourceRef<'_> {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "${}:{}", self.kind, self.name)
175    }
176}
177
178/// A name with its value: a plural form, or an attribute of a theme or
179/// pattern.
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[non_exhaustive]
182pub struct Attr<'a> {
183    /// The plural quantity (`one`, `other`) or the attribute name.
184    pub name: Cow<'a, str>,
185    /// The text for that quantity or attribute, as stored.
186    pub value: Cow<'a, str>,
187}
188
189/// What the index stores for one resource under one qualifier set. Every
190/// value is text in the file; text that is not UTF-8 is read lossily.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum ResourceValue<'a> {
193    /// string, integer, boolean, color, float, symbol, id: the text as
194    /// written in the source (`#FFFFFF`, `50fp`, `true`), or a reference
195    /// to another resource by id (`$color:16777245`).
196    Text(Cow<'a, str>),
197    /// media, profile: where the file is, as the index spells it:
198    /// `entry/resources/base/media/icon.png`.
199    File(Cow<'a, str>),
200    /// strarray, intarray.
201    Array(Vec<Cow<'a, str>>),
202    /// plural: one text per quantity (`one`, `other`).
203    Plural(Vec<Attr<'a>>),
204    /// theme, pattern.
205    Map {
206        /// The theme or pattern this one extends, as a reference.
207        parent: Option<Cow<'a, str>>,
208        /// The attributes in file order.
209        attrs: Vec<Attr<'a>>,
210    },
211    /// The bytes of a value of an unnamed type, or of a list that does
212    /// not read as one.
213    Raw(&'a [u8]),
214}
215
216impl<'a> ResourceValue<'a> {
217    /// `terminated` says the format ends every value with a NUL that its
218    /// length counts (the first index format does).
219    pub(super) fn decode(kind: ResourceType, bytes: &'a [u8], terminated: bool) -> ResourceValue<'a> {
220        let bytes = match bytes.split_last() {
221            Some((0, rest)) if terminated => rest,
222            _ => bytes,
223        };
224        let text = || String::from_utf8_lossy(bytes);
225        let raw = ResourceValue::Raw(bytes);
226        match kind {
227            ResourceType::Integer
228            | ResourceType::String
229            | ResourceType::Boolean
230            | ResourceType::Color
231            | ResourceType::Id
232            | ResourceType::Float
233            | ResourceType::Symbol => ResourceValue::Text(text()),
234            ResourceType::Media | ResourceType::Profile => ResourceValue::File(text()),
235            ResourceType::StrArray | ResourceType::IntArray => strings(bytes).map_or(raw, ResourceValue::Array),
236            ResourceType::Plural => match strings(bytes) {
237                Some(items) if items.len() % 2 == 0 => ResourceValue::Plural(pairs(items)),
238                _ => raw,
239            },
240            ResourceType::Theme | ResourceType::Pattern => match strings(bytes) {
241                // An odd count is how the format says the first string is the parent.
242                Some(mut items) if items.len() % 2 == 1 => {
243                    let parent = items.remove(0);
244                    ResourceValue::Map { parent: Some(parent), attrs: pairs(items) }
245                }
246                Some(items) => ResourceValue::Map { parent: None, attrs: pairs(items) },
247                None => raw,
248            },
249            ResourceType::Other(_) => raw,
250        }
251    }
252
253    /// The zip entry of a [`File`](ResourceValue::File) value. The index
254    /// prefixes the path with the module's name; the package does not.
255    pub fn archive_path(&self) -> Option<&str> {
256        match self {
257            ResourceValue::File(path) => Some(path.split_once('/').map_or(path.as_ref(), |(_, rest)| rest)),
258            ResourceValue::Text(_)
259            | ResourceValue::Array(_)
260            | ResourceValue::Plural(_)
261            | ResourceValue::Map { .. }
262            | ResourceValue::Raw(_) => None,
263        }
264    }
265}
266
267/// A list value: strings one after another, each a `u16` length, the
268/// bytes and a NUL the length does not count.
269fn strings(bytes: &[u8]) -> Option<Vec<Cow<'_, str>>> {
270    let mut reader = Reader::new(bytes);
271    let mut items = Vec::new();
272    while reader.remaining() > 0 {
273        items.push(String::from_utf8_lossy(reader.sized16("list item").ok()?));
274        if reader.take(1, "list item terminator").ok()? != [0] {
275            return None;
276        }
277    }
278    Some(items)
279}
280
281fn pairs(items: Vec<Cow<'_, str>>) -> Vec<Attr<'_>> {
282    let mut items = items.into_iter();
283    std::iter::from_fn(|| Some(Attr { name: items.next()?, value: items.next()? })).collect()
284}
285
286/// A string in JSON quotes on one line ([`asc_query::text::quoted`]).
287struct Quoted<'s>(&'s str);
288
289impl fmt::Display for Quoted<'_> {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        f.write_str(&asc_query::text::quoted(self.0))
292    }
293}
294
295fn write_attrs(f: &mut fmt::Formatter<'_>, attrs: &[Attr<'_>], mut first: bool) -> fmt::Result {
296    for attr in attrs {
297        if !first {
298            f.write_str(", ")?;
299        }
300        first = false;
301        write!(f, "{}: {}", Quoted(&attr.name), Quoted(&attr.value))?;
302    }
303    Ok(())
304}
305
306/// One line: text quoted as JSON (`"Bridge"`), a file path bare
307/// (`entry/resources/base/media/icon.png`), an array `["a", "b"]`, a
308/// plural `{"one": "%d file", "other": "%d files"}`, a theme or pattern
309/// the same with its parent first (`{parent: "$theme:16777220", ...}`),
310/// raw bytes as `<12 bytes>`.
311impl fmt::Display for ResourceValue<'_> {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        match self {
314            ResourceValue::Text(text) => write!(f, "{}", Quoted(text)),
315            ResourceValue::File(path) => f.write_str(path),
316            ResourceValue::Array(items) => {
317                f.write_str("[")?;
318                for (i, item) in items.iter().enumerate() {
319                    if i > 0 {
320                        f.write_str(", ")?;
321                    }
322                    write!(f, "{}", Quoted(item))?;
323                }
324                f.write_str("]")
325            }
326            ResourceValue::Plural(attrs) => {
327                f.write_str("{")?;
328                write_attrs(f, attrs, true)?;
329                f.write_str("}")
330            }
331            ResourceValue::Map { parent, attrs } => {
332                f.write_str("{")?;
333                if let Some(parent) = parent {
334                    write!(f, "parent: {}", Quoted(parent))?;
335                }
336                write_attrs(f, attrs, parent.is_none())?;
337                f.write_str("}")
338            }
339            ResourceValue::Raw(bytes) => write!(f, "<{} bytes>", bytes.len()),
340        }
341    }
342}