compatdb_core/runtime/version.rs
1//! The reader of a `VS_VERSIONINFO` blob.
2
3use super::process::to_lower_u16;
4use crate::VersionInfo;
5
6/// The standard fields of a `VS_VERSIONINFO` blob.
7///
8/// The blob is the bytes of an `RT_VERSION` resource, as UTF-16 units. It is
9/// walked as the tree it is: the root, its `StringFileInfo` children, their
10/// string tables and the `String` entries in them. A field takes the value
11/// of the first entry with its key, in table order; an entry whose value is
12/// empty, or a key no table has, leaves the field empty, so a rule that pins
13/// it cannot match.
14#[must_use]
15pub fn version_info(blob: &[u16]) -> VersionInfo {
16 VersionInfo {
17 company: string_value(blob, "CompanyName"),
18 product: string_value(blob, "ProductName"),
19 original_filename: string_value(blob, "OriginalFilename"),
20 }
21}
22
23/// One node of a `VS_VERSIONINFO` tree. Every node, the root, the containers
24/// and the `String` entries alike, is `wLength` (bytes, children included),
25/// `wValueLength`, `wType`, a NUL-terminated UTF-16 key, padding to a 32-bit
26/// boundary, the value, padding again, and then the children.
27struct VersionNode<'a> {
28 key: &'a [u16],
29 /// From the start of the value to the end of the node.
30 value: &'a [u16],
31 /// The children, as the units that hold them.
32 children: &'a [u16],
33}
34
35impl<'a> VersionNode<'a> {
36 /// Parse the node at the start of `units` and return it with the units
37 /// after it, where its next sibling starts. `units` has to start on a
38 /// 32-bit boundary, which every node does. A node whose length is too
39 /// short for its header or whose key is not NUL-terminated ends the walk;
40 /// one that claims more than `units` holds is cut off at the end.
41 fn parse(units: &'a [u16]) -> Option<(Self, &'a [u16])> {
42 let length = usize::from(*units.first()?).div_ceil(2);
43 let value_length = usize::from(*units.get(1)?);
44 // Any nonzero wType is text, as in Wine's VerQueryValue.
45 let text = *units.get(2)? != 0;
46 if length < 3 {
47 return None;
48 }
49 let node = units.get(..length).unwrap_or(units);
50 let key_text = node.get(3..)?;
51 let key_end = key_text.iter().position(|&u| u == 0)?;
52 let key = key_text.get(..key_end)?;
53 // The key starts 3 units in, and its NUL is one more.
54 let value_at = key_end.checked_add(4)?.checked_next_multiple_of(2)?;
55 let value = node.get(value_at..).unwrap_or_default();
56 // wValueLength counts characters for a text value and bytes for a
57 // binary one; the children start at the next 32-bit boundary after it.
58 let value_units = if text {
59 value_length
60 } else {
61 value_length.div_ceil(2)
62 };
63 let children_at = value_at
64 .checked_add(value_units)?
65 .checked_next_multiple_of(2)?;
66 let children = node.get(children_at..).unwrap_or_default();
67 let next = length.checked_next_multiple_of(2)?;
68 let rest = units.get(next..).unwrap_or_default();
69 Some((
70 Self {
71 key,
72 value,
73 children,
74 },
75 rest,
76 ))
77 }
78
79 /// The nodes in a run of sibling units, in order.
80 fn siblings(mut units: &'a [u16]) -> impl Iterator<Item = Self> {
81 std::iter::from_fn(move || {
82 let (node, rest) = Self::parse(units)?;
83 units = rest;
84 Some(node)
85 })
86 }
87
88 /// Whether the key is `want`, compared ASCII-case-insensitively as
89 /// `VerQueryValue` does.
90 fn key_is(&self, want: &str) -> bool {
91 self.key
92 .iter()
93 .map(|&u| to_lower_u16(u))
94 .eq(want.encode_utf16().map(to_lower_u16))
95 }
96}
97
98/// The value of the first `String` entry with this key in any string table of
99/// the blob, empty when there is none or its value is empty.
100///
101/// The value is read up to its NUL within the entry rather than by
102/// `wValueLength`, which resource compilers do not all fill in the same way.
103fn string_value(blob: &[u16], key: &str) -> String {
104 let Some((root, _)) = VersionNode::parse(blob) else {
105 return String::new();
106 };
107 if !root.key_is("VS_VERSION_INFO") {
108 return String::new();
109 }
110 VersionNode::siblings(root.children)
111 .filter(|info| info.key_is("StringFileInfo"))
112 .flat_map(|info| VersionNode::siblings(info.children))
113 .flat_map(|table| VersionNode::siblings(table.children))
114 .find(|entry| entry.key_is(key))
115 .and_then(|entry| entry.value.split(|&u| u == 0).next())
116 .map(String::from_utf16_lossy)
117 .unwrap_or_default()
118}
119
120#[cfg(test)]
121mod tests;