Coverage Report

Created: 2026-07-31 00:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/feoxdb/feoxdb/src/storage/free_space.rs
Line
Count
Source
1
use crate::constants::*;
2
use crate::error::{FeoxError, Result};
3
use std::collections::BTreeMap;
4
5
/// Represents a contiguous range of free sectors on disk
6
#[derive(Debug, Clone, PartialEq, Eq)]
7
pub struct FreeSpace {
8
    pub start: u64, // Starting sector
9
    pub size: u64,  // Size in sectors
10
}
11
12
/// Free space manager using dual RB-trees for efficient allocation and coalescing
13
/// Matches the kernel implementation's design for correctness
14
pub struct FreeSpaceManager {
15
    /// Tree sorted by (size, start) for best-fit allocation
16
    /// Using composite key ensures uniqueness
17
    by_size: BTreeMap<(u64, u64), FreeSpace>,
18
19
    /// Tree sorted by start address for efficient merging
20
    by_start: BTreeMap<u64, FreeSpace>,
21
22
    /// Total free space in bytes
23
    total_free: u64,
24
25
    /// Device size in bytes (for validation)
26
    device_size: u64,
27
28
    /// Fragmentation percentage (0-100)
29
    fragmentation_percent: u32,
30
}
31
32
impl FreeSpaceManager {
33
    /// Create a new free space manager
34
285
    pub fn new() -> Self {
35
285
        Self {
36
285
            by_size: BTreeMap::new(),
37
285
            by_start: BTreeMap::new(),
38
285
            total_free: 0,
39
285
            device_size: 0,
40
285
            fragmentation_percent: 0,
41
285
        }
42
285
    }
43
44
    /// Initialize with device size and initial free space
45
73
    pub fn initialize(&mut self, device_size: u64) -> Result<()> {
46
73
        self.device_size = device_size;
47
48
73
        let metadata_sectors = FEOX_DATA_START_BLOCK;
49
73
        let total_sectors = device_size / FEOX_BLOCK_SIZE as u64;
50
51
73
        if total_sectors <= metadata_sectors {
52
0
            return Err(FeoxError::InvalidDevice);
53
73
        }
54
55
        // Add all space after metadata as free
56
73
        let free_sectors = total_sectors - metadata_sectors;
57
73
        self.insert_free_space(FreeSpace {
58
73
            start: metadata_sectors,
59
73
            size: free_sectors,
60
73
        })
?0
;
61
62
73
        Ok(())
63
73
    }
64
65
    /// Set device size for bounds checking (used when rebuilding from scan)
66
108
    pub fn set_device_size(&mut self, device_size: u64) {
67
108
        self.device_size = device_size;
68
108
    }
69
70
    /// Allocate sectors using best-fit algorithm
71
21.6k
    pub fn allocate_sectors(&mut self, sectors_needed: u64) -> Result<u64> {
72
21.6k
        if sectors_needed == 0 {
73
1
            return Err(FeoxError::InvalidArgument);
74
21.6k
        }
75
76
21.6k
        let best_fit = self
77
21.6k
            .by_size
78
21.6k
            .range((sectors_needed, 0)..)
79
21.6k
            .next()
80
21.6k
            .map(|((size, start), space)| (
*size21.6k
,
*start21.6k
,
space21.6k
.
clone21.6k
()));
81
82
21.6k
        if let Some((
size21.6k
,
start21.6k
,
space21.6k
)) = best_fit {
83
            // Validate the space
84
21.6k
            if !self.is_valid_free_space(&space) {
85
0
                return Err(FeoxError::CorruptedData);
86
21.6k
            }
87
88
            // Remove from both trees
89
21.6k
            self.by_size.remove(&(size, start));
90
21.6k
            self.by_start.remove(&space.start);
91
21.6k
            self.total_free -= space.size * FEOX_BLOCK_SIZE as u64;
92
93
21.6k
            let allocated_start = space.start;
94
95
            // Handle remaining space if any
96
21.6k
            if space.size > sectors_needed {
97
21.6k
                let remaining = FreeSpace {
98
21.6k
                    start: space.start + sectors_needed,
99
21.6k
                    size: space.size - sectors_needed,
100
21.6k
                };
101
102
                // Insert remaining space back
103
21.6k
                if let Err(
e0
) = self.insert_free_space(remaining) {
104
                    // Try to restore original space on error
105
0
                    let _ = self.insert_free_space(space);
106
0
                    return Err(e);
107
21.6k
                }
108
23
            }
109
110
21.6k
            self.update_fragmentation();
111
112
21.6k
            Ok(allocated_start)
113
        } else {
114
9
            Err(FeoxError::OutOfSpace)
115
        }
116
21.6k
    }
117
118
    /// Release sectors back to free space pool with coalescing
119
596
    pub fn release_sectors(&mut self, start: u64, count: u64) -> Result<()> {
120
596
        if start < FEOX_DATA_START_BLOCK || 
count == 0579
{
121
18
            return Err(FeoxError::InvalidArgument);
122
578
        }
123
124
        // Validate bounds
125
578
        if !self.is_valid_sector_range(start, count) {
126
2
            return Err(FeoxError::InvalidArgument);
127
576
        }
128
129
        // Try to merge with adjacent spaces
130
576
        let 
merged573
= self.try_merge_spaces(start, count)
?3
;
131
132
        // Insert the merged space (this will update total_free)
133
573
        self.insert_free_space(merged)
?0
;
134
135
573
        self.update_fragmentation();
136
137
573
        Ok(())
138
596
    }
139
140
    /// Try to merge with adjacent free spaces
141
576
    fn try_merge_spaces(&mut self, start: u64, size: u64) -> Result<FreeSpace> {
142
576
        let end = start.checked_add(size).ok_or(FeoxError::InvalidArgument)
?0
;
143
144
        // Overlap is rejected from these two probes, before either neighbour is
145
        // removed or debited, so a rejected release leaves the free list untouched.
146
        // Checking after the merge would already have destroyed the neighbours.
147
576
        let preceding = self
148
576
            .by_start
149
576
            .range(..start)
150
576
            .next_back()
151
576
            .map(|(_, space)| 
space334
.
clone334
());
152
576
        if let Some(
ref space334
) = preceding {
153
334
            if space.start + space.size > start {
154
0
                return Err(FeoxError::DuplicateKey);
155
334
            }
156
242
        }
157
158
576
        let following = self
159
576
            .by_start
160
576
            .range(start..=end)
161
576
            .next()
162
576
            .map(|(_, space)| 
space125
.
clone125
());
163
576
        if let Some(
ref space125
) = following {
164
125
            if space.start < end {
165
3
                return Err(FeoxError::DuplicateKey);
166
122
            }
167
451
        }
168
169
573
        let mut merged_start = start;
170
573
        let mut merged_size = size;
171
172
573
        let prev = preceding.filter(|space| 
space.start + space.size334
==
start334
);
173
573
        let next = following.filter(|space| 
space.start122
==
end122
);
174
175
        // Merge with predecessor if found
176
573
        if let Some(
prev_space308
) = prev {
177
308
            // Remove from both trees
178
308
            self.by_size.remove(&(prev_space.size, prev_space.start));
179
308
            self.by_start.remove(&prev_space.start);
180
308
181
308
            // Subtract the removed space from total_free (will be re-added when inserting merged)
182
308
            self.total_free -= prev_space.size * FEOX_BLOCK_SIZE as u64;
183
308
184
308
            merged_start = prev_space.start;
185
308
            merged_size += prev_space.size;
186
308
        
}265
187
188
        // Merge with successor if found
189
573
        if let Some(
next_space122
) = next {
190
122
            // Remove from both trees
191
122
            self.by_size.remove(&(next_space.size, next_space.start));
192
122
            self.by_start.remove(&next_space.start);
193
122
194
122
            // Subtract the removed space from total_free (will be re-added when inserting merged)
195
122
            self.total_free -= next_space.size * FEOX_BLOCK_SIZE as u64;
196
122
197
122
            merged_size += next_space.size;
198
451
        }
199
200
573
        Ok(FreeSpace {
201
573
            start: merged_start,
202
573
            size: merged_size,
203
573
        })
204
576
    }
205
206
    /// Insert a free space into both trees
207
22.2k
    fn insert_free_space(&mut self, space: FreeSpace) -> Result<()> {
208
22.2k
        if space.size == 0 {
209
0
            return Err(FeoxError::InvalidArgument);
210
22.2k
        }
211
212
        // Validate the space
213
22.2k
        if !self.is_valid_free_space(&space) {
214
0
            return Err(FeoxError::InvalidArgument);
215
22.2k
        }
216
217
        // Check for duplicates
218
22.2k
        if self.by_start.contains_key(&space.start) {
219
0
            return Err(FeoxError::DuplicateKey);
220
22.2k
        }
221
222
        // Update total free space
223
22.2k
        self.total_free += space.size * FEOX_BLOCK_SIZE as u64;
224
225
        // Insert into both trees
226
22.2k
        self.by_size
227
22.2k
            .insert((space.size, space.start), space.clone());
228
22.2k
        self.by_start.insert(space.start, space);
229
230
22.2k
        Ok(())
231
22.2k
    }
232
233
    /// Check if a free space is valid
234
43.9k
    fn is_valid_free_space(&self, space: &FreeSpace) -> bool {
235
43.9k
        if space.start < FEOX_DATA_START_BLOCK {
236
0
            return false;
237
43.9k
        }
238
239
        // Check device bounds
240
43.9k
        if self.device_size > 0 {
241
43.9k
            let device_sectors = self.device_size / FEOX_BLOCK_SIZE as u64;
242
43.9k
            if space.start >= device_sectors {
243
0
                return false;
244
43.9k
            }
245
43.9k
            if space.start.checked_add(space.size).is_none() {
246
0
                return false;
247
43.9k
            }
248
43.9k
            if space.start + space.size > device_sectors {
249
0
                return false;
250
43.9k
            }
251
0
        }
252
253
43.9k
        true
254
43.9k
    }
255
256
    /// Check if a sector range is valid
257
578
    fn is_valid_sector_range(&self, start: u64, count: u64) -> bool {
258
578
        if start < FEOX_DATA_START_BLOCK || count == 0 {
259
0
            return false;
260
578
        }
261
262
578
        if self.device_size > 0 {
263
578
            let device_sectors = self.device_size / FEOX_BLOCK_SIZE as u64;
264
578
            if start >= device_sectors {
265
1
                return false;
266
577
            }
267
577
            match start.checked_add(count) {
268
577
                Some(
end576
) if end <= device_sector
s576
=>
{}576
269
1
                _ => return false,
270
            }
271
0
        }
272
273
576
        true
274
578
    }
275
276
    /// Update fragmentation metric
277
22.2k
    fn update_fragmentation(&mut self) {
278
22.2k
        if self.total_free == 0 {
279
9
            self.fragmentation_percent = 0;
280
9
            return;
281
22.2k
        }
282
283
22.2k
        let num_chunks = self.by_start.len() as u64;
284
22.2k
        if num_chunks <= 1 {
285
22.1k
            self.fragmentation_percent = 0;
286
22.1k
            return;
287
59
        }
288
289
        // Find largest free chunk
290
59
        let largest = self
291
59
            .by_size
292
59
            .iter()
293
59
            .next_back()
294
59
            .map(|(_, space)| space.size * FEOX_BLOCK_SIZE as u64)
295
59
            .unwrap_or(0);
296
297
        // Calculate fragmentation as percentage of free space not in largest chunk
298
59
        if self.total_free > 0 {
299
59
            let fragmented = self.total_free - largest;
300
59
            self.fragmentation_percent = ((fragmented * 100) / self.total_free) as u32;
301
59
        
}0
302
22.2k
    }
303
304
    /// Get total free space in bytes
305
21
    pub fn get_total_free(&self) -> u64 {
306
21
        self.total_free
307
21
    }
308
309
    /// Get fragmentation percentage
310
208
    pub fn get_fragmentation(&self) -> u32 {
311
208
        self.fragmentation_percent
312
208
    }
313
314
    /// Get number of free chunks
315
7
    pub fn get_free_chunks_count(&self) -> usize {
316
7
        self.by_start.len()
317
7
    }
318
319
    /// Get largest free chunk in bytes
320
1
    pub fn get_largest_free_chunk(&self) -> u64 {
321
1
        self.by_size
322
1
            .iter()
323
1
            .next_back()
324
1
            .map(|(_, space)| space.size * FEOX_BLOCK_SIZE as u64)
325
1
            .unwrap_or(0)
326
1
    }
327
}
328
329
impl Default for FreeSpaceManager {
330
0
    fn default() -> Self {
331
0
        Self::new()
332
0
    }
333
}