Skip to main content

aria2_core/filesystem/
file_allocation.rs

1use super::disk_adaptor::{DirectDiskAdaptor, DiskAdaptor};
2use crate::error::{Aria2Error, FatalError, Result};
3use crate::filesystem::disk_space::check_disk_space;
4use std::path::Path;
5
6/// One-time warning emitted when `fallocate`-style allocation succeeds but
7/// the allocated blocks are NOT zero-filled by the platform (macOS
8/// `F_PREALLOCATE`, Windows `SetFileValidData`) and the caller did not opt
9/// into `secure-falloc`. In that case residual disk data may be exposed
10/// until the download overwrites those blocks. `std::sync::Once` ensures the
11/// warning is logged only once per process to avoid log spam.
12#[cfg_attr(target_os = "linux", allow(dead_code))]
13static SECURE_FALLOC_WARN_ONCE: std::sync::Once = std::sync::Once::new();
14
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub enum AllocationStrategy {
17    #[default]
18    None,
19    Prealloc,
20    Falloc,
21    Trunc,
22    /// Memory-mapped I/O: pre-allocate blocks via `fallocate`, then the writer
23    /// uses `MmapDiskWriter` for direct memory access. The allocation step is
24    /// identical to `Falloc`; the difference is in the writer construction
25    /// (handled by `DownloadCommand` based on the `file_allocation` option).
26    Mmap,
27}
28
29impl AllocationStrategy {
30    /// Parse allocation strategy from string
31    /// This is intentionally not implementing FromStr to avoid confusion with the standard trait
32    #[allow(clippy::should_implement_trait)]
33    pub fn from_str(s: &str) -> Self {
34        match s {
35            "prealloc" => AllocationStrategy::Prealloc,
36            "falloc" => AllocationStrategy::Falloc,
37            "trunc" => AllocationStrategy::Trunc,
38            "mmap" => AllocationStrategy::Mmap,
39            _ => AllocationStrategy::None,
40        }
41    }
42}
43
44/// Allocate file space using the specified strategy.
45/// This function provides cross-platform support for file preallocation:
46/// - On Unix: Uses posix_fallocate64 when available (for Prealloc/Falloc), falls back to set_len
47/// - On Windows: Uses SetEndOfFile via set_len() (works for all strategies)
48/// - On macOS: Uses set_len() as fallback (macOS doesn't have fallocate)
49///
50/// # Arguments
51/// * `adaptor` - Disk adaptor for file operations
52/// * `path` - Path to the file (used for error messages)
53/// * `length` - Desired file length in bytes
54/// * `strategy` - Allocation strategy to use
55/// * `secure` - When `true`, zero-fill allocated blocks on platforms that
56///   don't zero-fill (macOS, Windows). No-op on Linux where `fallocate(2)`
57///   always returns zeroed blocks.
58pub async fn allocate_file<D: DiskAdaptor>(
59    adaptor: &mut D,
60    _path: &Path,
61    length: u64,
62    strategy: AllocationStrategy,
63    secure: bool,
64) -> Result<()> {
65    match strategy {
66        AllocationStrategy::None => Ok(()),
67        AllocationStrategy::Prealloc => preallocate(adaptor, length).await,
68        AllocationStrategy::Falloc => fallocate(adaptor, length, secure).await,
69        AllocationStrategy::Trunc => truncate(adaptor, length).await,
70        // Mmap uses fallocate to ensure blocks are allocated before mapping;
71        // the actual mmap is performed by MmapDiskWriter at open time.
72        AllocationStrategy::Mmap => fallocate(adaptor, length, secure).await,
73    }
74}
75
76pub async fn preallocate_file(
77    path: &Path,
78    length: u64,
79    strategy: &str,
80    secure: bool,
81) -> Result<()> {
82    preallocate_file_with_progress(path, length, strategy, None::<&fn(u64, u64)>, secure).await
83}
84
85/// Preallocate file with optional progress callback for large allocations.
86///
87/// The callback `on_progress` is invoked at 10% intervals during allocation
88/// for files larger than 100MB, receiving `(bytes_allocated, total_bytes)`.
89pub async fn preallocate_file_with_progress<F>(
90    path: &Path,
91    length: u64,
92    strategy: &str,
93    on_progress: Option<&F>,
94    secure: bool,
95) -> Result<()>
96where
97    F: Fn(u64, u64) + Send + Sync,
98{
99    let alloc_strategy = AllocationStrategy::from_str(strategy);
100
101    if length == 0 || alloc_strategy == AllocationStrategy::None {
102        return Ok(());
103    }
104
105    // K5.3: Pre-allocation disk space check
106    // Verify sufficient disk space before attempting allocation to prevent
107    // failures mid-download due to exhausted storage. The check includes
108    // a 10% headroom margin for filesystem overhead.
109    if let Err(_e) = check_disk_space(path, length) {
110        return Err(Aria2Error::Fatal(FatalError::DiskSpaceExhausted));
111    }
112
113    if let Some(parent) = path.parent() {
114        let parent: &Path = parent;
115        if !parent.exists() {
116            tokio::fs::create_dir_all(parent)
117                .await
118                .map_err(|e: std::io::Error| Aria2Error::Io(e.to_string()))?;
119        }
120    }
121
122    const PROGRESS_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB
123
124    if let Some(cb) = on_progress
125        && length >= PROGRESS_THRESHOLD
126    {
127        cb(0, length);
128    }
129
130    let mut adaptor = DirectDiskAdaptor::new();
131    adaptor.open(path).await?;
132    allocate_file(&mut adaptor, path, length, alloc_strategy, secure).await?;
133
134    if let Some(cb) = on_progress
135        && length >= PROGRESS_THRESHOLD
136    {
137        cb(length, length);
138    }
139
140    adaptor.close().await
141}
142
143/// Preallocate file space using truncation (set_len).
144/// This is the simplest allocation method that works on all platforms:
145/// - Unix: Uses ftruncate via set_len
146/// - Windows: Uses SetEndOfFile via set_len
147/// - macOS: Uses set_len
148///
149/// Note: This method does not guarantee contiguous disk space allocation,
150/// but it ensures the file has the specified size.
151async fn preallocate<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
152    adaptor.truncate(length).await
153}
154
155/// Zero-fill a file region in async 1 MiB chunks.
156///
157/// This is used as a fallback when `fallocate(2)` returns `EOPNOTSUPP`
158/// (Linux) or as a security measure after `SetFileValidData`/`F_PREALLOCATE`
159/// (Windows/macOS) which don't zero-fill the allocated blocks.
160///
161/// Uses `tokio::task::yield_now()` between chunks to avoid blocking the
162/// reactor. The zero buffer is allocated once and reused.
163async fn async_zero_fill<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
164    const CHUNK_SIZE: usize = 1024 * 1024; // 1 MiB
165    let zero_chunk = vec![0u8; CHUNK_SIZE];
166    let mut remaining = length;
167    let mut offset: u64 = 0;
168
169    while remaining > 0 {
170        let write_len = remaining.min(CHUNK_SIZE as u64) as usize;
171        adaptor.write(offset, &zero_chunk[..write_len]).await?;
172        offset += write_len as u64;
173        remaining -= write_len as u64;
174
175        // Cooperative yield to avoid starving other tasks
176        tokio::task::yield_now().await;
177    }
178
179    Ok(())
180}
181
182/// Allocate file space using platform-native preallocation syscalls.
183/// This method attempts true disk-space allocation (avoiding sparse files)
184/// when the platform and filesystem support it, with graceful fallbacks.
185///
186/// Platform-specific behavior:
187/// - **Linux**: Uses raw `fallocate(2)` (NOT `posix_fallocate64`) so that
188///   `EOPNOTSUPP` from the filesystem can be detected explicitly. When the
189///   filesystem doesn't support `fallocate(2)`, we fall back to
190///   `async_zero_fill` (cooperative zero-fill) instead of letting
191///   `posix_fallocate64` block the async runtime with its internal
192///   zero-fill loop. `fallocate(2)` always returns zeroed blocks on success,
193///   so `secure` has no effect on Linux. Falls back to `set_len` if no raw
194///   file descriptor is available.
195/// - **macOS**: Uses `fcntl(F_PREALLOCATE)` with `F_ALLOCATEALL` for true space
196///   allocation. `F_PREALLOCATE` does not zero-fill the allocated blocks, so
197///   when `secure == true` we additionally run `async_zero_fill`. When
198///   `secure == false`, a one-time warning is emitted (residual disk data
199///   may be exposed). `F_PREALLOCATE` does not extend the file size, so the
200///   file is sized first via `ftruncate` (set_len). Falls back to the sparse
201///   `set_len` result if the raw fd is unavailable.
202/// - **Windows**: Attempts `SetFileValidData` to extend the valid data length
203///   and force allocation. `SetFileValidData` does NOT zero-fill, so when
204///   `secure == true` we additionally run `async_zero_fill`. When
205///   `secure == false`, a one-time warning is emitted. Requires
206///   `SE_MANAGE_VOLUME_PRIVILEGE`; if the privilege is not held (or the call
207///   fails for any other reason), it falls back to the sparse file produced
208///   by `set_len` (SetEndOfFile).
209/// - **Other Unix (BSD, etc.)**: No portable preallocate syscall; uses `set_len`.
210#[cfg_attr(target_os = "linux", allow(unused_variables))]
211async fn fallocate<D: DiskAdaptor>(adaptor: &mut D, length: u64, secure: bool) -> Result<()> {
212    #[cfg(target_os = "linux")]
213    {
214        if let Some(fd) = adaptor.unix_raw_fd() {
215            // Use raw fallocate(2) to detect EOPNOTSUPP explicitly.
216            // posix_fallocate64 would silently fall back to a blocking
217            // zero-fill loop inside libc when the kernel returns EOPNOTSUPP,
218            // which would stall the async runtime. By calling fallocate(2)
219            // directly we can fall back to our own cooperative async_zero_fill.
220            //
221            // FALLOC_FL_NONE (0) requests default behavior: allocate space
222            // and zero-fill it at the filesystem block level.
223            let ret = unsafe {
224                libc::fallocate(
225                    fd,
226                    // FALLOC_FL_NONE is 0 in the Linux kernel headers but is
227                    // not exported by the libc crate. Literal 0 == default
228                    // allocate-and-zero-fill behavior.
229                    0 as libc::c_int,
230                    0,
231                    length as libc::off_t,
232                )
233            };
234            if ret == 0 {
235                // Success: kernel allocates zeroed blocks; secure is a no-op.
236                return Ok(());
237            }
238            let errno = unsafe { *libc::__errno_location() };
239            if errno == libc::EOPNOTSUPP {
240                tracing::warn!(
241                    length,
242                    "fallocate(2) not supported by filesystem; \
243                     falling back to async zero-fill"
244                );
245                // Size the file first so writes land at correct offsets.
246                adaptor.truncate(length).await?;
247                return async_zero_fill(adaptor, length).await;
248            }
249            // Other errors: return as I/O error
250            Err(Aria2Error::Io(
251                std::io::Error::from_raw_os_error(errno).to_string(),
252            ))
253        } else {
254            // Fall back to set_len if no raw fd available
255            adaptor.truncate(length).await
256        }
257    }
258
259    #[cfg(all(unix, not(target_os = "linux")))]
260    {
261        // macOS-only: untested on this Windows dev machine, validated via
262        // type-checking only. `libc::fstore_t` and the F_* constants are
263        // exposed by the libc crate on macOS targets.
264        #[cfg(target_os = "macos")]
265        {
266            match adaptor.unix_raw_fd() {
267                Some(fd) => {
268                    // F_PREALLOCATE does not change the file size; size it first
269                    // via ftruncate so the file is correct even if preallocation
270                    // is rejected by the filesystem.
271                    adaptor.truncate(length).await?;
272                    // F_ALLOCATEALL allocates all requested space;
273                    // F_PEOFPOSMODE measures offset from physical end of file.
274                    let fstore = libc::fstore_t {
275                        fst_flags: libc::F_ALLOCATEALL as libc::c_uint,
276                        fst_posmode: libc::F_PEOFPOSMODE,
277                        fst_offset: 0,
278                        fst_length: length as libc::off_t,
279                        fst_bytesalloc: 0,
280                    };
281                    let ret = unsafe { libc::fcntl(fd, libc::F_PREALLOCATE, &fstore) };
282                    if ret != 0 {
283                        // Filesystem may not support F_PREALLOCATE; the file is
284                        // already sized above, so it remains sparse but correct.
285                        tracing::warn!(
286                            length,
287                            "F_PREALLOCATE failed on macOS, file remains sparse"
288                        );
289                        return Ok(());
290                    }
291                    // F_PREALLOCATE succeeded but does NOT zero-fill the
292                    // allocated blocks. Zero-fill when secure is requested,
293                    // otherwise emit a one-time warning about the trade-off.
294                    if secure {
295                        async_zero_fill(adaptor, length).await
296                    } else {
297                        SECURE_FALLOC_WARN_ONCE.call_once(|| {
298                            tracing::warn!(
299                                "F_PREALLOCATE succeeded but does not zero-fill; \
300                                 residual disk data may be exposed. \
301                                 Enable --secure-falloc=true to zero-fill (at a \
302                                 performance cost). This warning is logged once."
303                            );
304                        });
305                        Ok(())
306                    }
307                }
308                None => adaptor.truncate(length).await,
309            }
310        }
311
312        #[cfg(not(target_os = "macos"))]
313        {
314            // Other Unix (BSD, etc.): no portable preallocate syscall; use
315            // ftruncate via set_len which is the standard approach.
316            adaptor.truncate(length).await
317        }
318    }
319
320    #[cfg(not(unix))]
321    {
322        // Windows: attempt SetFileValidData for true space allocation.
323        // Requires SE_MANAGE_VOLUME_PRIVILEGE; fall back to sparse set_len
324        // (SetEndOfFile) when the privilege is not held or the call fails.
325        //
326        // NOTE: The raw HANDLE (*mut c_void) is not `Send`, so it must NOT be
327        // held across an await point (the future is required to be `Send` by
328        // callers). We therefore size the file first via `truncate` (which does
329        // not need the handle), then fetch the handle and invoke
330        // SetFileValidData synchronously with no await while the handle is live.
331        // The zero-fill (which may await) happens AFTER the handle goes out of
332        // scope, using a boolean flag to carry the result across the scope
333        // boundary.
334        adaptor.truncate(length).await?;
335        let valid_data_succeeded: bool = if let Some(handle) = adaptor.windows_raw_handle() {
336            // Extend the valid data length up to `length`, forcing the
337            // filesystem to allocate real blocks rather than a sparse hole.
338            let ok = unsafe {
339                windows_sys::Win32::Storage::FileSystem::SetFileValidData(handle, length as i64)
340            };
341            if ok == 0 {
342                let err = unsafe { windows_sys::Win32::Foundation::GetLastError() };
343                if err == windows_sys::Win32::Foundation::ERROR_PRIVILEGE_NOT_HELD {
344                    // Promote to warn: SetFileValidData failure causes a 2x I/O
345                    // penalty because the writer must zero-fill every block on
346                    // first write (sparse file allocation on Windows).
347                    tracing::warn!(
348                        length,
349                        "SetFileValidData requires SE_MANAGE_VOLUME_PRIVILEGE; \
350                         falling back to sparse file. This causes ~2x I/O on \
351                         first write because each block must be zeroed by the \
352                         writer instead of being pre-allocated."
353                    );
354                } else {
355                    tracing::warn!(length, err, "SetFileValidData failed; file remains sparse");
356                }
357                false
358            } else {
359                true
360            }
361        } else {
362            false
363        };
364
365        // SetFileValidData succeeded but does NOT zero-fill the allocated
366        // blocks (it exposes whatever was previously on disk). Zero-fill when
367        // secure is requested, otherwise emit a one-time warning about the
368        // trade-off. This block is outside the handle scope so the await is
369        // safe (no non-Send raw handle is live across the suspension point).
370        if valid_data_succeeded {
371            if secure {
372                async_zero_fill(adaptor, length).await
373            } else {
374                SECURE_FALLOC_WARN_ONCE.call_once(|| {
375                    tracing::warn!(
376                        "SetFileValidData succeeded but does not zero-fill; \
377                         residual disk data may be exposed. \
378                         Enable --secure-falloc=true to zero-fill (at a \
379                         performance cost). This warning is logged once."
380                    );
381                });
382                Ok(())
383            }
384        } else {
385            Ok(())
386        }
387    }
388}
389
390/// Truncate file to the specified length.
391/// Works identically on all platforms using set_len:
392/// - Unix: ftruncate system call
393/// - Windows: SetEndOfFile API
394/// - macOS: ftruncate
395async fn truncate<D: DiskAdaptor>(adaptor: &mut D, length: u64) -> Result<()> {
396    adaptor.truncate(length).await
397}
398
399pub async fn get_available_space(path: &Path) -> Result<u64> {
400    let parent = path.parent().unwrap_or_else(|| Path::new("."));
401
402    #[cfg(target_os = "linux")]
403    {
404        let _metadata = tokio::fs::metadata(parent)
405            .await
406            .map_err(|e| Aria2Error::Io(e.to_string()))?;
407
408        let statvfs_result = unsafe {
409            let mut stat: libc::statvfs64 = std::mem::zeroed();
410            let ret = libc::statvfs64(
411                parent.to_str().unwrap_or(".").as_ptr() as *const i8,
412                &mut stat,
413            );
414            (ret, stat)
415        };
416
417        if statvfs_result.0 == 0 {
418            let stat = statvfs_result.1;
419            Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
420        } else {
421            Err(Aria2Error::Io("Failed to get disk space".to_string()))
422        }
423    }
424
425    #[cfg(all(unix, not(target_os = "linux")))]
426    {
427        // On macOS and other Unix systems, use statvfs (not statvfs64)
428        // macOS statvfs already handles large files
429        let _metadata = tokio::fs::metadata(parent)
430            .await
431            .map_err(|e| Aria2Error::Io(e.to_string()))?;
432
433        let statvfs_result = unsafe {
434            let mut stat: libc::statvfs = std::mem::zeroed();
435            let ret = libc::statvfs(
436                parent.to_str().unwrap_or(".").as_ptr() as *const i8,
437                &mut stat,
438            );
439            (ret, stat)
440        };
441
442        if statvfs_result.0 == 0 {
443            let stat = statvfs_result.1;
444            Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
445        } else {
446            Err(Aria2Error::Io("Failed to get disk space".to_string()))
447        }
448    }
449
450    #[cfg(windows)]
451    {
452        let metadata = tokio::fs::metadata(parent)
453            .await
454            .map_err(|e| Aria2Error::Io(e.to_string()))?;
455
456        let free = metadata.len();
457        if free > 0 { Ok(free) } else { Ok(u64::MAX / 2) }
458    }
459
460    #[cfg(all(not(unix), not(windows)))]
461    {
462        Ok(u64::MAX)
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_allocation_strategy_from_str() {
472        assert_eq!(
473            AllocationStrategy::from_str("none"),
474            AllocationStrategy::None
475        );
476        assert_eq!(
477            AllocationStrategy::from_str("prealloc"),
478            AllocationStrategy::Prealloc
479        );
480        assert_eq!(
481            AllocationStrategy::from_str("falloc"),
482            AllocationStrategy::Falloc
483        );
484        assert_eq!(
485            AllocationStrategy::from_str("trunc"),
486            AllocationStrategy::Trunc
487        );
488        assert_eq!(
489            AllocationStrategy::from_str("mmap"),
490            AllocationStrategy::Mmap
491        );
492        assert_eq!(
493            AllocationStrategy::from_str("invalid"),
494            AllocationStrategy::None
495        );
496        assert_eq!(AllocationStrategy::from_str(""), AllocationStrategy::None);
497    }
498
499    #[tokio::test]
500    async fn test_preallocate_file_none() {
501        let dir = tempfile::tempdir().unwrap();
502        let path = dir.path().join("test_none.bin");
503        preallocate_file(&path, 1024, "none", false).await.unwrap();
504        assert!(!path.exists());
505    }
506
507    #[tokio::test]
508    async fn test_preallocate_file_trunc() {
509        let dir = tempfile::tempdir().unwrap();
510        let path = dir.path().join("test_trunc.bin");
511        preallocate_file(&path, 4096, "trunc", false).await.unwrap();
512
513        let metadata = tokio::fs::metadata(&path).await.unwrap();
514        assert_eq!(metadata.len(), 4096);
515    }
516
517    #[tokio::test]
518    async fn test_preallocate_file_prealloc() {
519        let dir = tempfile::tempdir().unwrap();
520        let path = dir.path().join("test_prealloc.bin");
521        preallocate_file(&path, 1024 * 1024, "prealloc", false)
522            .await
523            .unwrap();
524
525        let metadata = tokio::fs::metadata(&path).await.unwrap();
526        assert_eq!(metadata.len(), 1024 * 1024);
527    }
528
529    #[tokio::test]
530    async fn test_preallocate_zero_length() {
531        let dir = tempfile::tempdir().unwrap();
532        let path = dir.path().join("test_zero.bin");
533        preallocate_file(&path, 0, "trunc", false).await.unwrap();
534        assert!(!path.exists());
535    }
536
537    #[tokio::test]
538    async fn test_preallocate_creates_parent_dirs() {
539        let dir = tempfile::tempdir().unwrap();
540        let path = dir.path().join("sub1").join("sub2").join("test_nested.bin");
541        preallocate_file(&path, 100, "trunc", false).await.unwrap();
542
543        assert!(path.exists());
544        let metadata = tokio::fs::metadata(&path).await.unwrap();
545        assert_eq!(metadata.len(), 100);
546    }
547
548    #[tokio::test]
549    async fn test_get_available_space_returns_value() {
550        let dir = tempfile::tempdir().unwrap();
551        let space = get_available_space(dir.path()).await;
552        assert!(space.is_ok());
553        let val = space.unwrap();
554        assert!(val > 0);
555    }
556
557    #[tokio::test]
558    async fn test_preallocate_overwrite_existing() {
559        let dir = tempfile::tempdir().unwrap();
560        let path = dir.path().join("test_overwrite.bin");
561
562        tokio::fs::write(&path, b"original data").await.unwrap();
563        preallocate_file(&path, 2048, "trunc", false).await.unwrap();
564
565        let metadata = tokio::fs::metadata(&path).await.unwrap();
566        assert_eq!(metadata.len(), 2048);
567    }
568
569    /// Test cross-platform file allocation with Prealloc strategy
570    /// Verifies that Prealloc works on Windows, macOS, and Linux
571    #[tokio::test]
572    async fn test_allocate_file_cross_platform_prealloc() {
573        let dir = tempfile::tempdir().unwrap();
574        let path = dir.path().join("test_alloc_prealloc.bin");
575
576        // Create file with initial content
577        tokio::fs::write(&path, b"hello").await.unwrap();
578
579        // Allocate 10MB using Prealloc strategy
580        preallocate_file(&path, 10 * 1024 * 1024, "prealloc", false)
581            .await
582            .unwrap();
583
584        // Verify size is correct
585        let metadata = tokio::fs::metadata(&path).await.unwrap();
586        assert_eq!(metadata.len(), 10 * 1024 * 1024);
587    }
588
589    /// Test cross-platform file allocation with Falloc strategy
590    /// Verifies that Falloc works on Windows (using set_len fallback) and Unix (using posix_fallocate)
591    #[tokio::test]
592    async fn test_allocate_file_cross_platform_falloc() {
593        let dir = tempfile::tempdir().unwrap();
594        let path = dir.path().join("test_alloc_falloc.bin");
595
596        // Create file first
597        tokio::fs::write(&path, b"initial data").await.unwrap();
598
599        // Allocate 5MB using Falloc strategy
600        preallocate_file(&path, 5 * 1024 * 1024, "falloc", false)
601            .await
602            .unwrap();
603
604        // Verify size
605        let metadata = tokio::fs::metadata(&path).await.unwrap();
606        assert_eq!(metadata.len(), 5 * 1024 * 1024);
607    }
608
609    /// Test that the Falloc strategy produces a file of the correct logical size.
610    ///
611    /// On Windows the file is sparse unless `SetFileValidData` succeeds (which
612    /// requires `SE_MANAGE_VOLUME_PRIVILEGE`, typically absent in test runs).
613    /// On Unix, `posix_fallocate`/`F_PREALLOCATE` allocate real blocks when
614    /// supported. This test only asserts the logical size, keeping it
615    /// cross-platform and independent of privilege state.
616    #[tokio::test]
617    async fn test_fallocate_creates_sparse_file_of_correct_size() {
618        let dir = tempfile::tempdir().unwrap();
619        let path = dir.path().join("test_falloc_sparse.bin");
620
621        preallocate_file(&path, 1024 * 1024, "falloc", false)
622            .await
623            .unwrap();
624
625        let metadata = tokio::fs::metadata(&path).await.unwrap();
626        assert_eq!(metadata.len(), 1024 * 1024);
627    }
628
629    /// Test cross-platform file allocation with Trunc strategy
630    /// Verifies that Trunc works identically on all platforms via set_len
631    #[tokio::test]
632    async fn test_allocate_file_cross_platform_trunc() {
633        let dir = tempfile::tempdir().unwrap();
634        let path = dir.path().join("test_alloc_trunc.bin");
635
636        // Create file with some data
637        tokio::fs::write(&path, b"some initial content here")
638            .await
639            .unwrap();
640
641        // Truncate to 1MB using Trunc strategy
642        preallocate_file(&path, 1024 * 1024, "trunc", false)
643            .await
644            .unwrap();
645
646        // Verify size
647        let metadata = tokio::fs::metadata(&path).await.unwrap();
648        assert_eq!(metadata.len(), 1024 * 1024);
649    }
650
651    /// Test None strategy does not create files
652    #[tokio::test]
653    async fn test_allocate_file_cross_platform_none() {
654        let dir = tempfile::tempdir().unwrap();
655        let path = dir.path().join("test_alloc_none.bin");
656
657        // Try to allocate with None strategy - should not create file
658        preallocate_file(&path, 1024 * 1024, "none", false)
659            .await
660            .unwrap();
661
662        // File should not exist
663        assert!(!path.exists());
664    }
665
666    /// Test allocating a large file (50MB) to verify performance across platforms
667    #[tokio::test]
668    async fn test_allocate_large_file() {
669        let dir = tempfile::tempdir().unwrap();
670        let path = dir.path().join("test_large_alloc.bin");
671
672        // Allocate 50MB using falloc strategy
673        preallocate_file(&path, 50 * 1024 * 1024, "falloc", false)
674            .await
675            .unwrap();
676
677        // Verify size
678        let metadata = tokio::fs::metadata(&path).await.unwrap();
679        assert_eq!(metadata.len(), 50 * 1024 * 1024);
680
681        // Verify we can write to the allocated space
682        use tokio::io::{AsyncSeekExt, AsyncWriteExt};
683        let mut file = tokio::fs::OpenOptions::new()
684            .write(true)
685            .open(&path)
686            .await
687            .unwrap();
688
689        // Write at an offset near the end of the file
690        file.seek(std::io::SeekFrom::Start(49 * 1024 * 1024))
691            .await
692            .unwrap();
693        file.write_all(b"end marker").await.unwrap();
694        file.flush().await.unwrap();
695        drop(file);
696
697        // Verify final size unchanged
698        let metadata = tokio::fs::metadata(&path).await.unwrap();
699        assert_eq!(metadata.len(), 50 * 1024 * 1024);
700    }
701
702    /// Test that all three allocation strategies produce same result
703    #[tokio::test]
704    async fn test_all_strategies_same_result() {
705        let dir = tempfile::tempdir().unwrap();
706        let test_size: u64 = 1024 * 100; // 100KB
707
708        let strategies = ["prealloc", "falloc", "trunc"];
709
710        for (i, strategy) in strategies.iter().enumerate() {
711            let path = dir.path().join(format!("test_strategy_{}.bin", i));
712
713            preallocate_file(&path, test_size, strategy, false)
714                .await
715                .unwrap();
716
717            let metadata = tokio::fs::metadata(&path).await.unwrap();
718            assert_eq!(
719                metadata.len(),
720                test_size,
721                "Strategy {} produced wrong size",
722                strategy
723            );
724        }
725    }
726
727    /// Test progress callback is invoked for large file allocation (>=100MB)
728    #[tokio::test]
729    async fn test_preallocate_with_progress_callback() {
730        use std::sync::{Arc, Mutex};
731
732        let dir = tempfile::tempdir().unwrap();
733        let path = dir.path().join("test_progress.bin");
734
735        let progress_calls: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
736        let pc = progress_calls.clone();
737
738        preallocate_file_with_progress(
739            &path,
740            150 * 1024 * 1024, // 150MB — exceeds 100MB threshold
741            "prealloc",
742            Some(&|allocated, total| {
743                pc.lock().unwrap().push((allocated, total));
744            }),
745            false,
746        )
747        .await
748        .unwrap();
749
750        {
751            // Lock scope - must be dropped before await below
752            let calls = progress_calls.lock().unwrap();
753            // Should have at least start(0) and end(total) calls
754            assert!(
755                calls.len() >= 2,
756                "expected at least 2 progress calls, got {}",
757                calls.len()
758            );
759            assert_eq!(calls.first().unwrap().0, 0); // Start: 0 bytes
760            assert_eq!(
761                calls.last().unwrap().0,
762                150 * 1024 * 1024 // End: full size
763            );
764            assert_eq!(calls.last().unwrap().1, 150 * 1024 * 1024);
765        } // lock dropped here
766
767        // Verify file was actually created correctly
768        let metadata = tokio::fs::metadata(&path).await.unwrap();
769        assert_eq!(metadata.len(), 150 * 1024 * 1024);
770    }
771
772    /// Test small file does NOT trigger progress callback (<100MB)
773    #[tokio::test]
774    async fn test_preallocate_small_file_no_progress_callback() {
775        use std::sync::{Arc, Mutex};
776
777        let dir = tempfile::tempdir().unwrap();
778        let path = dir.path().join("test_small_progress.bin");
779
780        let progress_calls: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
781        let pc = progress_calls.clone();
782
783        preallocate_file_with_progress(
784            &path,
785            1024, // 1KB — well under 100MB threshold
786            "trunc",
787            Some(&|allocated, total| {
788                pc.lock().unwrap().push((allocated, total));
789            }),
790            false,
791        )
792        .await
793        .unwrap();
794
795        let calls = progress_calls.lock().unwrap();
796        // Small files should NOT trigger callback
797        assert!(
798            calls.is_empty(),
799            "small file should not trigger progress, got {} calls",
800            calls.len()
801        );
802    }
803
804    /// Verify the workspace default file allocation strategy is `falloc` and
805    /// that it parses to `AllocationStrategy::Falloc`.
806    #[test]
807    fn test_default_allocation_is_falloc() {
808        use crate::constants;
809        assert_eq!(constants::DEFAULT_FILE_ALLOCATION, "falloc");
810        assert_eq!(
811            AllocationStrategy::from_str(constants::DEFAULT_FILE_ALLOCATION),
812            AllocationStrategy::Falloc
813        );
814    }
815
816    /// Test that async_zero_fill produces a file of the correct size filled
817    /// with zeros.
818    #[tokio::test]
819    async fn test_async_zero_fill() {
820        let dir = tempfile::tempdir().unwrap();
821        let path = dir.path().join("test_zero_fill.bin");
822
823        let mut adaptor = DirectDiskAdaptor::new();
824        adaptor.open(&path).await.unwrap();
825        adaptor.truncate(5 * 1024 * 1024).await.unwrap(); // 5 MiB
826        async_zero_fill(&mut adaptor, 5 * 1024 * 1024)
827            .await
828            .unwrap();
829        adaptor.close().await.unwrap();
830
831        // Verify size
832        let metadata = tokio::fs::metadata(&path).await.unwrap();
833        assert_eq!(metadata.len(), 5 * 1024 * 1024);
834
835        // Verify content is all zeros
836        let content = tokio::fs::read(&path).await.unwrap();
837        assert!(content.iter().all(|&b| b == 0), "File should be all zeros");
838    }
839
840    /// Test that secure_falloc option defaults to false in DownloadOptions
841    #[test]
842    fn test_secure_falloc_default() {
843        use crate::request::request_group::DownloadOptions;
844        let opts = DownloadOptions::default();
845        assert!(!opts.secure_falloc, "secure_falloc should default to false");
846    }
847}