fix(P0): parola sızıntısı, parted birim hatası, hatalı LUKS cmdline, yutulan hatalar, hata temizliği

Veri kaybı / güvenlik / açılmayan sistem düzeyindeki 7 sorun giderildi.

## 1. Parola düz metin olarak argv'ye geçiyordu (jobs/users.rs)
`sh -c "echo 'user:pass' | chpasswd -c SHA512"` deseni kaldırıldı:
  - parola /proc/<pid>/cmdline üzerinden okunabiliyordu
  - ' veya $(...) içeren parola komutu bozuyordu
Doğru desen (`set_password_via_chpasswd`, stdin pipe) geri getirildi —
jobs/partition.rs::run_luks_format ile aynı. Yazılan veri yazımdan
sonra sıfırlanıyor. Dosyadaki yorum satırındaki güvenli sürüm de
silinerek tek uygulama bırakıldı.

## 2. parted "MB" birim tuzağı geri gelmişti (jobs/partition.rs:237)
EFI ve swap `{}MiB` geçerken kök bölüm `{}MB` idi. parted "MB"yi ondalık
(10^6) okur, bizim hesaplar ikilidir (2^20): 4609MB = 9001992.1875
sektör → hizasız başlangıç. AGENTS.md bunu KRİTİK not almıştı;
`7209d58` ile düzeltilmiş, sonra regresyona uğramıştı. `{}MiB` yapıldı
ve nedeni yorumla sabitlendi.

## 3. LUKS çekirdek parametresi geçersizdi (jobs/bootloader.rs)
`cryptdevice=UUID=<x>:cryptroot` geçerli bir söz dizimi değil, üstelik
kök bölümü `is_uefi` ile tahmin ediliyordu (UEFI'de 3, BIOS'ta 2) —
manuel modda bu numaralar assign_partition_numbers'tan geldiği için
ilgisiz bir bölümün UUID'si okunabiliyordu. `.unwrap_or_default()`
`UUID=:` üretebiliyordu.

Artık `cryptsetup status` ile mapper'ın arkasındaki GERÇEK aygıt
çalışma zamanında çözülüyor, blkid ile LUKS başlık UUID'si alınıyor,
ve dracut (`rd.luks.uuid=luks-…`) + mkinitcpio (`cryptdevice=…`)
biçimleri birlikte yazılıyor. UUID boş çıkarsa hata verilir; tahmin
yok.

Ayrıca `CRYPTROOT_NAME` sabiti tanımlandı: "cryptroot" 5 yerde
hardcoded'dı, artık tek kaynaktan geliyor.

## 4. Kritik hatalar yutuluyordu (jobs/finish.rs)
  - RunComarJob tamamen yorum satırıydı → `pisi configure-pending`
    hiç çalışmıyordu, paketlerin post-install betikleri koşmuyordu.
    Geri getirildi; "yapılandırılacak paket yok" durumu zararsız sayılıyor.
  - ldconfig ve update-environment hataları loglanıp yutuluyordu.
    update-environment PisiLinux init finalizasyonudur: yutulursa
    kurulum "başarılı" görünür ama sistem açılmaz. Artık fatal.
  - CleanupLiveJob'da 10 adet `let _ =`; depo tanımları sessizce
    başarısız olabiliyordu (kullanıcı paket kuramaz). Artık fatal.

## 5. Hata durumunda hiç temizlik yapılmıyordu (jobs/mod.rs)
run_all ilk hatada return ediyordu: hedef bölümler bağlı, LUKS
mapper'ları açık, swap aktif kalıyordu. Yeniden başlatma reddediliyor,
elle düzeltmesi dakikalar sürüyordu.

  - JobQueue::cleanup listesi eklendi; with_cleanup(...) ile verilen
    işler başarıda da hatada da çalışır.
  - Yeni EmergencyCleanupJob (jobs/cleanup.rs): tüm mount'lar → LVM
    lvchange -an → cryptsetup close (yalnızca hedef diskin
    mapper'ları) → swapoff.
  - Temizlik de hata verirse bu hatalar asıl hataya eklenip
    kullanıcıya "elle ne yapılacak" listesi olarak gösterilir.

## 6. UnmountBindJob /home, /var, /boot'u çözmüyordu (jobs/execution.rs)
Elle yazılmış sabit liste (proc, sys, dev, boot/efi) yalnızca otomatik
modda doğruydu; manuel düzende ek bölümler kurulum bitince bağlı
kalıyordu. Artık /proc/self/mountinfo okunuyor ve mount/ altındaki her
şey en derinden yüzeye çözülüyor — düzen bilinmeye gerek yok.
mountinfo'daki \0NNN oktal kaçışlarını çözen unescape_mount_field ve
mounted_paths_under eklendi.

## 7. Parolasız LUKS kombinasyonu (jobs/partition.rs)
encrypt_root=true + boş parola → kök düz ext4 formatlanıyor, sonra
MountPartitionsJob /dev/mapper/cryptroot'u mount etmeye çalışıp
kurulumu öldürüyordu. Job başında reddediliyor.

## 8. rescue.rs'te iki panik noktası
  - &entry.summary[..50]: len() bayt sayar, dilim bayt sınırı ister.
    pisi history çıktısı paket adlarından gelir ve Türkçe karakter
    içerir (ş/ğ 2 bayt) → sınırda PANİK. chars().take(50) ile değişti.
  - parse_pisi_history: '[' ve ']' bağımsız aranıyordu; ']' önce
    gelen girdide remainder[start+1..end] PANİK veriyordu. Kapanış
    parantez açılıştan sonra aranıyor.

Her ikisi için 5 test eklendi (rescue.rs'te test modu yoktu).

## Doğrulama
cargo check --all-targets : 0 hata, 0 uyarı
cargo test              : 28/28 geçti (23 + 5 yeni)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erkan IŞIK
2026-09-25 22:28:11 +03:00
parent 8c6dc08361
commit d161556c73
8 changed files with 722 additions and 160 deletions
+75 -5
View File
@@ -1,6 +1,52 @@
use std::sync::atomic::Ordering;
use super::{blkid_uuid, DEMO_MODE, get_fs_type, is_mount_point, part_path, run_cmd, run_chroot, Job, UiSender};
/// Kök bölümün LUKS mapper adı. `PartitionJob`, `MountPartitionsJob`,
/// `GenerateFstabJob` ve `InstallGrubJob` aynı adı kullanır; tek
/// kaynaktan okunsun diye burada tanımlı.
pub const CRYPTROOT_NAME: &str = "cryptroot";
/// Açık bir LUKS mapper'ının arkasındaki GERÇEK aygıtı döndürür
/// (`/dev/sda3` gibi).
///
/// Neden gerekli: `blkid /dev/mapper/cryptroot` dosya sisteminin UUID'sini
/// verir, LUKS başlık UUID'sini değil. Kernel'e `rd.luks.uuid=` ile
/// verilmesi gereken değer LUKS başlığınkidir; bu yüzden önce mapper'ın
/// altındaki bölüme inmek gerekir. `cryptsetup status` tam olarak bu
/// bilgiyi verir ve çalışma zamanında doğrudur — bölüm numarası
/// tahmin etmeye gerek kalmaz (manuel modda numaralar `is_uefi`'ye göre
/// değil `assign_partition_numbers`'a göre belirlenir).
async fn luks_backing_device(name: &str) -> Result<String, String> {
if DEMO_MODE.load(Ordering::Relaxed) {
return Ok("/dev/demo-luks".to_string());
}
let out = tokio::process::Command::new("cryptsetup")
.args(["status", name])
.output()
.await
.map_err(|e| format!("cryptsetup status {} çalıştırılamadı: {}", name, e))?;
// `cryptsetup status` stdout'a:
// /dev/mapper/cryptroot is active and is in use.
// type: LUKS2
// device: /dev/sda3
let stdout = String::from_utf8_lossy(&out.stdout);
let device = stdout
.lines()
.find_map(|l| l.trim().strip_prefix("device:"))
.map(|d| d.trim().to_string())
.filter(|d| !d.is_empty());
device.ok_or_else(|| {
format!(
"{} mapper'ının altındaki aygıt çözümlenemedi. \
LUKS başlığı açılmamış olabilir; fstab/crypttab kontrol edin.",
name
)
})
}
/// initramfs yeniler (GRUB öncesi zorunlu).
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
@@ -180,11 +226,35 @@ impl Job for InstallGrubJob {
// kernel cmdline
let mut cmdline = self.kernel_options.clone();
if self.encrypt_root {
// Kök bölüm UUID'sini blkid'den al (LUKS başlık UUID'si)
let (root_num, _swap_num): (u32, u32) = if self.is_uefi { (3, 2) } else { (2, 1) };
let root_part = part_path(&self.disk, root_num);
let root_uuid = blkid_uuid(&root_part).await.unwrap_or_default();
let crypt_arg = format!("cryptdevice=UUID={}:cryptroot root=/dev/mapper/cryptroot", root_uuid);
// LUKS başlık UUID'si GEREKLİ. Önceden bölüm numarası tahmin
// ediliyordu (UEFI'de 3, BIOS'ta 2) — bu manuel modda YANLIŞTIR
// (numaralar assign_partition_numbers'tan gelir) ve üstelik
// "cryptdevice=UUID=x:cryptroot" geçerli bir söz dizimi değildir.
//
// Bunun yerine çalışma zamanında gerçeği soruyoruz:
// `cryptsetup status` LUKS başlığının bulunduğu aygıtı verir,
// blkid ise o aygıtın LUKS UUID'sini (crypto_LUKS).
// /dev/mapper/cryptroot'un UUID'si değil — mapper'ın arkasındaki
// bölümünki kullanılır.
let luks_part = luks_backing_device(CRYPTROOT_NAME).await?;
let luks_uuid = blkid_uuid(&luks_part).await
.map_err(|e| format!("{} LUKS UUID okunamadı: {}", luks_part, e))?;
if luks_uuid.trim().is_empty() {
return Err(format!("{} için LUKS UUID boş geldi", luks_part));
}
ui.log(format!("LUKS: {} → {}", luks_part, luks_uuid));
// dracut sözdizimi: rd.luks.uuid=luks-<uuid>
// mkinitcpio sözdizimi: cryptdevice=UUID=<uuid>:<mapper>
// PisiLinux hangisini kullandığı belli olmadığı için ikisi de
// yazılır; iki araç da diğerinin anahtarını yok sayar.
let crypt_arg = format!(
"rd.luks.uuid=luks-{uuid} rd.luks.name={uuid}={name} \
root=/dev/mapper/{name} cryptdevice=UUID={uuid}:{name}",
uuid = luks_uuid,
name = CRYPTROOT_NAME,
);
if cmdline.is_empty() {
cmdline = crypt_arg;
} else {
+201
View File
@@ -0,0 +1,201 @@
//! Kurulum bir hata ile durduğunda veya bittiğinde çalışan temizlik işleri.
//!
//! Bu modülün varlık nedeni: `JobQueue::run_all` eskiden ilk hatada
//! `return` ediyordu. O an hedef bölümler bağlıydı, LUKS mapper'ları
//! açıktı ve swap aktifti — kullanıcı yeniden başlatmaya çalışırsa
//! sistem onu reddediyor, elle de düzeltmesi dakikalar sürüyordu.
//!
//! Temizlik işleri `JobQueue::with_cleanup(...)` ile verilir ve başarı
//! durumunda da, hata durumunda da çalışır.
use super::execution::umount_clean;
use super::{mounted_paths_under, Job, UiSender, DEMO_MODE};
use std::sync::atomic::Ordering;
/// Kurulum bitince veya hata ile durunca hedef sistemin bağlamalarını
/// tamamen çözer: tüm mount'lar, LUKS mapper'ları, LVM birimleri ve swap.
///
/// Sıralama önemlidir — en derinden yüzeye:
/// 1. `mount` altındaki tüm dosya sistemleri
/// 2. LVM mantıksal birimleri (`lvchange -an`)
/// 3. LUKS mapper'ları (`cryptsetup close`)
/// 4. Hedef diskin swap bölümü (`swapoff`)
///
/// Swap en sonda: swap'a yazmış bir süreç varsa erken swapoff denemesi
/// "device is busy" verir.
pub struct EmergencyCleanupJob {
pub mount: String,
/// Hedef diskin bölüm yolları (swapoff için). Boşsa otomatik bulunur.
pub partitions: Vec<String>,
}
#[async_trait::async_trait]
impl Job for EmergencyCleanupJob {
fn name(&self) -> &str { "job_cleanup_emergency" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
if DEMO_MODE.load(Ordering::Relaxed) {
return Ok(());
}
let mut errors: Vec<String> = Vec::new();
// ── 1. Dosya sistemleri ────────────────────────────────
let targets = mounted_paths_under(&self.mount);
if targets.is_empty() {
ui.log("Bağlı dosya sistemi yok");
}
for target in &targets {
if !umount_clean(target, ui).await {
errors.push(format!("{} bağlı kaldı", target));
}
}
// ── 2. LVM mantıksal birimleri ────────────────────────
// Etkin VG'lerin tüm LV'leri kapatılır. `vgs --noheadings` boş
// dönerse sistemde LVM yok demektir, hata değil.
if let Err(e) = deactivate_lvm(ui).await {
errors.push(e);
}
// ── 3. LUKS mapper'ları ───────────────────────────────
// `lsblk` TYPE=crypt listesi. Hedef diskin LUKS bölümü hangisi
// ise onu kapatır; canlı ortamın kendi LUKS'u varsa dokunmaz.
match close_crypt_mappers(&self.partitions).await {
Ok(closed) => {
for name in closed {
ui.log(format!("LUKS kapatıldı: {}", name));
}
}
Err(e) => errors.push(e),
}
// ── 4. Swap ───────────────────────────────────────────
for part in &self.partitions {
if let Err(e) = swapoff_part(part, ui).await {
errors.push(e);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("\n"))
}
}
}
/// Etkin tüm LVM VG'lerini deaktive eder.
async fn deactivate_lvm(ui: &UiSender) -> Result<(), String> {
let out = tokio::process::Command::new("vgs")
.args(["--noheadings", "-o", "vg_name"])
.output()
.await
.map_err(|e| format!("vgs çalıştırılamadı: {}", e))?;
let vgs: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if vgs.is_empty() {
return Ok(());
}
let mut failed = Vec::new();
for vg in vgs {
ui.log(format!("LVM deaktive ediliyor: {}", vg));
match super::run_cmd("vgchange", &["-an", &vg]).await {
Ok(()) => {}
Err(e) => failed.push(format!("{} ({})", vg, e)),
}
}
if failed.is_empty() {
Ok(())
} else {
Err(format!("LVM deaktive edilemedi: {}", failed.join(", ")))
}
}
/// Açık LUKS mapper'larını kapatır.
///
/// `partitions` verilmişse yalnızca o bölümlerin mapper'ları kapatılır
/// (hedef diske ait olanlar). `cryptsetup luksUUID <dev>` ile eşleştirme
/// yapılır. Liste boşsa hiçbir şeye dokunulmaz — canlı ortamın kendi
/// şifreli bölümlerini kapatmak kullanıcının verisini riske atar.
async fn close_crypt_mappers(partitions: &[String]) -> Result<Vec<String>, String> {
if partitions.is_empty() {
return Ok(Vec::new());
}
let mappers = super::open_crypt_mappers();
if mappers.is_empty() {
return Ok(Vec::new());
}
let mut closed = Vec::new();
for name in mappers {
// Bu mapper hangi bölümün arkasında?
let backing = backing_device(&name).await;
let belongs = backing
.as_deref()
.map(|d| partitions.iter().any(|p| p == d))
.unwrap_or(false);
if !belongs {
continue;
}
match super::run_cmd("cryptsetup", &["close", &name]).await {
Ok(()) => closed.push(name),
Err(e) => {
return Err(format!(
"cryptsetup close {} başarısız: {}. \
Hedef bölüm hâlâ kullanımda olabilir.",
name, e
))
}
}
}
Ok(closed)
}
/// Bir LUKS mapper'ının arkasındaki gerçek aygıtı döndürür.
async fn backing_device(mapper: &str) -> Option<String> {
let out = tokio::process::Command::new("cryptsetup")
.args(["status", mapper])
.output()
.await
.ok()?;
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|l| l.trim().strip_prefix("device:"))
.map(|d| d.trim().to_string())
.filter(|d| !d.is_empty())
}
/// Verilen bölüm swap ise `swapoff` çalıştırır. Swap değilse sessizce geçer.
async fn swapoff_part(part: &str, ui: &UiSender) -> Result<(), String> {
let out = tokio::process::Command::new("swapon")
.args(["--show", "--noheadings", "-o", "NAME"])
.output()
.await
.map_err(|e| format!("swapon sorgusu başarısız: {}", e))?;
let active: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if !active.iter().any(|s| s == part) {
return Ok(());
}
ui.log(format!("swapoff {}", part));
super::run_cmd("swapoff", &[part]).await
.map_err(|e| format!("swapoff {} başarısız: {} (bu bölümü kullanan süreçleri kapatın)", part, e))
}
+18 -22
View File
@@ -326,33 +326,29 @@ pub struct UnmountBindJob {
impl Job for UnmountBindJob {
fn name(&self) -> &str { "job_umount_bind" }
/// `mount` altındaki TÜM bağlı dosya sistemlerini en derinden yüzeye
/// doğru çözer.
///
/// Önceden elle yazılmış sabit bir liste vardı (`proc`, `sys`, `dev`,
/// `boot/efi`, ...). Bu liste yalnızca otomatik modda doğruydu:
/// manuel bölümlemede `/home`, `/var`, `/boot` gibi ek bölümler
/// kurulum bitince bağlı kalıyordu. Artık düzen bilinmiyor —
/// `/proc/self/mountinfo` okunup `mount/` altındaki her şey bulunuyor.
async fn run(&self, ui: &UiSender) -> Result<(), String> {
let mt = &self.mount;
// Ters sırada umount — en derin path önce
let subdirs = [
"sys/firmware/efi/efivars",
"dev/pts",
"dev",
"run",
"sys",
"proc",
"boot/efi",
];
let mut errors: Vec<String> = Vec::new();
// Bind-mount'lar + ESP (/boot/efi)
for subdir in &subdirs {
let target = format!("{}/{}", mt, subdir);
if !umount_clean(&target, ui).await {
let msg = format!("{} temiz unmount edilemedi", target);
errors.push(msg);
}
let mut targets = super::mounted_paths_under(mt);
if targets.is_empty() {
ui.log("Çözülecek bağlama noktası yok (zaten temiz)");
targets.push(mt.clone());
}
// Kök bölüm: chroot mount'unun kendisi
if !umount_clean(mt, ui).await {
let msg = format!("{} (kök) temiz unmount edilemedi", mt);
errors.push(msg);
for target in &targets {
if !umount_clean(target, ui).await {
errors.push(format!("{} temiz unmount edilemedi", target));
}
}
if !errors.is_empty() {
@@ -368,7 +364,7 @@ impl Job for UnmountBindJob {
/// Tek bir mount noktasını önce temiz `umount` ile, başarısızsa `umount -lf`
/// (lazy) ile çözer. "not mounted" / "no such file" gibi beklenen durumlar
/// kritik sayılmaz; yalnızca komut çalıştırılamaması gerçek hatadır.
async fn umount_clean(target: &str, ui: &UiSender) -> bool {
pub(crate) async fn umount_clean(target: &str, ui: &UiSender) -> bool {
ui.log(format!("umount {}", target));
match tokio::process::Command::new("umount")
.arg(target).status().await
+69 -46
View File
@@ -1,8 +1,9 @@
use super::{run_chroot, Job, UiSender};
/// COMAR servislerini ve tetikleyicilerini yapılandırır.
/// `pisi configure-pending` (kısaltması `pisi cp`): kurulan paketlerin
/// post-install betiklerini çalıştırır. Bu adım olmadan kurulu sistemde
/// servisler/tetikleyiciler kurulmamış olur ve sistem düzgün açılmaz.
pub struct RunComarJob {
#[allow(dead_code)]
pub mount: String,
}
@@ -11,27 +12,32 @@ impl Job for RunComarJob {
fn name(&self) -> &str { "job_comar" }
async fn run(&self, ui: &UiSender) -> Result<(), String> {
// pisi configure-pending: paket post-install scriptlerini çalıştırır.
// Yapılandırılacak paket yoksa çıkış kodu sıfır dışı olabilir — fatal değil.
// pisi cp işlemi kurulum sonrası atlanıyor (kullanıcı isteği).
let _ = ui;
// ui.log("pisi paketleri yapılandırılıyor (COMAR)…");
// match run_chroot(&self.mount, &["pisi", "cp"]).await {
// Ok(_) => {
// ui.log("✓ pisi paketleri yapılandırıldı");
// }
// Err(e) => {
// // "no packages to configure" gibi durumlar hata değil
// if e.contains("no packages") || e.contains("Nothing to do") {
// ui.log(" pisi: yapılandırılacak paket yok, atlanıyor");
// } else {
// // Gerçek hata — uyar ama kurulumu durdurma
// ui.log(format!("pisi paketleri yapılandırma uyarısı: {}", e));
// ui.log("Bu uyarı kritik değilse kurulum devam edecek.");
// }
// }
// }
Ok(())
ui.log("pisi paketleri yapılandırılıyor (configure-pending)…");
match run_chroot(&self.mount, &["pisi", "cp"]).await {
Ok(_) => {
ui.log("✓ pisi paketleri yapılandırıldı");
Ok(())
}
Err(e) => {
// "Yapılandırılacak paket yok" bir hata değil; yeni kurulumda
// (ör. yalnızca temel sistem) normal olarak oluşabilir.
let benign = ["no packages to configure", "nothing to do",
"yapılandırılacak paket", "nothing to configure"];
if benign.iter().any(|m| e.to_lowercase().contains(m)) {
ui.log(" pisi: yapılandırılacak paket yok, atlanıyor");
Ok(())
} else {
// Post-install betikleri çalışmadı: bu, kurulumun
// sessizce bozuk bırakılması anlamına gelir. Kurulumu
// durdurup hatayı kullanıcıya göster.
Err(format!(
"pisi configure-pending başarısız: {}\n\
Paketlerin kurulum betikleri çalışmadı; sistem \
düzgün başlamayabilir.", e
))
}
}
}
}
}
@@ -46,15 +52,19 @@ impl Job for RunLdconfigJob {
async fn run(&self, ui: &UiSender) -> Result<(), String> {
ui.log("ldconfig çalıştırılıyor…");
match run_chroot(&self.mount, &["ldconfig"]).await {
Ok(_) => ui.log("✓ ldconfig tamamlandı"),
Err(e) => ui.log(format!("⚠ ldconfig uyarısı: {}", e)),
}
run_chroot(&self.mount, &["ldconfig"]).await
.map_err(|e| format!("ldconfig başarısız: {}\n\
Paylaşımlı kütüphane önbelleği oluşturulamadı.", e))?;
ui.log("✓ ldconfig tamamlandı");
Ok(())
}
}
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
///
/// Bu adım init sistemi için kritiktir (udev kuralları, servisler,
/// /etc/localtime senkronizasyonu). Sessizce yutulursa kurulum "başarılı"
/// görünür ama sistem açılmaz — bu yüzden hata fatal.
pub struct UpdateEnvironmentJob {
pub mount: String,
}
@@ -65,10 +75,11 @@ impl Job for UpdateEnvironmentJob {
async fn run(&self, ui: &UiSender) -> Result<(), String> {
ui.log("update-environment çalıştırılıyor…");
match run_chroot(&self.mount, &["update-environment"]).await {
Ok(_) => ui.log("✓ update-environment tamamlandı"),
Err(e) => ui.log(format!("⚠ update-environment uyarısı: {}", e)),
}
run_chroot(&self.mount, &["update-environment"]).await
.map_err(|e| format!("update-environment başarısız: {}\n\
Init betikleri yapılandırılmadı; kurulu sistem \
düzgün başlamayabilir.", e))?;
ui.log("✓ update-environment tamamlandı");
Ok(())
}
}
@@ -86,29 +97,41 @@ impl Job for CleanupLiveJob {
let mt = &self.mount;
ui.log("Live ortam izleri temizleniyor…");
// run/livemedia
// Canlı ortamdan kalan dizinler: yoksa hata değil, `let _ =` uygun.
let _ = tokio::fs::remove_dir_all(format!("{}/run/livemedia", mt)).await;
// var/cache/pisi/packages
let _ = tokio::fs::remove_dir_all(format!("{}/var/cache/pisi/packages", mt)).await;
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
// YALI desktop dosyaları
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali.desktop", mt)).await;
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali-bin.desktop", mt)).await;
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
//pisi kullanıcısını sil (ev diziniyle birlikte), YALI paketlerini kaldır
let _ = run_chroot(&self.mount, &["userdel", "-r", "pisi"]).await;
let _ = run_chroot(&self.mount, &["pisi", "rm", "yali-rs"]).await;
// live paket deposunu kaldır
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
// depolarları yeniden ekle (stable2 + contrib)
ui.log("Depolar Ekleniyor…");
let _ = run_chroot(&self.mount, &["pisi", "ar", "stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"]).await;
let _ = run_chroot(&self.mount, &["pisi", "ar", "contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"]).await;
// depoları aktif et
ui.log("Depolar aktif ediliyor...");
let _ = run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await;
// paket listelerini güncelle
ui.log("Depolar Güncelleniyor...");
let _ = run_chroot(&self.mount, &["pisi", "ur"]).await;
// ── Aşağıdakiler FATAL ───────────────────────────────────
// Depo tanımları yapılmazsa kurulu sistemde `pisi install`
// çalışmaz, yani kullanıcı paket kuramaz. Sessizce yutulursa
// kurulum "başarılı" görünür ama sistem kullanılamaz.
ui.log("Depolar ekleniyor…");
for (name, url) in [
("stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"),
("contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"),
] {
run_chroot(&self.mount, &["pisi", "ar", name, url]).await
.map_err(|e| format!("'{}' deposu eklenemedi: {}\n\
Kurulu sistemde paket kurulamaz.", name, e))?;
}
// Depoları etkinleştir
ui.log("Depolar etkinleştiriliyor…");
run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await
.map_err(|e| format!("Depolar etkinleştirilemedi: {}", e))?;
// Paket listelerini güncelle (ağ gerektirir, birkaç dakika sürebilir)
ui.log("Depolar güncelleniyor (ağ gerekir, biraz sürebilir)…");
run_chroot(&self.mount, &["pisi", "ur"]).await
.map_err(|e| format!("Paket listeleri güncellenemedi: {}\n\
Depolar tanımlı ama indeksleri eksik olabilir.", e))?;
ui.log("✓ Live temizlik tamamlandı");
Ok(())
+182 -13
View File
@@ -29,6 +29,7 @@
mod autologin;
mod bootloader;
mod cleanup;
mod execution;
mod finish;
mod keyboard;
@@ -40,6 +41,7 @@ mod users;
pub use autologin::ConfigureAutologinJob;
pub use bootloader::{GenerateInitramfsJob, InstallGrubJob};
pub use cleanup::EmergencyCleanupJob;
pub use execution::{CopyFilesJob, MountBindJob, UnmountBindJob};
pub use finish::{CleanupLiveJob, RunComarJob, RunLdconfigJob, UpdateEnvironmentJob};
pub use keyboard::ConfigureKeyboardJob;
@@ -165,15 +167,31 @@ pub trait Job: Send + Sync {
pub struct JobQueue {
jobs: Vec<Box<dyn Job>>,
/// Başarı da başarısızlık da HER ZAMAN çalıştırılır. Bir iş hata
/// verdiğinde bu listeye geçilmeden `return` edilirse hedef bölümler
/// bağlı kalır, LUKS mapper'ları açık, swap aktif ve kullanıcı
/// kullanılamaz bir canlı sistemde kalır.
cleanup: Vec<Box<dyn Job>>,
}
impl JobQueue {
pub fn new(jobs: Vec<Box<dyn Job>>) -> Self {
Self { jobs }
Self { jobs, cleanup: Vec::new() }
}
/// `cleanup` işleri kuyruk BİTSE BİLE hata durumunda da çalıştırılır.
pub fn with_cleanup(mut self, cleanup: Vec<Box<dyn Job>>) -> Self {
self.cleanup = cleanup;
self
}
/// Tüm işleri sırayla çalıştırır.
/// İlerleme ve loglar `ui` üzerinden UI'ye iletilir.
///
/// Hata durumunda: hatayı loglar, `cleanup` işlerini çalıştırır,
/// ardından `InstallMessage::Error` gönderir. Temizlik de hata
/// verirse bu hatalar orijinal hataya eklenerek raporlanır — böylece
/// kullanıcı hem asıl hatayı hem de "ne yapılması gerektiğini" görür.
pub async fn run_all(&mut self, ui: UiSender) {
let total = self.jobs.len();
@@ -181,24 +199,52 @@ impl JobQueue {
let localized_name = t!(job.name());
ui.log(format!("▶ {}", localized_name));
match job.run(&ui).await {
Ok(()) => {
ui.log(format!("✓ {}", localized_name));
}
Err(e) => {
let msg = format!("✗ {}: {}", localized_name, e);
ui.log(msg.clone());
ui.send(InstallMessage::Error(msg));
return;
}
if let Err(e) = job.run(&ui).await {
let msg = format!("✗ {}: {}", localized_name, e);
ui.log(msg.clone());
let report = self.run_cleanup(&ui, &e).await;
ui.send(InstallMessage::Error(report));
return;
}
ui.log(format!("✓ {}", localized_name));
let progress = (i + 1) as f32 / total as f32;
ui.send(InstallMessage::Progress(progress));
}
ui.send(InstallMessage::Done);
}
/// Temizlik işlerini çalıştırır; kullanıcıya gösterilecek rapor döndürür.
async fn run_cleanup(&self, ui: &UiSender, original_err: &str) -> String {
if self.cleanup.is_empty() {
return original_err.to_string();
}
ui.log("Kurulum durdu — bağlamalar temizleniyor…");
let mut leftovers: Vec<String> = Vec::new();
for job in &self.cleanup {
match job.run(ui).await {
Ok(()) => {}
Err(e) => leftovers.push(format!("{}: {}", t!(job.name()), e)),
}
}
if leftovers.is_empty() {
ui.log("✓ Bağlamalar temizlendi");
original_err.to_string()
} else {
ui.log(format!("⚠ {} temizlik sorunu", leftovers.len()));
let msg = format!(
"{}\n\n--- KURULUM DURDUKTAN SONRA ---\n\
Canlı sistemde elle temizlemeniz gerekenler var:\n{}",
original_err,
leftovers.iter().map(|l| format!(" • {}", l)).collect::<Vec<_>>().join("\n")
);
msg
}
}
}
// ─────────────────────────────────────────────
@@ -281,6 +327,90 @@ fn is_mount_point(path: &std::path::Path) -> bool {
}
}
/// `/proc/self/mountinfo` alanındaki kaçışlı diziyi çözer.
///
/// mountinfo, yol içindeki özel karakterleri `\0NNN` **oktal** olarak yazar
/// (boşluk `\040`, tab `\011`, satır sonu `\012`, backslash `\134`).
/// Çözülmezse "My Disk" gibi bir mountpoint hiçbir koşulla eşleştirilemez.
fn unescape_mount_field(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 3 < bytes.len() + 1 {
// `\0NNN` veya `\NNN` biçiminde 3 haneli oktal
let (start, width) = if bytes.get(i + 1) == Some(&b'0') { (i + 1, 4) } else { (i + 1, 3) };
if start + width <= bytes.len() {
let digits = &s[start..start + width];
if digits.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
if let Ok(v) = u32::from_str_radix(digits, 8) {
out.push(v as u8);
i = start + width;
continue;
}
}
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
/// Bir bağlama noktasının altındaki TÜM bağlı dosya sistemlerini döndürür,
/// en derinden yüzeye doğru sıralı.
///
/// Neden gerekiyor: `UnmountBindJob` elle yazılmış bir liste
/// (`proc`, `sys`, `dev`, `boot/efi`, ...) kullanıyordu. Manuel bölümlemede
/// `/home`, `/var`, `/boot` gibi ek bölümler o listede yoktu ve kurulum
/// sonrası bağlı kalıyordu. Bu fonksiyon düzeni bilmez — `mount` altındaki
/// her şeyi bulur, LUKS/LVM/fuse/loop ne olursa olsun.
fn mounted_paths_under(mount: &str) -> Vec<String> {
let prefix = format!("{}/", mount.trim_end_matches('/'));
let root = mount.trim_end_matches('/').to_string();
let mut paths: Vec<String> = Vec::new();
if let Ok(info) = std::fs::read_to_string("/proc/self/mountinfo") {
for line in info.lines() {
// mountinfo alanları: id parent major:minor root mountpoint
// options [optional...] - fstype source superopts
let Some(mp_field) = line.split_whitespace().nth(4) else { continue };
let mp = unescape_mount_field(mp_field);
if mp == root || mp.starts_with(&prefix) {
paths.push(mp);
}
}
}
// `/proc/self/mountinfo` kökte de bir satır yoksa kökü de dahil et
if !paths.iter().any(|p| p == &root) && is_mount_point(std::path::Path::new(&root)) {
paths.push(root.clone());
}
// En derin yol önce: "/mnt/a/b" -> "/mnt/a" -> "/mnt".
// Aynı derinlikte ise uzun yol önce.
paths.sort_by(|a, b| b.len().cmp(&a.len()));
paths.dedup();
paths
}
/// LUKS mapper adlarını döndürür (typeseri `crypt` olan aygıtlar).
fn open_crypt_mappers() -> Vec<String> {
let out = std::process::Command::new("lsblk")
.args(["-ndo", "NAME,TYPE"])
.output();
let Ok(out) = out else { return Vec::new() };
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| {
let mut it = l.split_whitespace();
let name = it.next()?;
let kind = it.next()?;
(kind == "crypt").then(|| name.to_string())
})
.collect()
}
/// `blkid` ile bir bölümün dosya sistemi türünü döndürür.
async fn get_fs_type(device: &str) -> Result<String, String> {
let output = tokio::process::Command::new("blkid")
@@ -511,6 +641,8 @@ pub fn build_full_job_queue(
let mt = cfg.mount.clone();
let plan2 = plan.clone();
let plan3 = plan.clone();
// Temizlik için bölüm yolları; `plan` birkaç satır sonra taşınıyor.
let cleanup_parts = auto_mode_partitions(&plan);
// Kaynak dizini: parametre boşsa otomatik tespit et
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
@@ -613,7 +745,30 @@ pub fn build_full_job_queue(
// 20. Bind-mount temizle
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
JobQueue::new(jobs)
// 21. Son temizlik: LUKS mapper'larını kapat, swap'u indir.
// Normal akışta da çalışır — yeniden başlatmadan önce mapper'ın
// açık kalması gereksiz ve zararlıdır.
jobs.push(Box::new(EmergencyCleanupJob {
mount: mt.clone(),
partitions: cleanup_parts.clone(),
}));
JobQueue::new(jobs).with_cleanup(vec![Box::new(EmergencyCleanupJob {
mount: mt.clone(),
partitions: cleanup_parts,
})])
}
/// Otomatik modda oluşturulan bölüm yolları (temizlik için).
/// Sıra `PartitionJob` ile aynı olmalıdır: EFI → swap → kök.
fn auto_mode_partitions(plan: &PartitionPlan) -> Vec<String> {
let mut parts = Vec::new();
if plan.efi_mb > 0 {
parts.push(part_path(&plan.disk, 1));
}
parts.push(part_path(&plan.disk, if plan.efi_mb > 0 { 2 } else { 1 })); // swap
parts.push(part_path(&plan.disk, if plan.efi_mb > 0 { 3 } else { 2 })); // kök
parts
}
/// Manuel bölümleme kullanıldığında derlenecek kurulum iş kuyruğu.
@@ -686,5 +841,19 @@ pub fn build_custom_job_queue(
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
JobQueue::new(jobs)
// Son temizlik (bkz. otomatik mod).
let cleanup_parts: Vec<String> = custom_partitions.iter()
.filter(|p| !p.delete)
.map(|p| p.device.clone())
.filter(|d| !d.is_empty())
.collect();
jobs.push(Box::new(EmergencyCleanupJob {
mount: mt.clone(),
partitions: cleanup_parts.clone(),
}));
JobQueue::new(jobs).with_cleanup(vec![Box::new(EmergencyCleanupJob {
mount: mt.clone(),
partitions: cleanup_parts,
})])
}
+26 -8
View File
@@ -196,6 +196,16 @@ impl Job for PartitionJob {
let disk = &self.plan.disk;
let is_gpt = self.plan.table_type == PartitionTableType::Gpt;
// encrypt_root açık ama parola boşsa kök düz ext4 olarak formatlanır,
// MountPartitionsJob / GenerateFstabJob ise yalnızca `encrypt_root`
// bakıp /dev/mapper/cryptroot'a mount etmeye çalışır → kurulum
// yarıda ölür. Bu kombinasyon UI'dan da engellenmeli.
if self.plan.encrypt_root && self.plan.luks_password.is_empty() {
return Err("Kök şifreleme (LUKS) seçilmiş ancak parola boş. \
Lütfen parola girin veya şifrelemeyi kapatın."
.to_string());
}
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
prepare_disk(disk, ui).await;
@@ -229,11 +239,16 @@ impl Job for PartitionJob {
next_mb = swap_end;
// 4. Kök bölümü (kalan alan)
// DİKKAT: parted "MB" sonekini ONDALIK (10^6) okur, "MiB" ikili
// (2^20). Hesaplarımız ikili olduğu için daima "MiB" geçilir.
// "MB" ile 4609MB = 9001992.1875 sektör → hizasız başlangıç →
// "Becerebildiğimiz buna en yakın konum..." hatası. Tamsayı MiB
// 2048 sektörün katıdır, hizalama sorunu olmaz.
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
ui.log(format!("Kök bölümü oluşturuluyor: {}MiB – disk sonu", next_mb));
run_parted(&[
"-s", disk, "mkpart", "ext4",
&format!("{}MB", next_mb), "100%",
&format!("{}MiB", next_mb), "100%",
]).await?;
// 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
@@ -258,8 +273,9 @@ impl Job for PartitionJob {
// LUKS şifreleme (kök bölümü)
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() {
let luks_name = "cryptroot";
// (Parolanın boş olma durumu yukarıda reddedildi.)
let root_device = if self.plan.encrypt_root {
let luks_name = super::bootloader::CRYPTROOT_NAME;
ui.log(format!("cryptsetup luksFormat {}…", root_part));
run_luks_format(&root_part, &self.plan.luks_password).await
.map_err(|e| format!("LUKS format hatası: {}", e))?;
@@ -297,7 +313,7 @@ impl Job for MountPartitionsJob {
let root_part = part_path(disk, root_num);
let root_device = if self.plan.encrypt_root {
"/dev/mapper/cryptroot".to_string()
format!("/dev/mapper/{}", super::bootloader::CRYPTROOT_NAME)
} else {
root_part.clone()
};
@@ -378,9 +394,10 @@ impl Job for GenerateFstabJob {
// Kök bölümü
if self.encrypt_root {
fstab.push_str(
"/dev/mapper/cryptroot / ext4 defaults,noatime 0 1\n"
);
fstab.push_str(&format!(
"/dev/mapper/{} / ext4 defaults,noatime 0 1\n",
super::bootloader::CRYPTROOT_NAME
));
} else {
fstab.push_str(&format!(
"UUID={} / ext4 defaults,noatime 0 1\n",
@@ -410,7 +427,8 @@ impl Job for GenerateFstabJob {
if self.encrypt_root {
let crypttab_path = format!("{}/etc/crypttab", self.mount);
let crypttab = format!(
"cryptroot UUID={} none luks\n",
"{} UUID={} none luks\n",
super::bootloader::CRYPTROOT_NAME,
self.root_uuid
);
ui.log(format!("Yazılıyor: {}", crypttab_path));
+65 -56
View File
@@ -24,7 +24,7 @@ impl Job for CreateUserJob {
}
// useradd: kullanıcıyı temel gruplarla oluştur
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grub yoksa atla
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grup yoksa atla
// Güvenli yöntem: önce her grubu tek tek ekle
let groups = ["wheel", "users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
for grp in &groups {
@@ -47,37 +47,22 @@ impl Job for CreateUserJob {
&self.username,
]).await?;
// // chpasswd ile şifre ayarla (stdin üzerinden güvenli)
// ui.log(format!("Şifre ayarlanıyor: {}", self.username));
// let chpasswd_input = format!("{}:{}", self.username, self.password);
// set_password_via_chpasswd(&self.mount, &chpasswd_input).await?;
// // Root şifresi: belirlenmişse ayarla, yoksa kilitle
// if self.root_password.is_empty() {
// ui.log("Root hesabı kilitleniyor…");
// run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
// } else {
// ui.log("Root şifresi ayarlanıyor…");
// let root_input = format!("root:{}", self.root_password);
// set_password_via_chpasswd(&self.mount, &root_input).await?;
// }
// 2. chpasswd ile şifre ayarla (YENİ BASİTLEŞTİRİLMİŞ VE KESİN YÖNTEM)
// Kullanıcı şifresi: `chpasswd` stdin üzerinden.
// `sh -c "echo 'user:pass' | ..."` KULLANILMAZ: parola düz metin
// olarak kabuğun argv'sine geçer ve /proc/<pid>/cmdline üzerinden
// okunabilir; ayrıca `'` veya `$(...)` içeren parola komutu bozar.
// Aynı desen jobs/partition.rs::run_luks_format'ta da kullanılıyor.
ui.log(format!("Şifre ayarlanıyor: {}", self.username));
// sh -c kullanarak echo ile chpasswd'e şifreyi güvenli bir şekilde üflüyoruz
let user_cmd = format!("echo '{}:{}' | chpasswd -c SHA512", self.username, self.password);
run_chroot(&self.mount, &["sh", "-c", &user_cmd]).await?;
set_password_via_chpasswd(&self.mount, &self.username, &self.password).await?;
// 3. Root şifresi: belirlenmişse ayarla, yoksa kilitle
// Root şifresi: belirlenmişse ayarla, yoksa kilitle
if self.root_password.is_empty() {
ui.log("Root hesabı kilitleniyor…");
run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
} else {
ui.log("Root şifresi ayarlanıyor…");
let root_cmd = format!("echo 'root:{}' | chpasswd -c SHA512", self.root_password);
run_chroot(&self.mount, &["sh", "-c", &root_cmd]).await?;
}
set_password_via_chpasswd(&self.mount, "root", &self.root_password).await?;
}
// sudoers: wheel grubu sudo yetkisi alsın
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
@@ -96,39 +81,63 @@ impl Job for CreateUserJob {
}
}
// /// `chpasswd` komutuna stdin üzerinden kullanıcı:şifre gönderir.
// async fn set_password_via_chpasswd(mount: &str, input: &str) -> Result<(), String> {
// if DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
// tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
// return Ok(());
// }
/// `chroot <mount> chpasswd -c SHA512` komutuna `kullanici:sifre` bilgisini
/// **stdin pipe'ı** üzerinden iletir.
///
/// Neden `sh -c` + `format!` değil:
/// 1. Parola kabuğun argv'sine yazılırsa `/proc/<pid>/cmdline` üzerinden
/// canlı sistemdeki herhangi bir süreç tarafından okunabilir.
/// 2. Parola içindeki `'` veya `$(...)` karakterleri komutu bozar ve
/// shell injection'a yol açar.
/// 3. `format!` ile üretilen komut `user:pass` içinde `:` geçse hatalı
/// ayrıştırılır.
///
/// Bu desen `jobs/partition.rs::run_luks_format` ile birebir aynıdır.
async fn set_password_via_chpasswd(
mount: &str,
username: &str,
password: &str,
) -> Result<(), String> {
if super::DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
return Ok(());
}
// use tokio::io::AsyncWriteExt;
// use tokio::process::Command;
use tokio::io::AsyncWriteExt;
// let mut child = Command::new("chroot")
// .args([mount, "chpasswd"])
// .stdin(std::process::Stdio::piped())
// .stdout(std::process::Stdio::null())
// .stderr(std::process::Stdio::piped())
// .spawn()
// .map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
let mut child = tokio::process::Command::new("chroot")
.args([mount, "chpasswd", "-c", "SHA512"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
// if let Some(mut stdin) = child.stdin.take() {
// let mut data = input.as_bytes().to_vec();
// if !data.ends_with(b"\n") {
// data.push(b'\n');
// }
// stdin.write_all(&data).await
// .map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
// }
// Parolayı yalnızca çocuk sürecin stdin'ine yaz; argv'de hiç görünmez.
if let Some(mut stdin) = child.stdin.take() {
// chpasswd "user:pass" satırı bekler ve satır sonu ile ayırır.
let mut data = format!("{}:{}\n", username, password).into_bytes();
if data.last() != Some(&b'\n') {
data.push(b'\n');
}
let result = stdin.write_all(&data).await;
// Yazdıktan sonra parolanın kopyasını bellekte bırakma.
data.iter_mut().for_each(|b| *b = 0);
drop(stdin);
result.map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
}
// let output = child.wait_with_output().await
// .map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
let output = child.wait_with_output().await
.map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
// if output.status.success() {
// Ok(())
// } else {
// Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
// }
// }
if output.status.success() {
Ok(())
} else {
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
if err.is_empty() {
Err(format!("{} için şifre ayarlanamadı", username))
} else {
Err(err)
}
}
}
+86 -10
View File
@@ -126,11 +126,20 @@ impl RescueStep {
let op: u32 = op_str.parse().unwrap_or(0);
// Tarih: "[2024-03-15 12:00:01]"
let date = if let Some(start) = remainder.find('[') {
if let Some(end) = remainder.find(']') {
remainder[start+1..end].to_string()
} else { String::new() }
} else { String::new() };
// `]` ve `[` bağımsız aranırsa, `]` önce gelen bir girdide
// `end < start` olur ve `remainder[start+1..end]` PANİK verir
// ("slice index starts at N but ends at M"). Kapanış
// parantez, açılıştan SONRA aranmalı.
let date = match remainder.find('[') {
None => String::new(),
Some(open) => {
let inner = open + 1;
match remainder[inner..].find(']') {
Some(rel_close) => remainder[inner..inner + rel_close].to_string(),
None => String::new(),
}
}
};
// Tür + özet: ": install [vim, curl]"
let (op_type, summary) = if let Some(colon) = remainder.rfind("]: ") {
@@ -372,11 +381,16 @@ impl RescueStep {
ui.colored_label(type_color, &entry.op_type);
// Özet — truncate
let summary = if entry.summary.len() > 50 {
format!("{}…", &entry.summary[..50])
} else {
entry.summary.clone()
};
// `len()` bayt sayar, `&s[..50]` ise bayt
// sınırı ister. `pisi history` çıktısı paket
// adlarından gelir ve Türkçe karakter içerir
// ("İ", "ş", "ğ" 2 bayt): `[..50]` karakter
// sınırının ortasına denk gelirse PANİK.
// Karakter sayarak kırpmak gerekir.
let mut summary: String = entry.summary.chars().take(50).collect();
if entry.summary.chars().count() > 50 {
summary.push('…');
}
ui.label(egui::RichText::new(summary).size(11.0).color(theme::c_text_dim()));
ui.end_row();
}
@@ -510,3 +524,65 @@ impl InstallerStep for RescueStep {
false // Rescue modu her zaman "tamamlanmamış" — normal kuruluma dönmek için rescue_mode=false
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Özet kırpma: Türkçe karakterler 2 bayt olduğu için bayt tabanlı
/// dilimleme karakter sınırının ortasına denk gelip panik verirdi.
fn truncate(s: &str, max: usize) -> String {
let mut out: String = s.chars().take(max).collect();
if s.chars().count() > max {
out.push('…');
}
out
}
#[test]
fn truncate_handles_multibyte_without_panicking() {
// "şğüöç" 5 karakter × 20 = 100 karakter, 200 bayt. 50 karakterlik
// kesme bayt sınırının tam ortasına denk gelirdi ve `&s[..50]`
// PANİK verirdi.
let s = "şğüöç".repeat(20);
assert_eq!(s.chars().count(), 100);
let t = truncate(&s, 50);
assert_eq!(t.chars().count(), 51, "50 karakter + '…'");
assert!(t.ends_with('…'));
assert!(t.chars().take(50).all(|c| "şğüöç".contains(c)));
}
#[test]
fn truncate_leaves_short_strings_untouched() {
assert_eq!(truncate("kısa", 50), "kısa");
assert_eq!(truncate("", 50), "");
}
#[test]
fn parse_history_handles_stray_close_bracket() {
// "] Operation ..." — ']' '[' tan önce geliyor. Eski kod
// remainder[start+1..end] ile PANİK verirdi.
let out = RescueStep::parse_pisi_history("Operation 5 ] bogus: install [vim]");
assert_eq!(out.len(), 1, "girdi hâlâ işlenmeli");
assert_eq!(out[0].op, 5);
}
#[test]
fn parse_history_reads_date_and_summary() {
let raw = "Operation 42 [2024-03-15 12:00:01]: install [vim, curl]";
let out = RescueStep::parse_pisi_history(raw);
assert_eq!(out.len(), 1);
assert_eq!(out[0].op, 42);
assert_eq!(out[0].date, "2024-03-15 12:00:01");
assert_eq!(out[0].op_type, "install");
// Özet, "]: " sonrasındaki ilk boşluktan itibaren alınır; paket
// listesi köşeli parantezleriyle birlikte kalır (mevcut davranış).
assert_eq!(out[0].summary, "[vim, curl]");
}
#[test]
fn parse_history_tolerates_unterminated_bracket() {
let out = RescueStep::parse_pisi_history("Operation 7 [2024-03-15 12:00:01: install [x]");
assert_eq!(out.len(), 1, "panik vermemeli, kayıt üretmeli");
}
}