From 70f3737f01759d64864798c49dbf54990ea6f61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Sun, 27 Sep 2026 18:00:11 +0300 Subject: [PATCH] =?UTF-8?q?fix(partition):=20"Kalan=20Alan"=20tan=C4=B1m?= =?UTF-8?q?=C4=B1=20alan=C4=B1=20bo=C5=9F=20alandan=20d=C3=BC=C5=9Fm=C3=BC?= =?UTF-8?q?yordu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kullanıcı raporu: "boş alanı gösterdi ve o kısmı yeni bölümden ntfs olarak formatlamasını seçtim ama eklenen bölümün 19.8 gb demesine rağmen bunu boş alandan düşmedi." ## Kök neden `size_mb == 0` bir tanım **"Kalan Alan"** demektir: kurulumda diskin o an kalanının TAMAMINI alır. Bu yüzden sabit boyutlu tanımlarla toplanırken katılmaz (`size_mb` zaten 0). Ama ne tablo ne de kapasite kontrolü bunu biliyordu. Sonuç: 20 GB'lik disk 39 GB kullanıyor gibi görünüyordu — bölüm satırı kalanın tamamını (19.8 GB) gösteriyor, alttaki "Boş Alan" satırı da aynı 19.8 GB'yi gösteriyordu. ## Düzeltme `has_symbolic_remaining(parts, disk)` tek kaynak olarak eklendi (`!delete && size_mb == 0 && orig_device.is_empty()`) ve dört yere bağlandı: - **tablo** (`build_table_rows`): sembolik varsa boş alan satırı çizilmez — o alan o tanıma ait - **çubuk** (`manual_dual_bars`): sembolik varsa serbest segment eklenmez. Zaten doğruydu ama kuralın AYRI kopyası vardı; artık aynı fonksiyona bakıyor, tablo ile çubuk ayrışamaz - **kapasite** (`manual_capacity_error`): sembolik varsa `fixed_mb >= cap_mb` reddedilir ## Kapanan açık madde AGENTS.md'de şu yazıyordu: "`manual_capacity_error` sembolik (`size_mb == 0`) tanımları hesaba katmıyor → `fixed == cap` geçiyor, kurulumda 'yeterli boş alan yok' ile düşüyor." Disk sabit tanımlarla tam doluysa ve bir de "Kalan Alan" tanımı varsa o tanıma 0 byte kalıyordu. Artık `>=` ile reddediliyor. Ayrıca **aynı diskte iki "Kalan Alan" tanımı** reddediliyor: ikisi de kalanın tamamını alır, ikincisine hiç yer kalmaz. Bu da kuruluma başlayıp orada düşmeye yol açan bir durumdu. ## Not `orig_device` dolu (taranmış) 0 boyutlu bölüm sembolik SAYILMAZ: sıfır boyut "kalanın tamamı" anlamına gelmez. Testle sabitlendi. ## Testler (113 → 122) Yeni `symbolic_capacity_tests` (5) + `free_space_tests`'e 4 test: - sembolik tanım boş alan satırını yutar (kullanıcı senaryosu) - sembolik bölümün kendi satırı kalanın tamamını gösterir - sembolik tanım silinince boş alan satırı geri gelir - 0 boyutlu fiziksel bölüm sembolik sayılmaz - dolu disk + sembolik tanım reddedilir - yer olan sembolik tanım geçerli - aynı diskte iki sembolik reddedilir - her diskte birer tane geçerli - sabit tanımlarla tam dolu disk (semboliksiz) yine geçerli Testlerden biri ilk yazımda yanlış beklenti koydu (sembolik silinince 20480 bekliyordum, doğrusu 19980 — sda1'in 500 MB'ı hâlâ kullanımda). Beklenti düzeltildi. cargo check --all-targets : 0 hata, 0 uyarı cargo test : 122/122 Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 36 +++++-- src/steps/partition.rs | 227 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 253 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 52b3d7d..b82c42b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -298,6 +298,31 @@ ikincil (veri) diskler içindir: boş diske bölüm eklemek için alanı elle doldurmak zorunda kalmamak. Silinmek işaretli tanımlar sayılmaz. +### ⭐ "Kalan Alan" (sembolik) tanımlar — `size_mb == 0` + +`CustomPartition::size_mb == 0` **"Kalan Alan"** demektir: kurulumda +diskin o an kalan boşluğunun **tamamını** alır. Bu yüzden sabit +boyutlu tanımlarla birlikte hesaplanırken **sabit toplama katılmaz**. + +Kullanıcı raporu: *"eklenen bölümün 19.8 gb demesine rağmen bunu +boş alandan düşmedi"* — 20 GB'lik disk 39 GB kullanıyor gibi görünüyordu, +çünkü hem bölüm satırı hem boş alan satırı kalanın tamamını gösteriyordu. + +| Yer | Kural | +|---|---| +| `has_symbolic_remaining(parts, disk)` | **TEK KAYNAK.** `!delete && size_mb == 0 && orig_device.is_empty()` | +| tablo (`build_table_rows`) | sembolik varsa boş alan satırı **çizilmez** — o alan o tanıma ait | +| çubuk (`manual_dual_bars`) | sembolik varsa serbest segment **eklenmez** | +| `manual_capacity_error` | sembolik varsa `fixed_mb >= cap_mb` reddedilir | + +Son satır, AGENTS.md'de açık madde olarak duran hatayı kapatır: +`fixed == cap` geçiyordu, kurulumda "yeterli boş alan yok" ile düşülüyordu. +Ayrıca **aynı diskte iki "Kalan Alan" tanımı** reddedilir — ikisi de +kalanın tamamını alır, ikincisine hiç yer kalmaz. + +`orig_device` dolu (taranmış) 0 boyutlu bölüm sembolik **DEĞİLDİR**: +sıfır boyut "kalanın tamamı" anlamına gelmez. + ### `selected_disk` hâlâ 3 yerde kullanılıyor 1. Tablo hangi diski gösteriyor + `manual_dual_bars` 2. Diyaloğun varsayılan hedefi @@ -457,11 +482,11 @@ nedeni yorumla sabitlendi. Log bile "MiB" derken argüman "MB" gönderiyordu. ``` cargo check --all-targets : 0 hata, 0 uyarı -cargo test : 113/113 (test modülü sayısı arttıkça güncel tut) +cargo test : 122/122 (test modülü sayısı arttıkça güncel tut) cargo clippy : ~77 uyarı, tamamı stil (collapsed_if, replace zinciri) ``` -Bu oturumda 1 → 113 test. Test modülü olmayan dosyalar (`users.rs`, +Bu oturumda 1 → 122 test. Test modülü olmayan dosyalar (`users.rs`, `location.rs`, `rescue.rs`, `funct.rs`, `autoinstall/mod.rs`, `main.rs`) içine modüller eklendi. Çoklu test modülü paylaşılan yardımcıları `steps/partition.rs::testutil` altındadır (`disk()`, `part()`). @@ -507,9 +532,8 @@ senaryoları dene: job `pvcreate` çalıştırmaya çalışabilir) - [ ] BIOS `device.map` `/dev` **alfabetik** sıraya göre yazılıyor; firmware sırası ile aynı olmayabilir → çoklu diskte BIOS boot kırılgan -- [ ] `manual_capacity_error` sembolik (`size_mb == 0`) tanımları - hesaba katmıyor → `fixed == cap` geçiyor, kurulumda "yeterli boş - alan yok" ile düşüyor +- [ ] ~~`manual_capacity_error` sembolik tanımları hesaba katmıyor~~ — + KAPANDI (bkz. "Kalan Alan" bölümü; `has_symbolic_remaining`) ### Orta öncelik - [ ] `available_disks` `< 4 GB` diski tamamen gizliyor (ComboBox'ta yok, @@ -557,7 +581,7 @@ senaryoları dene: |---|---| | `src/jobs/partition.rs` | `plan_disk_layout`, `PhysicalExtent`, `PlannedCreate`, `disk_total_mb`, `read_physical_extents`, `device_map`; `disk_free_regions_mb` silindi | | `src/jobs/cleanup.rs` | **YENİ** — `EmergencyCleanupJob` | -| `src/steps/partition.rs` | **tek diske odaklı tablo + disk şeridi** (diyalogda disk seçicisi yok), **seçime göre görünen butonlar** (`selected_free`), diske filtreli LVM, `validate_manual_on`, `mountpoint_required`, `suggest_mountpoint`, `build_table_rows`; `multidisk_tests`, `table_scope_tests`, `free_space_tests`, `selection_tests`, `edit_flow_tests`, `edit_disk_tests`, `testutil` | +| `src/steps/partition.rs` | **tek diske odaklı tablo + disk şeridi** (diyalogda disk seçicisi yok), **seçime göre görünen butonlar** (`selected_free`), diske filtreli LVM, `validate_manual_on`, `manual_capacity_error`, `mountpoint_required`, `suggest_mountpoint`, `has_symbolic_remaining`, `build_table_rows`; `multidisk_tests`, `table_scope_tests`, `free_space_tests`, `symbolic_capacity_tests`, `selection_tests`, `edit_flow_tests`, `edit_disk_tests`, `testutil` | | `src/jobs/users.rs` | `set_password_via_chpasswd` (stdin pipe) | | `src/jobs/bootloader.rs` | `CRYPTROOT_NAME`, `luks_backing_device`, `mounted_root_device` | | `src/jobs/finish.rs` | `RunComarJob` geri geldi; `ldconfig`/`update-environment`/depo fatal | diff --git a/src/steps/partition.rs b/src/steps/partition.rs index e86d258..23f7640 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -534,6 +534,22 @@ fn suggest_mountpoint(parts: &[CustomPartition], is_uefi: bool) -> &'static str } } +/// Diskte "Kalan Alan" (sembolik, `size_mb == 0`) bir tanım var mı? +/// +/// Böyle bir tanım, diskte kalan alanın **tamamını** kendine alır. +/// Bu yüzden o diskte ayrı bir "Boş Alan" satırı çizilmez: tablo +/// satırı zaten kalanın tamamını kendi boyut sütununda gösteriyor, +/// ikinci bir satır aynı alanı iki kez sayar ve kullanıcı diskin +/// iki katı büyük olduğunu sanır. +/// +/// `manual_dual_bars` (kapasite çubuğu) da aynı kuralı uygular; +/// ikisi de bu fonksiyona bakmalı ki tablo ile çubuk ayrışmasın. +fn has_symbolic_remaining(parts: &[CustomPartition], disk: &str) -> bool { + parts.iter().any(|p| { + p.disk == disk && !p.delete && p.size_mb == 0 && p.orig_device.is_empty() + }) +} + /// Bölümleme tablosunda görünen bir satır. /// /// Görünen her bölüm satırı `GlobalState.custom_partitions` içindeki @@ -583,6 +599,8 @@ fn build_table_rows<'a>( .map(|p| p.size_mb) .sum(); let free_total_mb = disk_mb.saturating_sub(used_total); + // "Kalan Alan" tanımı kalanın tamamını alır → boş alan satırı çizilmez. + let has_symbolic = has_symbolic_remaining(parts, active_disk); let mut fs_first: Option = None; // Boş alan satırı disk başına TEK'tir (GParted tarzı: silinenler @@ -620,7 +638,7 @@ fn build_table_rows<'a>( first_idx: Some(first), }); } - } else if free_total_mb > 0 && !free_row_shown { + } else if free_total_mb > 0 && !free_row_shown && !has_symbolic { // Disk SONUNDAKİ boş alan — diskte tanım olsun ya da olmasın. // // REGRESYON: koşul `all(|p| p.disk != active_disk)` idi, yani @@ -628,6 +646,14 @@ fn build_table_rows<'a>( // Diskte tanım var ama hiçbiri silinmek işaretli değilse // (`sda1` + 15 GB boş) satır hiç basılmıyordu. // + // `has_symbolic` kuralı: "Kalan Alan" tanımı varsa boş alan o + // tanıma aittir. REGRESYON 2: sembolik tanım (`size_mb == 0`) + // hiçbir hesaba katılmadığı için hem bölüm satırı hem boş alan + // satırı kalanın tamamını gösteriyordu — 20 GB'lik disk + // 39 GB kullanıyormuş gibi görünüyordu. Kullanıcı raporu: + // "eklenen bölümün 19.8 gb demesine rağmen bunu boş alandan + // düşmedi". + // // `first_idx: None` burada "seçilemez" DEĞİL demektir; yalnızca // temsil ettiği silinmiş bir tanım yoktur. Tıklanabilirdir // (`global_idx == None` dalı `selected_free` tutar ve @@ -695,8 +721,34 @@ fn manual_capacity_error(parts: &[CustomPartition], disks: &[DiskInfo]) -> Optio None => continue, }; + // Aynı diskte iki "Kalan Alan" tanımı çelişkidir: ikisi de + // kalanın tamamını alır, ikincisine hiç yer kalmaz. + let symbolic: Vec<&CustomPartition> = defs.iter() + .copied() + .filter(|p| p.size_mb == 0 && p.orig_device.is_empty()) + .collect(); + if symbolic.len() > 1 { + return Some(format!( + "{}: {} adet \"Kalan Alan\" tanımı var; her biri diskin kalanının tamamını alır, birden fazlası olamaz", + d, symbolic.len() + )); + } + + // Sembolik tanımın `size_mb` = 0 olduğu için sabit toplama + // KATILMAZ. Önceden yalnızca `fixed_mb > cap_mb` kontrolü + // vardı; disk tamamen sabit tanımlarla doluysa ve bir de + // "Kalan Alan" tanımı varsa `fixed_mb == cap_mb` geçiyordu ve + // kurulumda "yeterli boş alan yok" hatasıyla düşülüyordu. + // + // Sembolik varken boşluk SIRAYA BAĞLI: `fixed_mb == cap_mb` + // demek o tanıma 0 byte kalacağı demektir → `>=` ile reddedilir. let fixed_mb: u64 = defs.iter().map(|p| p.size_mb).sum(); - if fixed_mb > cap_mb { + let over_capacity = if symbolic.is_empty() { + fixed_mb > cap_mb + } else { + fixed_mb >= cap_mb + }; + if over_capacity { return Some(format!( "{}: sabit tanımlar ({:.1} GB) disk boyutunu ({:.1} GB) aşıyor", d, fixed_mb as f64 / 1024.0, cap_mb as f64 / 1024.0 @@ -2521,8 +2573,10 @@ fn manual_dual_bars(ui: &mut egui::Ui, state: &GlobalState, disk: &str) { (label, if p.size_mb == 0 { 0 } else { p.size_mb }, color) }) .collect(); - // Sembolik ("kalan") tanım varsa kalan boşluğun tamamını o alır - let has_symbolic = defs.iter().any(|p| !p.delete && p.size_mb == 0 && p.orig_device.is_empty()); + // Sembolik ("kalan") tanım varsa kalan boşluğun tamamını o alır. + // Tablo ile çubuk AYRIŞMAMASI için aynı kural ortak yardımcıdan + // gelir — önceden burada ayrı bir kopya vardı. + let has_symbolic = has_symbolic_remaining(&state.custom_partitions, disk); let fixed_new: u64 = new_segs.iter().map(|s| s.1).sum(); let free_mb = disk_mb.saturating_sub(fixed_new); @@ -3554,6 +3608,171 @@ mod free_space_tests { let parts = vec![part("/dev/sda1", 500, FsType::Ntfs, "")]; assert!(build_table_rows(&parts, "", 20_480).is_empty()); } + + // ── "Kalan Alan" (sembolik) tanımlar ─────────────────────── + + /// Kullanıcı raporu: "eklenen bölümün 19.8 gb demesine rağmen + /// bunu boş alandan düşmedi". + /// + /// REGRESYON: sembolik tanımın `size_mb` = 0 olduğu için + /// `used_total`'a katkısı yok; hem bölüm satırı hem boş alan + /// satırı kalanın tamamını gösteriyordu. + #[test] + fn symbolic_remaining_absorbs_the_free_space_row() { + let mut rest = part("/dev/sda2", 0, FsType::Ntfs, "/data"); + rest.orig_device = String::new(); // sembolik = yeni tanım + let parts = vec![part("/dev/sda1", 500, FsType::Ntfs, ""), rest]; + let rows = build_table_rows(&parts, "/dev/sda", 20_480); + + assert!( + free_mb_of(&rows).is_empty(), + "sembolik tanım kalanı alıyor, ayrı boş alan satırı olmamalı: {:?}", + free_mb_of(&rows) + ); + assert_eq!(rows.len(), 2, "sda1 + sembolik bölüm olmalı"); + } + + /// Sembolik tanımın kendi satırı kalanın tamamını GÖSTERİR — + /// kullanıcı alanın nereye gittiğini görebilmeli. + #[test] + fn symbolic_partition_row_shows_the_full_remainder() { + let mut rest = part("/dev/sda2", 0, FsType::Ntfs, "/data"); + rest.orig_device = String::new(); + let parts = vec![part("/dev/sda1", 500, FsType::Ntfs, ""), rest]; + let rows = build_table_rows(&parts, "/dev/sda", 20_480); + + let symbolic = rows.iter().find_map(|r| match r { + TableRow::Part(_, p) if p.size_mb == 0 => Some(p.device.as_str()), + _ => None, + }); + assert_eq!(symbolic, Some("/dev/sda2"), "sembolik bölüm satırı görünmeli"); + } + + /// Sembolik tanım silinmek işaretliyse boş alan satırı GERİ GELİR — + /// artık o alanı kimse almıyor. (sda1 hâlâ 500 MB kullandığı için + /// serbest alan 19980 MB, 20480 değil.) + #[test] + fn deleting_the_symbolic_definition_frees_the_row_again() { + let mut rest = part("/dev/sda2", 0, FsType::Ntfs, "/data"); + rest.orig_device = String::new(); + rest.delete = true; + let parts = vec![part("/dev/sda1", 500, FsType::Ntfs, ""), rest]; + let rows = build_table_rows(&parts, "/dev/sda", 20_480); + + assert_eq!( + free_mb_of(&rows), + vec![20_480 - 500], + "sembolik tanım silinince kalan boş alan satırı geri gelmeli" + ); + } + + /// `size_mb == 0` OLAN FİZİKSEL bölüm sembolik sayılmaz — 0 boyutlu + /// taranmış bölüm "kalanın tamamı" demek değildir. (Yine de fiziksel + /// bölümler `skip_format` geldiği için ayrı konu.) + #[test] + fn zero_sized_physical_partition_is_not_symbolic() { + let physical = part("/dev/sda1", 0, FsType::Ext4, ""); // orig_device DOLU + let parts = vec![physical]; + assert!( + !super::has_symbolic_remaining(&parts, "/dev/sda"), + "orig_device dolu bölüm sembolik sayılmamalı" + ); + } +} + +/// ⭐ Sembolik ("Kalan Alan") tanımların KAPASİTE doğrulaması +/// +/// Bu, AGENTS.md'de açık madde olarak duruyordu: +/// "`manual_capacity_error` sembolik (`size_mb == 0`) tanımları +/// hesaba katmıyor → `fixed == cap` geçiyor, kurulumda 'yeterli boş +/// alan yok' ile düşüyor". +#[cfg(test)] +mod symbolic_capacity_tests { + use super::manual_capacity_error; + use super::testutil::{disk, part}; + use crate::installer::FsType; + + fn symbolic(device: &str, fstype: FsType, mountpoint: &str) -> crate::installer::CustomPartition { + let mut p = part(device, 0, fstype, mountpoint); + p.orig_device = String::new(); + p + } + + /// Disk sabit tanımlarla TAM doluysa ve bir de "Kalan Alan" tanımı + /// varsa, o tanıma 0 byte kalır. Kuruluma başlamadan reddedilmeli. + #[test] + fn full_disk_with_a_symbolic_definition_is_rejected() { + let disks = vec![disk("/dev/sda", 20)]; + let parts = vec![ + part("/dev/sda1", 20_480, FsType::Ext4, "/"), + symbolic("/dev/sda2", FsType::Ext4, "/data"), + ]; + let err = manual_capacity_error(&parts, &disks); + assert!( + err.is_some(), + "dolu disk + Kalan Alan tanımı reddedilmeli: {:?}", + err + ); + } + + /// REGRESYON: burada sabit toplam 500 MB, kapasite 20 GB — bol yer + /// var, geçerli olmalı. `>=` kontrolü yanlışlıkla burada da + /// reddetmemeli. + #[test] + fn symbolic_with_headroom_is_accepted() { + let disks = vec![disk("/dev/sda", 20)]; + let parts = vec![ + part("/dev/sda1", 500, FsType::Ntfs, ""), + symbolic("/dev/sda2", FsType::Ntfs, "/data"), + ]; + assert_eq!( + manual_capacity_error(&parts, &disks), + None, + "yer olan sembolik tanım geçerli olmalı" + ); + } + + /// Aynı diskte iki "Kalan Alan" tanımı çelişkidir: ikisi de kalanın + /// tamamını alır, ikincisine hiç yer kalmaz. + #[test] + fn two_symbolic_definitions_on_one_disk_are_rejected() { + let disks = vec![disk("/dev/sda", 20)]; + let parts = vec![ + symbolic("/dev/sda1", FsType::Ext4, "/"), + symbolic("/dev/sda2", FsType::Ext4, "/data"), + ]; + let err = manual_capacity_error(&parts, &disks); + assert!(err.is_some(), "iki Kalan Alan tanımı reddedilmeli: {:?}", err); + } + + /// Farklı disklerde birer tane olabilir — /home sdb'de, / sda'da. + #[test] + fn one_symbolic_per_disk_is_accepted() { + let disks = vec![disk("/dev/sda", 20), disk("/dev/sdb", 500)]; + let parts = vec![ + part("/dev/sda1", 500, FsType::Ntfs, ""), + symbolic("/dev/sda2", FsType::Ext4, "/"), + symbolic("/dev/sdb1", FsType::Ext4, "/home"), + ]; + assert_eq!( + manual_capacity_error(&parts, &disks), + None, + "her diskte bir tane sembolik tanım geçerli olmalı" + ); + } + + /// Sembolik olmayan, sabit boyutlu tanımlar için davranış DEĞİŞMEDİ: + /// tam dolu olmak yine geçerli. + #[test] + fn exactly_full_disk_without_symbolic_is_accepted() { + let disks = vec![disk("/dev/sda", 20)]; + let parts = vec![part("/dev/sda1", 20_480, FsType::Ext4, "/")]; + assert_eq!( + manual_capacity_error(&parts, &disks), + None, + "sabit tanımlarla tam dolu disk geçerli olmalı" + ); + } } #[cfg(test)]