fix(mesh): never show mojibake device names — strict decode with readable fallback
Name fields sit at firmware-version-dependent offsets; when an offset lands in binary data, from_utf8_lossy handed the UI replacement-character soup in the mesh modals and settings panel. decode_mesh_name(): strict UTF-8 to the first NUL, no control chars, else a readable fallback (short pubkey / node-<id>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
be6f06a573
commit
e59ea1da69
@ -353,6 +353,28 @@ pub fn build_get_stats() -> Vec<u8> {
|
||||
|
||||
// ─── Response parsers ───────────────────────────────────────────────────
|
||||
|
||||
/// Decode a device/contact name from raw frame bytes, defensively.
|
||||
///
|
||||
/// Name fields sit at firmware-version-dependent offsets; when an offset
|
||||
/// lands inside binary data (path/pubkey bytes), `from_utf8_lossy` used to
|
||||
/// hand the UI replacement-character soup ("<22>\u{618}…"). Strict rules: valid
|
||||
/// UTF-8 up to the first NUL, no control characters, at least one
|
||||
/// non-whitespace char — anything else gets the caller's readable fallback.
|
||||
fn decode_mesh_name(bytes: &[u8], fallback: &str) -> String {
|
||||
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||
match std::str::from_utf8(&bytes[..end]) {
|
||||
Ok(s) => {
|
||||
let s = s.trim();
|
||||
if !s.is_empty() && s.chars().all(|c| !c.is_control()) {
|
||||
s.to_string()
|
||||
} else {
|
||||
fallback.to_string()
|
||||
}
|
||||
}
|
||||
Err(_) => fallback.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse RESP_DEVICE_INFO (0x0D) response.
|
||||
/// Returns firmware version string and device capabilities.
|
||||
pub fn parse_device_info(data: &[u8]) -> Result<(String, u16)> {
|
||||
@ -384,15 +406,12 @@ pub fn parse_self_info(data: &[u8]) -> Result<(u32, String)> {
|
||||
|
||||
let node_id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
|
||||
|
||||
// Name follows after fixed fields — find it by scanning for printable ASCII
|
||||
// Name follows after fixed fields. A firmware whose fixed-field layout
|
||||
// differs would put binary here — decode defensively so the settings
|
||||
// panel never shows byte soup.
|
||||
let name_start = 4;
|
||||
let name = if data.len() > name_start {
|
||||
let name_end = data[name_start..]
|
||||
.iter()
|
||||
.position(|&b| b == 0)
|
||||
.map(|p| name_start + p)
|
||||
.unwrap_or(data.len());
|
||||
String::from_utf8_lossy(&data[name_start..name_end]).to_string()
|
||||
decode_mesh_name(&data[name_start..], &format!("node-{node_id:08x}"))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@ -453,12 +472,11 @@ pub fn parse_contact(data: &[u8]) -> Result<ParsedContact> {
|
||||
// name at data[99..131] (32 bytes)
|
||||
let name_start = 99.min(data.len());
|
||||
let name_end = (name_start + 32).min(data.len());
|
||||
let short_id = format!("{}...", &public_key_hex[..8]);
|
||||
let advert_name = if data.len() > name_start {
|
||||
String::from_utf8_lossy(&data[name_start..name_end])
|
||||
.trim_end_matches('\0')
|
||||
.to_string()
|
||||
decode_mesh_name(&data[name_start..name_end], &short_id)
|
||||
} else {
|
||||
format!("{}...", &public_key_hex[..8])
|
||||
short_id
|
||||
};
|
||||
|
||||
// last_advert at data[131..135]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user