fix(partition): mount edilmemiş fiziksel bölüm düzeni geçersiz kılıyordu

Kullanıcı raporu: "sdc ise tamamen boş disk bağlama noktası boş
olamaz uyarısı veriyor."

Kurulum: sda'da Windows (sda1 boot sağlam, mount değil), sdb'de
Linux (EFI+swap+kök), sdc tamamen boş.

## Neden oluyordu

`read_existing_partitions` mountpoint'i lsblk'in MOUNTPOINT
sütunundan okur ve mount edilmemiş bölümlerde bu sütun BOŞTUR.
Çift önyüklemeli bir makinede Windows'un boot bölümü her zaman bu
durumdadır.

Ama `validate_manual_on` boş mountpoint'i TÜM tanımlarda reddediyordu
(LVM PV dışında) — kullanıcının hiç dokunmadığı sda1 yüzünden düzen
geçersiz oluyordu. Hata hangi bölümden geldiğini de söylemiyordu.

## Düzeltmeler

### 1. Boş mountpoint zorunluluğu yalnızca kullanıcı tanımlarında

Boş mountpoint'li FİZİKSEL bölüm "henüz atanmadı" demektir ve iş
zincirinde zaten güvenli: mount edilmez (jobs/partition.rs:1111),
fstab'a girmez (:1171), `skip_format: true` geldiği için
formatlanmaz da. Doğrulama bir şey engellemiyor, sadece kullanıcıyı
yanlış yönlendiriyordu.

Kural: `orig_device` boş (kullanıcı tanımı) ve LVM PV değilse zorunlu.

### 2. Mükerrer bağlama noktası kontrolü boş değerleri dışlar

İki atanmamış bölüm aynı "" değerini taşıdığı için kullanıcı var
olmayan bir "mükerrer" hatası görüyor, asıl sorun maskeleniyordu.

### 3. suggest_mountpoint: öneri tüm düzeni görmeli

`/`, `/boot/efi` ve `swap` TEKİLDİR. Öneri aktif diske bakıyordu;
boş bir ikinci diske geçince o diskte tanım olmadığı için
`/boot/efi` öneriyordu — sdb1'de zaten var olan mountpoint'i ikinci
kez. Kaydetmeye çalışınca "mükerrer", alanı temizleyince "boş
olamaz": iki hata da kullanıcıya hiçbir şey söylemiyordu.

Saf ve test edilebilir bir fonksiyona çıkarıldı. Sıra:
`/boot/efi` (UEFI) → `/` → `swap` → `/home` → boş. Son adım ikincil
(veri) diskler için; sdc'de artık `/home` önerilir. Silinmek
işaretli tanımlar sayılmaz.

## Testler (92 → 98)

6 yeni test, kullanıcının senaryosu dahil:
- mount edilmemiş fiziksel bölüm düzeni geçersiz kılmaz
- birden çok atanmamış bölüm mükerrer sayılmaz
- boş ikinci disk /home önerir, mükerrer /boot/efi önermez
- öneri aktif diske değil tüm düzene bakar
- öneri sırası kök → swap
- silinen tanım "alınmış" sayılmaz

Mevcut `lvm_pv_may_have_no_mountpoint_but_others_may_not` testi
yeni semantiğe göre güncellendi: "normal bölüm" yerine "kullanıcı
tanımı" (orig_device boş) reddediliyor.

cargo check --all-targets : 0 hata, 0 uyarı
cargo test              : 98/98

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erkan IŞIK
2026-09-27 16:00:35 +03:00
parent 3104adbc41
commit a47c6e52bc
2 changed files with 255 additions and 46 deletions
+43
View File
@@ -239,6 +239,49 @@ sda1 ile sdb1 çakışmaz.
PV'lerinden toplanır — önceden tüm disklerin PV'leri birleştiriliyordu
ve boyut toplamı yanlış çıkıyordu (AGENTS.md'deki açık maddeydi).
### ⭐ Mountpoint boşluğu mount EDİLMEMİŞ bölüm demek değil
`read_existing_partitions` mountpoint'i **`lsblk` MOUNTPOINT sütunundan**
okur (satır ~67: `tokens.next().unwrap_or("")`). Canlı ISO'da mount
edilmemiş her bölümde bu sütun **boştur**.
**Çift önyüklemeli makinede Windows'un boot bölümü neredeyse her zaman
bu durumdadır** ve kullanıcı ona dokunmamıştır. Buna rağmen
`validate_manual_on` tüm tanımları kapsayan bir "boş mountpoint"
kontrolü içeriyordu → düzen reddediliyordu, hata hangi bölümden
geldiğini söylemeden.
Kural artık: **boş mountpoint zorunluluğu yalnızca `orig_device` BOŞ
olan (kullanıcı tanımı) bölümlerde geçerli.** Fiziksel taranmış
bölümde boş mountpoint "henüz atanmadı" demektir ve iş zincirinde
zaten güvenli:
| Aşama | Davranış |
|---|---|
| mount | atlanır (`jobs/partition.rs:1111`) |
| fstab | atlanır (`:1171`) |
| mkfs | atlanır (`skip_format: true`) |
Aynı sebeple **mükerrer bağlama noktası kontrolü de boş değerleri
dışlar** — aksi hâlde "N atanmamış bölüm" kendi kendine mükerrer
sayılır ve asıl sorun maskelenir.
### `suggest_mountpoint` — öneri TÜM düzeni görmeli
`/`, `/boot/efi` ve `swap` bir kurulumda **TEKİLDİR**. Öneri bu
yüzden aktif diske değil **tüm düzene** bakar.
Aktif diske bakıyordu: kullanıcı boş bir ikinci diske geçip "Yeni
Bölüm" dediğinde o diskte tanım olmadığı için `/boot/efi` öneriliyordu
— sdb1'de zaten var olan mountpoint'i ikinci kez. Kullanıcı
kaydetmeye çalışınca "mükerrer" alıyor, alanı temizleyince "boş
olamaz" alıyordu. İkisi de hiçbir şey söylemiyordu.
Sıra: `/boot/efi` (UEFI) → `/` → `swap` → `/home` → boş. Son adım
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.
### `selected_disk` hâlâ 3 yerde kullanılıyor
1. Tablo hangi diski gösteriyor + `manual_dual_bars`
2. Diyaloğun varsayılan hedefi
+212 -46
View File
@@ -409,10 +409,21 @@ fn validate_manual_on(
}
}
// Aynı mountpoint'ta iki bölüm tanımlı mı kontrolü
let mountpoints: std::collections::HashSet<&str> =
live_parts.iter().map(|p| p.mountpoint.as_str()).collect();
if mountpoints.len() != live_parts.len() {
// Aynı mountpoint'ta iki bölüm tanımlı mı kontrolü.
//
// BOŞ mountpoint'ler ÇİKARILIR: `read_existing_partitions` mountpoint'i
// lsblk'in MOUNTPOINT sütunundan alır ve canlı ISO'da mount EDILMEMİŞ
// bölümlerde bu sütun boştur. Çift önyüklemeli bir makinede Windows'un
// sda1'i neredeyse her zaman boş gelir. Boş değerler karşılaştırılırsa
// "N tane atanmamış bölüm" kendi kendine mükerrer sayılır ve kullanıcı
// var olmayan bir çakışma hatası görürdü.
let mountpoints: std::collections::HashSet<&str> = live_parts
.iter()
.map(|p| p.mountpoint.as_str())
.filter(|m| !m.is_empty())
.collect();
let assigned = live_parts.iter().filter(|p| !p.mountpoint.is_empty()).count();
if mountpoints.len() != assigned {
return Some(t!("mp_duplicate_mountpoint").to_string());
}
@@ -426,10 +437,26 @@ fn validate_manual_on(
}
}
// Mountpoint boş olmamalı — TEK istisna LVM PV'dir (üzerinde
// mantıksal hacimler yaşar, kendisi bağlanmaz).
// Mountpoint boş olmamalı — ama YALNIZCA KULLANICI TANIMLARI için.
//
// REGRESYON: bu kontrol tüm tanımları kapsıyordu. Taranmış fiziksel
// bir bölümün mountpoint'i canlı ISO'da mount edilmediği için BOŞTUR
// (lsblk MOUNTPOINT sütunu). Çift önyüklemeli bir makinede Windows'un
// sda1'i her zaman bu durumdadır ve kullanıcı ona hiç dokunmamıştı —
// yine de düzen reddediliyordu:
//
// "sda: sda1 bozuk değil, sadece mount değil" → hata ✗
//
// Boş mountpointli bir FİZİKSEL bölüm "henüz atanmadı" demektir ve
// iş zincirinde zaten güvenli: mount edilmez (jobs/partition.rs:1111),
// fstab'a girmez (:1171) ve `skip_format: true` geldiği için
// formatlanmaz da. Yani doğrulama bir şey engellemiyor, sadece
// kullanıcıyı yanlış yönlendiriyordu.
//
// Zorunluluk yeni tanımlarda (orig_device boş) ve LVM PV dışında
// geçerlidir: kullanıcı "ne işe yarayacak" diye sormadan kaydedemez.
for p in &live_parts {
if p.mountpoint.is_empty() && p.fstype != FsType::Lvm {
if p.orig_device.is_empty() && p.mountpoint.is_empty() && p.fstype != FsType::Lvm {
return Some(t!("mp_error_empty_mountpoint").to_string());
}
}
@@ -441,6 +468,41 @@ fn validate_manual_on(
// Çoklu disk yardımcıları
// ─────────────────────────────────────────────
/// "Yeni Bölüm" diyaloğu için varsayılan bağlama noktası önerisi.
///
/// `/`, `/boot/efi` ve `swap` bir kurulumda **TEKİLDİR** — birdir. Bu
/// yüzden sayım aktif disk DEĞİL, TÜM düzen üzerinden yapılır.
///
/// REGRESYON: sayım aktif diske bakıyordu. Kullanıcı boş bir ikinci
/// diske geçip "Yeni Bölüm" dediğinde o diskte hiçbir tanım olmadığı
/// için öneri `/boot/efi` çıkıyordu — sdb1'de zaten var olan bir
/// mountpoint'i ikinci kez öneriyordu. Kullanıcı kaydetmeye
/// çalışınca "mükerrer bağlama noktası" hatası alıyor, alanı
/// temizleyince de "boş olamaz" hatası. İkisi de kullanıcıya hiçbir
/// şey söylemiyordu.
///
/// `/home` adımı ikincil (veri) diskler içindir: kök, EFI ve swap
/// başka yerdeyse sıra ona gelir, böylece boş bir diske bölüm
/// eklemek için alanı elle doldurmak zorunda kalmazsınız.
fn suggest_mountpoint(parts: &[CustomPartition], is_uefi: bool) -> &'static str {
let live: Vec<&CustomPartition> = parts.iter().filter(|p| !p.delete).collect();
let has_mp = |mp: &str| live.iter().any(|p| p.mountpoint == mp);
let has_fs = |fs: FsType| live.iter().any(|p| p.fstype == fs);
if is_uefi && !has_mp("/boot/efi") && !has_fs(FsType::Fat32) {
"/boot/efi"
} else if !has_mp("/") {
"/"
} else if !has_fs(FsType::Swap) {
"swap"
} else if !has_mp("/home") {
"/home"
} else {
// Her şey atanmış — kullanıcı elle seçer.
""
}
}
/// Verilen diskin toplam boyutunu MB cinsinden döndürür.
/// Disk listede yoksa 0 döner.
fn disk_size_mb(state: &GlobalState, disk: &str) -> u64 {
@@ -1453,49 +1515,24 @@ impl PartitionStep {
self.manual.adding = true;
self.manual.editing = false;
self.manual.edit_idx = None;
self.manual.add_encrypt = false;
self.manual.add_luks_password.clear();
// Yeni tanım SEÇİLİ diske eklenir. Diyalogda ayrı bir
// hedef disk seçicisi yok; hedefi değiştirmek için
// disk şeridinden önce o diske geçilir.
let active_disk = state.selected_disk.clone().unwrap_or_default();
// Öneriler bu diskin DURUMUNA göre yapılır:
// - UEFI'de ve henüz EFI yoksa → /boot/efi
// - kök yoksa → /
// - kök varsa ve swap yoksa → swap
// Böylece kullanıcı sdb'ye geçtiğinde öneri de sdb'ye göre
// değişir; daha önce her zaman aktif diskin sayımı
// kullanılıyordu ve yeni diske geçince "/boot/efi" gibi
// alakasız bir öneri çıkıyordu.
let defs: Vec<&CustomPartition> = state
.custom_partitions
.iter()
.filter(|p| p.disk == active_disk && !p.delete)
.collect();
let has_root = defs.iter().any(|p| p.mountpoint == "/");
let has_efi = defs.iter().any(|p| {
p.mountpoint == "/boot/efi" || p.fstype == FsType::Fat32
});
let has_swap = defs.iter().any(|p| p.fstype == FsType::Swap);
self.manual.add_mountpoint.clear();
self.manual.add_encrypt = false;
self.manual.add_luks_password.clear();
self.manual.add_mountpoint = if self.is_uefi && !has_efi {
"/boot/efi".to_string()
} else if !has_root {
"/".to_string()
} else if !has_swap {
"swap".to_string()
} else {
String::new()
};
let suggested = suggest_mountpoint(&state.custom_partitions, self.is_uefi);
self.manual.add_mountpoint = suggested.to_string();
// Mountpoint'e göre otomatik boyut ata
self.manual.add_size_str = match self.manual.add_mountpoint.as_str() {
self.manual.add_size_str = match suggested {
"/boot/efi" => "1024".to_string(), // 1GB
"swap" => "1024".to_string(), // Sabit 1GB
"/" => "remaining".to_string(), // Geri kalan alan
"swap" => "1024".to_string(), // Sabit 1GB
// "/" ve "/home" → diskin kalanının tamamı. Veri diski
// olan ikinci bir diskte de bu çoğu zaman istenendir.
"/" | "/home" => "remaining".to_string(),
_ => String::new(),
};
self.manual.add_fstype = match self.manual.add_mountpoint.as_str() {
self.manual.add_fstype = match suggested {
"swap" => FS_OPTIONS.iter().position(|(_, t)| *t == FsType::Swap).unwrap_or(0),
"/boot/efi" => FS_OPTIONS.iter().position(|(_, t)| *t == FsType::Fat32).unwrap_or(0),
_ => 0,
@@ -2997,14 +3034,143 @@ mod multidisk_tests {
validate_manual_on(&with_pv, &[], true, &disks)
);
// Normal bölüm: mountpoint yok → geçersiz
// KULLANICI TANIMI: mountpoint yok → geçersiz.
//
// `orig_device` boş olan bir tanım kullanıcının kendi eklediği
// yeni bölümdür; ne işe yarayacağını söylemeden kaydedilmesine
// izin verilmez. (Fiziksel taranmış bölümlerde mountpoint'in
// boş olması normaldir — aşağıdaki regression test'ine bakın.)
let mut new_def = p("/dev/sda", "/dev/sda2", 5_000, FsType::Ext4, "");
new_def.orig_device = String::new();
let no_mp = vec![
p("/dev/sda", "/dev/sda1", 20_000, FsType::Ext4, "/"),
p("/dev/sda", "/dev/sda2", 5_000, FsType::Ext4, ""),
new_def,
];
assert!(
validate_manual_on(&no_mp, &[], true, &disks).is_some(),
"mountpointsiz normal bölüm reddedilmeli"
"mountpointsiz kullanıcı tanımı reddedilmeli"
);
}
// ── REGRESYON: mount EDİLMEMİŞ fiziksel bölüm ──────────────
/// Canlı ISO'da mount edilmemiş fiziksel bölüm (mountpoint boş)
/// düzeni GEÇERSİZ kılmamalı.
///
/// REGRESYON: `read_existing_partitions` mountpoint'i lsblk'in
/// MOUNTPOINT sütunundan okur; mount edilmemiş bölümlerde bu sütun
/// boştur. Çift önyüklemeli bir makinede Windows'un boot bölümü
/// neredeyse her zaman bu durumdadır ve kullanıcı ona dokunmamıştır.
/// Yine de düzen "bağlama noktası boş olamaz" ile reddediliyordu —
/// hata, ekrandaki 5 bölümün hangisinden geldiğini söylemeden.
#[test]
fn unmounted_physical_partition_does_not_invalidate_layout() {
let disks = vec![disk("/dev/sda", 250), disk("/dev/sdb", 500)];
// sda1 = Windows boot (FAT32, mount edilmemiş) → mountpoint BOŞ
let parts = vec![
p("/dev/sda", "/dev/sda1", 512, FsType::Fat32, ""),
p("/dev/sdb", "/dev/sdb1", 1_024, FsType::Fat32, "/boot/efi"),
p("/dev/sdb", "/dev/sdb2", 8_192, FsType::Swap, "swap"),
p("/dev/sdb", "/dev/sdb3", 400_000, FsType::Ext4, "/"),
];
assert_eq!(
validate_manual_on(&parts, &[], true, &disks),
None,
"mount edilmemiş sda1 düzeni bozmamalı: {:?}",
validate_manual_on(&parts, &[], true, &disks)
);
}
/// Birden çok atanmamış fiziksel bölüm "mükerrer bağlama noktası"
/// hatası üretmemeli.
///
/// REGRESYON: mükerrer kontrolü boş mountpoint'leri de karşılaştırıyordu.
/// İki atanmamış bölüm aynı "" değerini taşıdığı için kullanıcı var
/// olmayan bir çakışma hatası görüyordu — asıl sorun (mountpoint
/// eksikliği) maskeleniyordu.
#[test]
fn several_unassigned_partitions_are_not_duplicate_mountpoints() {
let disks = vec![disk("/dev/sda", 250)];
let parts = vec![
p("/dev/sda", "/dev/sda1", 512, FsType::Ntfs, ""),
p("/dev/sda", "/dev/sda2", 8_000, FsType::Ntfs, ""),
p("/dev/sda", "/dev/sda3", 200_000, FsType::Ext4, "/"),
];
assert_eq!(
validate_manual_on(&parts, &[], false, &disks),
None,
"iki atanmamış bölüm mükerrer sayılmamalı: {:?}",
validate_manual_on(&parts, &[], false, &disks)
);
}
// ── REGRESYON: ikinci diske öneri ──────────────────────────
/// Kullanıcının gerçek senaryosu: sdb'de kurulum tamam (EFI+swap+kök),
/// sdc tamamen boş. sdc'de "Yeni Bölüm" → öneri `/boot/efi` OLMAZ
/// (sdb1'de zaten var) ve BOŞ DA OLMAZ (kullanıcı ne yapacağını
/// bilemez). `/home` önerilir.
#[test]
fn empty_secondary_disk_suggests_home_not_a_duplicate_efi() {
let parts = vec![
p("/dev/sdb", "/dev/sdb1", 1_024, FsType::Fat32, "/boot/efi"),
p("/dev/sdb", "/dev/sdb2", 8_192, FsType::Swap, "swap"),
p("/dev/sdb", "/dev/sdb3", 400_000, FsType::Ext4, "/"),
];
assert_eq!(
suggest_mountpoint(&parts, true),
"/home",
"boş ikinci diskte /home önerilmeli, mükerrer /boot/efi değil"
);
}
/// Öneri TÜM düzeni görmeli — aktif disk değil.
///
/// REGRESYON: sayım aktif diske bakıyordu; boş bir diske geçince
/// o diskte "EFI yok" sanıp `/boot/efi` öneriyordu.
#[test]
fn suggestion_counts_whole_layout_not_the_active_disk() {
// Aktif disk sdc (hiç tanım yok), ama kurulum sdb'de tamam.
let sdb: Vec<CustomPartition> = vec![
p("/dev/sdb", "/dev/sdb1", 1_024, FsType::Fat32, "/boot/efi"),
p("/dev/sdb", "/dev/sdb2", 8_192, FsType::Swap, "swap"),
p("/dev/sdb", "/dev/sdb3", 400_000, FsType::Ext4, "/"),
];
let suggested = suggest_mountpoint(&sdb, true);
assert_ne!(
suggested, "/boot/efi",
"başka diskte EFI varken ikinci bir EFI önerilmemeli"
);
assert!(!suggested.is_empty(), "öneri boş olmamalı, kullanıcı yönlendirilmeli");
}
/// Sıralama korunur: kurulum tamamen boşsa kök önerilir.
#[test]
fn suggestion_order_root_before_home() {
assert_eq!(suggest_mountpoint(&[], false), "/");
assert_eq!(suggest_mountpoint(&[], true), "/boot/efi");
let mut root = p("/dev/sda", "/dev/sda1", 100_000, FsType::Ext4, "/");
root.fstype = FsType::Ext4;
assert_eq!(suggest_mountpoint(&[root], false), "swap");
}
/// Silinmek işaretli bölümler sayılmamalı — kullanıcı /home'u
/// silip yeniden tanımlamak istiyor, öneri yine `/home` olmalı.
#[test]
fn deleted_definitions_do_not_count_as_taken() {
let mut dead = p("/dev/sdb", "/dev/sdb3", 400_000, FsType::Ext4, "/home");
dead.delete = true;
let parts = vec![
p("/dev/sdb", "/dev/sdb1", 1_024, FsType::Fat32, "/boot/efi"),
p("/dev/sdb", "/dev/sdb2", 8_192, FsType::Swap, "swap"),
p("/dev/sdb", "/dev/sdb3", 400_000, FsType::Ext4, "/"),
dead,
];
assert_eq!(
suggest_mountpoint(&parts, true),
"/home",
"silinen /home yeniden kullanılabilir olmalı"
);
}