1use crate::raw::{
8 BTRFS_DEFRAG_RANGE_COMPRESS, BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL,
9 BTRFS_DEFRAG_RANGE_NOCOMPRESS, BTRFS_DEFRAG_RANGE_START_IO,
10 btrfs_ioc_defrag_range, btrfs_ioctl_defrag_range_args,
11};
12use std::{
13 mem,
14 os::{fd::AsRawFd, unix::io::BorrowedFd},
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum CompressType {
26 Zlib = 1,
28 Lzo = 2,
30 Zstd = 3,
32}
33
34impl std::fmt::Display for CompressType {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::Zlib => f.write_str("zlib"),
38 Self::Lzo => f.write_str("lzo"),
39 Self::Zstd => f.write_str("zstd"),
40 }
41 }
42}
43
44impl std::str::FromStr for CompressType {
45 type Err = String;
46
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 match s.to_ascii_lowercase().as_str() {
49 "zlib" => Ok(Self::Zlib),
50 "lzo" => Ok(Self::Lzo),
51 "zstd" => Ok(Self::Zstd),
52 _ => Err(format!(
53 "unknown compress type '{s}'; expected zlib, lzo, or zstd"
54 )),
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
64pub struct DefragRangeArgs {
65 pub start: u64,
67 pub len: u64,
69 pub flush: bool,
71 pub extent_thresh: u32,
75 pub compress: Option<CompressSpec>,
78 pub nocomp: bool,
81}
82
83#[derive(Debug, Clone, Copy)]
85pub struct CompressSpec {
86 pub compress_type: CompressType,
88 pub level: Option<i8>,
93}
94
95impl DefragRangeArgs {
96 #[must_use]
99 pub fn new() -> Self {
100 Self {
101 start: 0,
102 len: u64::MAX,
103 flush: false,
104 extent_thresh: 0,
105 compress: None,
106 nocomp: false,
107 }
108 }
109
110 #[must_use]
112 pub fn start(mut self, start: u64) -> Self {
113 self.start = start;
114 self
115 }
116
117 #[must_use]
119 pub fn len(mut self, len: u64) -> Self {
120 self.len = len;
121 self
122 }
123
124 #[must_use]
126 pub fn flush(mut self) -> Self {
127 self.flush = true;
128 self
129 }
130
131 #[must_use]
134 pub fn extent_thresh(mut self, thresh: u32) -> Self {
135 self.extent_thresh = thresh;
136 self
137 }
138
139 #[must_use]
141 pub fn compress(mut self, spec: CompressSpec) -> Self {
142 self.compress = Some(spec);
143 self.nocomp = false;
144 self
145 }
146
147 #[must_use]
149 pub fn nocomp(mut self) -> Self {
150 self.nocomp = true;
151 self.compress = None;
152 self
153 }
154}
155
156impl Default for DefragRangeArgs {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
169 fn compress_type_display() {
170 assert_eq!(format!("{}", CompressType::Zlib), "zlib");
171 assert_eq!(format!("{}", CompressType::Lzo), "lzo");
172 assert_eq!(format!("{}", CompressType::Zstd), "zstd");
173 }
174
175 #[test]
178 fn compress_type_from_str() {
179 assert_eq!("zlib".parse::<CompressType>().unwrap(), CompressType::Zlib);
180 assert_eq!("lzo".parse::<CompressType>().unwrap(), CompressType::Lzo);
181 assert_eq!("zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
182 }
183
184 #[test]
185 fn compress_type_from_str_case_insensitive() {
186 assert_eq!("ZLIB".parse::<CompressType>().unwrap(), CompressType::Zlib);
187 assert_eq!("Zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
188 }
189
190 #[test]
191 fn compress_type_from_str_invalid() {
192 assert!("lz4".parse::<CompressType>().is_err());
193 assert!("".parse::<CompressType>().is_err());
194 }
195
196 #[test]
199 fn defrag_args_defaults() {
200 let args = DefragRangeArgs::new();
201 assert_eq!(args.start, 0);
202 assert_eq!(args.len, u64::MAX);
203 assert!(!args.flush);
204 assert_eq!(args.extent_thresh, 0);
205 assert!(args.compress.is_none());
206 assert!(!args.nocomp);
207 }
208
209 #[test]
210 fn defrag_args_builder_chain() {
211 let args = DefragRangeArgs::new()
212 .start(4096)
213 .len(1024 * 1024)
214 .flush()
215 .extent_thresh(256 * 1024);
216 assert_eq!(args.start, 4096);
217 assert_eq!(args.len, 1024 * 1024);
218 assert!(args.flush);
219 assert_eq!(args.extent_thresh, 256 * 1024);
220 }
221
222 #[test]
223 fn defrag_args_compress_clears_nocomp() {
224 let args = DefragRangeArgs::new().nocomp().compress(CompressSpec {
225 compress_type: CompressType::Zstd,
226 level: None,
227 });
228 assert!(args.compress.is_some());
229 assert!(!args.nocomp);
230 }
231
232 #[test]
233 fn defrag_args_nocomp_clears_compress() {
234 let args = DefragRangeArgs::new()
235 .compress(CompressSpec {
236 compress_type: CompressType::Zlib,
237 level: Some(3),
238 })
239 .nocomp();
240 assert!(args.compress.is_none());
241 assert!(args.nocomp);
242 }
243
244 #[test]
245 fn defrag_args_default_trait() {
246 let a = DefragRangeArgs::default();
247 let b = DefragRangeArgs::new();
248 assert_eq!(a.start, b.start);
249 assert_eq!(a.len, b.len);
250 }
251}
252
253pub fn defrag_range(fd: BorrowedFd, args: &DefragRangeArgs) -> nix::Result<()> {
263 let mut raw: btrfs_ioctl_defrag_range_args = unsafe { mem::zeroed() };
264
265 raw.start = args.start;
266 raw.len = args.len;
267 raw.extent_thresh = args.extent_thresh;
268
269 if args.flush {
270 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_START_IO);
271 }
272
273 if args.nocomp {
274 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_NOCOMPRESS);
275 } else if let Some(spec) = args.compress {
276 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS);
277 match spec.level {
278 None => {
279 raw.__bindgen_anon_1.compress_type = spec.compress_type as u32;
280 }
281 Some(level) => {
282 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL);
283 raw.__bindgen_anon_1.compress.type_ = spec.compress_type as u8;
284 raw.__bindgen_anon_1.compress.level = level;
285 }
286 }
287 }
288
289 unsafe { btrfs_ioc_defrag_range(fd.as_raw_fd(), &raw const raw) }?;
290 Ok(())
291}