Universal-Debloater-Alliance/universal-android-debloater-next-generation · error

SDK version numeral must be valid

Error message

SDK version numeral must be valid

What it means

get_android_sdk shells out to `adb shell getprop ro.build.version.sdk` and parses the returned string into a u8. If adb returns anything that is not a valid u8 (empty output, an error string, or a number larger than u8), the `.unwrap_or`-style map_or only covers the command failing — a successful command with unparseable output hits `.expect("SDK version numeral must be valid")` and panics. The library assumes getprop always yields a plain numeric SDK level when the command succeeds.

Solutions

  1. Replace .expect with graceful fallback: `sdk.parse().unwrap_or(0)` so an invalid value is treated the same as a failed adb command.
  2. Change the return type to Result<u8, String> and propagate the parse error to the caller instead of panicking.
  3. Change the return type to u32 (or parse as u32 then clamp) to remove the u8 overflow failure mode.
  4. Log the raw getprop output when parsing fails so the malformed device state is diagnosable.

Example fix

// before
.map_or(0, |sdk| {
    sdk.parse().expect("SDK version numeral must be valid")
})
// after
.map_or(0, |sdk| sdk.trim().parse().unwrap_or(0))
Defensive patterns

Strategy: fallback

Validate before calling

let out = AdbCommand::new().shell(serial).getprop("ro.build.version.sdk").unwrap_or_default();
let sdk: Option<u8> = out.trim().parse().ok();
if sdk.is_none() { eprintln!("device {serial} returned non-numeric SDK: {out:?}"); }

Type guard

fn is_valid_sdk(s: &str) -> bool { s.trim().parse::<u8>().is_ok() }

Try / catch

match sdk_str.trim().parse::<u8>() {
    Ok(v) => v,
    Err(e) => { warn!("bad SDK value {sdk_str:?}: {e}"); 0 }
}

Prevention

When it happens

Trigger: Calling get_android_sdk(device_serial) when the device's `ro.build.version.sdk` property is non-numeric or exceeds 255: e.g. a device booting into a broken state, a future Android release with SDK > 255, or a device whose getprop output includes extra whitespace/suffix text.

Common situations: Connecting to emulators/custom ROMs with malformed build props; testing against a hypothetical Android version whose API level exceeds 255 (u8 overflow); devices in a half-booted or recovery-adjacent state where getprop succeeds but returns garbage.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of Universal-Debloater-Alliance/universal-android-debloater-next-generation@64465c850c (2026-09-12). Data as JSON: /api/errors/18ea0407d5196113. Report an issue: GitHub.

Appendix: source

Thrown at crates/uad-core/src/sync.rs:272

    AdbCommand::new()
        .shell(serial)
        .getprop("ro.product.brand")
        // `trim` is just-in-case
        .map(|s| s.trim().to_string())
        .unwrap_or_default()
}

/// Get Android SDK version by querying the
// `ro.build.version.sdk` property or defaulting to 0.
///
/// If `device_serial` is empty, it lets ADB choose the default device.
#[must_use]
pub fn get_android_sdk(device_serial: &str) -> u8 {
    AdbCommand::new()
        .shell(device_serial)
        .getprop("ro.build.version.sdk")
        .map_or(0, |sdk| {
            sdk.parse().expect("SDK version numeral must be valid")
        })
}

/// Capture the current state of a package across all non-protected users.
/// This is used to detect cross-user behavior by comparing before and after states.
///
/// Only includes users where the package exists (Some state). Users where the package
/// doesn't exist (None) are not tracked.
#[must_use]
pub fn capture_cross_user_states(
    package_name: &str,
    device_serial: &str,
    target_user_id: u16,
    phone: &Phone,
) -> Vec<(u16, PackageState)> {
    phone
        .user_list
        .iter()

View on GitHub (pinned to 64465c850c)