Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "no known code detected",
"country_reason_no_lun": "no hardcoded LUN for label",
"country_reason_patch_failed": "patch failed: {error}",
"country_reason_verify_failed": "read-back failed: {error}",
"country_reason_verify_mismatch": "read-back does not match the patched image",
"dash_action_firmware_lookup": "Firmware Lookup",
"dash_action_ota_lookup": "OTA Lookup",
"dash_adb_server_blocking": "An external ADB server is preventing device access.",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label}: {from} → {to}",
"live_country_patched_flashed": "{label} patched + flashed",
"live_country_processing": "Processing {path}",
"live_country_verified": "{label} read back and verified",
"live_country_warning": "Warning: {error}; firmware already flashed, continuing to reboot.",
"live_country_written": "{name}: {old_code} → {new_code} written to {path}",
"live_crypto_decrypted": "Decrypted {bytes} bytes",
Expand Down
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "aucun code connu détecté",
"country_reason_no_lun": "aucun LUN prédéfini pour ce libellé",
"country_reason_patch_failed": "échec du patch : {error}",
"country_reason_verify_failed": "échec de la relecture : {error}",
"country_reason_verify_mismatch": "la relecture ne correspond pas à l’image patchée",
"dash_action_firmware_lookup": "Rechercher un firmware",
"dash_action_ota_lookup": "Rechercher une OTA",
"dash_adb_server_blocking": "Un serveur ADB externe empêche l’accès à l’appareil.",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label} : {from} → {to}",
"live_country_patched_flashed": "{label} patché + flashé",
"live_country_processing": "Traitement de {path}",
"live_country_verified": "{label} relu et vérifié",
"live_country_warning": "Attention : {error} ; firmware déjà flashé, poursuite vers le redémarrage.",
"live_country_written": "{name} : {old_code} → {new_code} écrit dans {path}",
"live_crypto_decrypted": "{bytes} octets déchiffrés",
Expand Down
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "既知のコードを検出できません",
"country_reason_no_lun": "ラベルにハードコードされた LUN がありません",
"country_reason_patch_failed": "パッチ失敗:{error}",
"country_reason_verify_failed": "読み戻しに失敗:{error}",
"country_reason_verify_mismatch": "読み戻した内容がパッチ済みイメージと一致しません",
"dash_action_firmware_lookup": "ファームウェア検索",
"dash_action_ota_lookup": "OTA 検索",
"dash_adb_server_blocking": "外部のADBサーバーが動作しているため、デバイスに接続できません。",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label}: {from} → {to}",
"live_country_patched_flashed": "{label} のパッチ適用 + フラッシュ完了",
"live_country_processing": "{path} を処理中",
"live_country_verified": "{label} を読み戻して確認しました",
"live_country_warning": "警告:{error}。ファームウェアはすでにフラッシュされているため、再起動を続行します。",
"live_country_written": "{name}: {old_code} → {new_code} が {path} に書き込まれました",
"live_crypto_decrypted": "{bytes} バイトを復号しました",
Expand Down
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "알려진 코드 감지 안 됨",
"country_reason_no_lun": "파티션 LUN 매핑 없음",
"country_reason_patch_failed": "패치 실패: {error}",
"country_reason_verify_failed": "다시 읽기 실패: {error}",
"country_reason_verify_mismatch": "다시 읽은 내용이 패치한 이미지와 다름",
"dash_action_firmware_lookup": "펌웨어 조회",
"dash_action_ota_lookup": "OTA 조회",
"dash_adb_server_blocking": "외부 ADB 서버가 실행 중이어서 기기에 연결할 수 없습니다.",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label}: {from} → {to}",
"live_country_patched_flashed": "{label} 패치 후 플래싱 완료",
"live_country_processing": "{path} 처리 중",
"live_country_verified": "{label} 다시 읽어 확인 완료",
"live_country_warning": "경고: {error}; 펌웨어는 이미 플래싱되었으므로 재부팅을 계속합니다.",
"live_country_written": "{name} 국가 코드 변경 완료: {old_code} → {new_code} (저장: {path})",
"live_crypto_decrypted": "복호화 완료 ({bytes} 바이트)",
Expand Down
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "известный код не обнаружен",
"country_reason_no_lun": "для метки нет жёстко заданного LUN",
"country_reason_patch_failed": "патч не удался: {error}",
"country_reason_verify_failed": "не удалось прочитать обратно: {error}",
"country_reason_verify_mismatch": "прочитанные данные не совпадают с исправленным образом",
"dash_action_firmware_lookup": "Поиск прошивки",
"dash_action_ota_lookup": "Поиск OTA",
"dash_adb_server_blocking": "Внешний сервер ADB мешает подключению к устройству.",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label}: {from} → {to}",
"live_country_patched_flashed": "Раздел пропатчен и прошит: {label}",
"live_country_processing": "Обработка {path}",
"live_country_verified": "{label}: записанные данные проверены",
"live_country_warning": "Предупреждение: {error}; прошивка уже записана, продолжаем перезагрузку.",
"live_country_written": "{name}: {old_code} → {new_code} записано в {path}",
"live_crypto_decrypted": "Расшифровано байт: {bytes}",
Expand Down
3 changes: 3 additions & 0 deletions crates/ltbox-gui/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
"country_reason_no_known_code": "未检测到已知代码",
"country_reason_no_lun": "该标签没有硬编码 LUN",
"country_reason_patch_failed": "修补失败:{error}",
"country_reason_verify_failed": "回读失败:{error}",
"country_reason_verify_mismatch": "回读内容与已修补镜像不一致",
"dash_action_firmware_lookup": "查询固件",
"dash_action_ota_lookup": "查询 OTA",
"dash_adb_server_blocking": "外部 ADB 服务器正在运行,无法连接设备。",
Expand Down Expand Up @@ -548,6 +550,7 @@
"live_country_patch_transition": "{label}:{from} → {to}",
"live_country_patched_flashed": "{label} 已修补并刷写",
"live_country_processing": "正在处理 {path}",
"live_country_verified": "已回读并验证 {label}",
"live_country_warning": "警告:{error};固件已刷写,继续重启。",
"live_country_written": "{name}:{old_code} → {new_code} 已写入 {path}",
"live_crypto_decrypted": "已解密 {bytes} 字节",
Expand Down
262 changes: 262 additions & 0 deletions crates/ltbox-gui/src/workers/flash/country_verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//! Country write/readback orchestration shared by both country-change callers.

use crate::CountryPatchProgress;
use ltbox_core::{live, tr_args};
use std::path::Path;

// Keep the seam at the device boundary so tests exercise the production
// ordering, comparison, and aggregate outcome without a connected device.
pub(super) trait CountrySession {
fn flash_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
log: &mut Vec<String>,
) -> Result<(), String>;
fn dump_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
log: &mut Vec<String>,
) -> Result<(), String>;
}

impl CountrySession for ltbox_device::edl::EdlSession {
fn flash_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
log: &mut Vec<String>,
) -> Result<(), String> {
Self::flash_partition(self, label, path, slot, lun, log).map_err(|e| e.to_string())
}
fn dump_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
log: &mut Vec<String>,
) -> Result<(), String> {
Self::dump_partition(self, label, path, slot, lun, log).map_err(|e| e.to_string())
}
}

pub(super) fn flash_and_verify_country(
session: &mut impl CountrySession,
label: &str,
lun: u8,
patched_path: &Path,
verify_path: &Path,
country_progress: &mut CountryPatchProgress,
log: &mut Vec<String>,
) {
if let Err(e) = session.flash_partition(label, patched_path, 0, lun, log) {
ltbox_core::live!(
log,
"[Country] {}",
tr_args!(
"live_country_flash_failed",
label = label,
error = e.to_string()
)
);
country_progress.mark_failed(label, tr_args!("country_reason_flash_failed", error = e));
} else {
live!(
log,
"[Country] {}",
tr_args!("live_country_patched_flashed", label = label)
);
// Read the partition back: a write the programmer acknowledged
// is not yet proof the device holds the patched bytes.
let verified = session
.dump_partition(label, verify_path, 0, lun, log)
.map_err(|e| tr_args!("country_reason_verify_failed", error = e))
.and_then(|_| {
super::files_identical(patched_path, verify_path)
.map_err(|e| tr_args!("country_reason_verify_failed", error = e))
});
match verified {
Ok(true) => {
live!(
log,
"[Country] {}",
tr_args!("live_country_verified", label = label)
);
country_progress.mark_flashed(label);
}
Ok(false) => {
let reason = ltbox_core::i18n::tr("country_reason_verify_mismatch");
ltbox_core::live!(
log,
"[Country] {}",
tr_args!(
"live_country_partition_status",
label = label,
reason = reason
)
);
country_progress.mark_failed(label, reason);
}
Err(reason) => {
ltbox_core::live!(
log,
"[Country] {}",
tr_args!(
"live_country_partition_status",
label = label,
reason = reason
)
);
country_progress.mark_failed(label, reason);
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

#[derive(Clone, Copy, Debug)]
enum Scenario {
Match,
Mismatch,
DumpError,
FlashError,
MissingReadback,
}

#[derive(Debug, PartialEq)]
struct Call {
operation: &'static str,
label: String,
path: PathBuf,
slot: u8,
lun: u8,
}

struct ScriptedSession {
scenario: Scenario,
calls: Vec<Call>,
}

impl CountrySession for ScriptedSession {
fn flash_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
_: &mut Vec<String>,
) -> Result<(), String> {
self.calls.push(Call {
operation: "flash",
label: label.into(),
path: path.into(),
slot,
lun,
});
assert_eq!(std::fs::read(path).unwrap(), b"patched country");
if matches!(self.scenario, Scenario::FlashError) {
Err("write rejected".into())
} else {
Ok(())
}
}

fn dump_partition(
&mut self,
label: &str,
path: &Path,
slot: u8,
lun: u8,
_: &mut Vec<String>,
) -> Result<(), String> {
self.calls.push(Call {
operation: "dump",
label: label.into(),
path: path.into(),
slot,
lun,
});
match self.scenario {
Scenario::Match => std::fs::write(path, b"patched country").unwrap(),
Scenario::Mismatch => std::fs::write(path, b"old country ").unwrap(),
Scenario::DumpError => return Err("read rejected".into()),
Scenario::MissingReadback => {}
Scenario::FlashError => panic!("must not read back after a rejected write"),
}
Ok(())
}
}

#[test]
fn country_write_requires_successful_matching_readback() {
for scenario in [
Scenario::Match,
Scenario::Mismatch,
Scenario::DumpError,
Scenario::FlashError,
Scenario::MissingReadback,
] {
let dir = tempfile::tempdir().unwrap();
let patched = dir.path().join("proinfo.patched.img");
let readback = dir.path().join("proinfo.verify.img");
std::fs::write(&patched, b"patched country").unwrap();
let mut session = ScriptedSession {
scenario,
calls: Vec::new(),
};
let mut progress = CountryPatchProgress::new(&["proinfo"]);
let mut log = Vec::new();
flash_and_verify_country(
&mut session,
"proinfo",
4,
&patched,
&readback,
&mut progress,
&mut log,
);

let mut expected = vec![Call {
operation: "flash",
label: "proinfo".into(),
path: patched,
slot: 0,
lun: 4,
}];
if !matches!(scenario, Scenario::FlashError) {
expected.push(Call {
operation: "dump",
label: "proinfo".into(),
path: readback,
slot: 0,
lun: 4,
});
}
assert_eq!(session.calls, expected, "{scenario:?}");
assert_eq!(
progress.finish().is_ok(),
matches!(scenario, Scenario::Match),
"{scenario:?}"
);
let verified = tr_args!("live_country_verified", label = "proinfo");
assert_eq!(
log.iter()
.any(|line| line == &format!("[Country] {verified}")),
matches!(scenario, Scenario::Match),
"{scenario:?}"
);
}
}
}
Loading
Loading