From d161556c73330e0970ec39bf3d27f495fb531645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Fri, 25 Sep 2026 22:28:11 +0300 Subject: [PATCH] =?UTF-8?q?fix(P0):=20parola=20s=C4=B1z=C4=B1nt=C4=B1s?= =?UTF-8?q?=C4=B1,=20parted=20birim=20hatas=C4=B1,=20hatal=C4=B1=20LUKS=20?= =?UTF-8?q?cmdline,=20yutulan=20hatalar,=20hata=20temizli=C4=9Fi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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=: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) --- src/jobs/bootloader.rs | 80 +++++++++++++++- src/jobs/cleanup.rs | 201 +++++++++++++++++++++++++++++++++++++++++ src/jobs/execution.rs | 40 ++++---- src/jobs/finish.rs | 115 +++++++++++++---------- src/jobs/mod.rs | 195 ++++++++++++++++++++++++++++++++++++--- src/jobs/partition.rs | 34 +++++-- src/jobs/users.rs | 121 +++++++++++++------------ src/steps/rescue.rs | 96 ++++++++++++++++++-- 8 files changed, 722 insertions(+), 160 deletions(-) create mode 100644 src/jobs/cleanup.rs diff --git a/src/jobs/bootloader.rs b/src/jobs/bootloader.rs index fcd49e3..abb2084 100644 --- a/src/jobs/bootloader.rs +++ b/src/jobs/bootloader.rs @@ -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 { + 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- + // mkinitcpio sözdizimi: cryptdevice=UUID=: + // 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 { diff --git a/src/jobs/cleanup.rs b/src/jobs/cleanup.rs new file mode 100644 index 0000000..6e9d2e4 --- /dev/null +++ b/src/jobs/cleanup.rs @@ -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, +} + +#[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 = 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::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 ` 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, 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 { + 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::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)) +} diff --git a/src/jobs/execution.rs b/src/jobs/execution.rs index ca861d0..687ac16 100644 --- a/src/jobs/execution.rs +++ b/src/jobs/execution.rs @@ -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 = 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 diff --git a/src/jobs/finish.rs b/src/jobs/finish.rs index ee9512e..2b67f52 100644 --- a/src/jobs/finish.rs +++ b/src/jobs/finish.rs @@ -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(()) diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 19ce06b..7e15533 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -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>, + /// 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>, } impl JobQueue { pub fn new(jobs: Vec>) -> 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>) -> 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 = 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::>().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 { + let prefix = format!("{}/", mount.trim_end_matches('/')); + let root = mount.trim_end_matches('/').to_string(); + + let mut paths: Vec = 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 { + 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 { 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 { + 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 = 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, + })]) } diff --git a/src/jobs/partition.rs b/src/jobs/partition.rs index d3ca5a9..6743382 100644 --- a/src/jobs/partition.rs +++ b/src/jobs/partition.rs @@ -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)); diff --git a/src/jobs/users.rs b/src/jobs/users.rs index 7eb417d..d13d507 100644 --- a/src/jobs/users.rs +++ b/src/jobs/users.rs @@ -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//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 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//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) + } + } +} diff --git a/src/steps/rescue.rs b/src/steps/rescue.rs index 1374624..edd4df7 100644 --- a/src/steps/rescue.rs +++ b/src/steps/rescue.rs @@ -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"); + } +}