Skip to main content

samloader_fus/
xml.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use md5::{Digest, Md5};
16use roxmltree::Document;
17use std::collections::HashMap;
18
19fn get_logic_check(inp: &str, nonce: &str) -> String {
20    let mut out = String::new();
21    for c in nonce.chars() {
22        let idx = (c as u32) & 0xf;
23        if let Some(ch) = inp.chars().nth(idx as usize) {
24            out.push(ch);
25        } else {
26            out.push('.');
27        }
28    }
29    out
30}
31
32pub(crate) fn parse_version_xml(xml: &str) -> Option<String> {
33    let doc = Document::parse(xml).ok()?;
34    let latest = doc
35        .descendants()
36        .find(|n| n.has_tag_name("latest"))?
37        .text()?;
38    let mut parts: Vec<&str> = latest.split('/').collect();
39    if parts.len() == 3 {
40        parts.push(parts[0]);
41    }
42    if parts.len() >= 3 && parts[2].is_empty() {
43        parts[2] = parts[0];
44    }
45    Some(parts.join("/"))
46}
47
48pub(crate) fn binary_inform_req_xml(model: &str, region: &str, fw: &str, nonce: &str) -> String {
49    let logic_check = get_logic_check(fw, nonce);
50
51    format!(
52        r#"<FUSMsg>
53<FUSHdr><ProtoVer>1.0</ProtoVer><SessionID>0</SessionID><MsgID>1</MsgID></FUSHdr>
54<FUSBody>
55    <Put>
56        <CmdID>1</CmdID>
57        <ACCESS_MODE><Data>1</Data></ACCESS_MODE>
58        <BINARY_NATURE><Data>1</Data></BINARY_NATURE>
59        <REQUEST_TYPE><Data>2</Data></REQUEST_TYPE>
60        <LOGIC_CHECK><Data>{logic_check}</Data></LOGIC_CHECK>
61        <BINARY_SW_VERSION><Data>{fw}</Data></BINARY_SW_VERSION>
62        <BINARY_LOCAL_CODE><Data>{region}</Data></BINARY_LOCAL_CODE>
63        <BINARY_MODEL_NAME><Data>{model}</Data></BINARY_MODEL_NAME>
64    </Put>
65    <Get>
66        <CmdID>2</CmdID>
67        <BINARY_SW_VERSION></BINARY_SW_VERSION>
68    </Get>
69</FUSBody>
70</FUSMsg>"#
71    )
72}
73
74pub(crate) fn binary_init_req_xml(
75    filename: &str,
76    nonce: &str,
77    fw: &str,
78    model_type: &str,
79    region: &str,
80) -> String {
81    let start = filename.len().saturating_sub(25);
82    let end = filename.len().saturating_sub(9);
83    let checkinp = &filename[start..end];
84
85    let logic_check = get_logic_check(checkinp, nonce);
86
87    format!(
88        r#"<FUSMsg>
89<FUSHdr><ProtoVer>1.0</ProtoVer><SessionID>0</SessionID><MsgID>1</MsgID></FUSHdr>
90<FUSBody>
91    <Put>
92        <BINARY_NAME><Data>{filename}</Data></BINARY_NAME>
93        <BINARY_SW_VERSION><Data>{fw}</Data></BINARY_SW_VERSION>
94        <DEVICE_LOCAL_CODE><Data>{region}</Data></DEVICE_LOCAL_CODE>
95        <DEVICE_MODEL_TYPE><Data>{model_type}</Data></DEVICE_MODEL_TYPE>
96        <LOGIC_CHECK><Data>{logic_check}</Data></LOGIC_CHECK>
97    </Put>
98</FUSBody>
99</FUSMsg>"#
100    )
101}
102
103fn parse_xml_data(xml: &str) -> Option<HashMap<String, String>> {
104    let doc = Document::parse(xml).ok()?;
105
106    let status_str = doc
107        .root_element()
108        .children()
109        .find(|n| n.has_tag_name("FUSBody"))?
110        .children()
111        .find(|n| n.has_tag_name("Results"))?
112        .children()
113        .find(|n| n.has_tag_name("Status"))?
114        .text()?;
115
116    if status_str != "200" && status_str != "S00" {
117        return None;
118    }
119
120    let mut kv = HashMap::new();
121    doc.descendants()
122        .filter(|n| n.has_tag_name("Data"))
123        .for_each(|n| {
124            if let Some(v) = n.text() {
125                let parent = n.parent().unwrap();
126                kv.insert(parent.tag_name().name().to_string(), v.to_string());
127            }
128        });
129    Some(kv)
130}
131
132#[derive(Default, Clone)]
133pub struct BinaryInform {
134    pub version: String,
135    pub filename: String,
136    pub path: String,
137    pub size: u64,
138    pub key: Vec<u8>,
139    pub model_type: String,
140    pub region: String,
141}
142
143impl BinaryInform {
144    pub(crate) fn parse(xml: &str) -> Option<BinaryInform> {
145        let mut kv = parse_xml_data(xml)?;
146        let size: u64 = kv.get("BINARY_BYTE_SIZE")?.parse().ok()?;
147        let fw_ver = kv
148            .get("BINARY_SW_VERSION")
149            .cloned()
150            .or_else(|| kv.get("LATEST_FW_VERSION").cloned())?;
151        let logic_val = kv
152            .remove("LOGIC_VALUE_FACTORY")
153            .or_else(|| kv.remove("LOGIC_VALUE_HOME"))?;
154        let key = get_logic_check(&fw_ver, &logic_val);
155
156        Some(Self {
157            version: fw_ver,
158            filename: kv.remove("BINARY_NAME")?,
159            path: kv.remove("MODEL_PATH")?,
160            size,
161            key: Md5::digest(key.as_bytes()).to_vec(),
162            model_type: kv.remove("DEVICE_MODEL_TYPE")?,
163            region: kv.remove("BINARY_LOCAL_CODE")?,
164        })
165    }
166}