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)]
22pub enum CompressType {
23 Zlib = 1,
24 Lzo = 2,
25 Zstd = 3,
26}
27
28impl std::fmt::Display for CompressType {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::Zlib => f.write_str("zlib"),
32 Self::Lzo => f.write_str("lzo"),
33 Self::Zstd => f.write_str("zstd"),
34 }
35 }
36}
37
38impl std::str::FromStr for CompressType {
39 type Err = String;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 match s.to_ascii_lowercase().as_str() {
43 "zlib" => Ok(Self::Zlib),
44 "lzo" => Ok(Self::Lzo),
45 "zstd" => Ok(Self::Zstd),
46 _ => Err(format!(
47 "unknown compress type '{s}'; expected zlib, lzo, or zstd"
48 )),
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
58pub struct DefragRangeArgs {
59 pub start: u64,
61 pub len: u64,
63 pub flush: bool,
65 pub extent_thresh: u32,
69 pub compress: Option<CompressSpec>,
72 pub nocomp: bool,
75}
76
77#[derive(Debug, Clone, Copy)]
79pub struct CompressSpec {
80 pub compress_type: CompressType,
82 pub level: Option<i8>,
87}
88
89impl DefragRangeArgs {
90 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 start: 0,
96 len: u64::MAX,
97 flush: false,
98 extent_thresh: 0,
99 compress: None,
100 nocomp: false,
101 }
102 }
103
104 #[must_use]
106 pub fn start(mut self, start: u64) -> Self {
107 self.start = start;
108 self
109 }
110
111 #[must_use]
113 pub fn len(mut self, len: u64) -> Self {
114 self.len = len;
115 self
116 }
117
118 #[must_use]
120 pub fn flush(mut self) -> Self {
121 self.flush = true;
122 self
123 }
124
125 #[must_use]
128 pub fn extent_thresh(mut self, thresh: u32) -> Self {
129 self.extent_thresh = thresh;
130 self
131 }
132
133 #[must_use]
135 pub fn compress(mut self, spec: CompressSpec) -> Self {
136 self.compress = Some(spec);
137 self.nocomp = false;
138 self
139 }
140
141 #[must_use]
143 pub fn nocomp(mut self) -> Self {
144 self.nocomp = true;
145 self.compress = None;
146 self
147 }
148}
149
150impl Default for DefragRangeArgs {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
163 fn compress_type_display() {
164 assert_eq!(format!("{}", CompressType::Zlib), "zlib");
165 assert_eq!(format!("{}", CompressType::Lzo), "lzo");
166 assert_eq!(format!("{}", CompressType::Zstd), "zstd");
167 }
168
169 #[test]
172 fn compress_type_from_str() {
173 assert_eq!("zlib".parse::<CompressType>().unwrap(), CompressType::Zlib);
174 assert_eq!("lzo".parse::<CompressType>().unwrap(), CompressType::Lzo);
175 assert_eq!("zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
176 }
177
178 #[test]
179 fn compress_type_from_str_case_insensitive() {
180 assert_eq!("ZLIB".parse::<CompressType>().unwrap(), CompressType::Zlib);
181 assert_eq!("Zstd".parse::<CompressType>().unwrap(), CompressType::Zstd);
182 }
183
184 #[test]
185 fn compress_type_from_str_invalid() {
186 assert!("lz4".parse::<CompressType>().is_err());
187 assert!("".parse::<CompressType>().is_err());
188 }
189
190 #[test]
193 fn defrag_args_defaults() {
194 let args = DefragRangeArgs::new();
195 assert_eq!(args.start, 0);
196 assert_eq!(args.len, u64::MAX);
197 assert!(!args.flush);
198 assert_eq!(args.extent_thresh, 0);
199 assert!(args.compress.is_none());
200 assert!(!args.nocomp);
201 }
202
203 #[test]
204 fn defrag_args_builder_chain() {
205 let args = DefragRangeArgs::new()
206 .start(4096)
207 .len(1024 * 1024)
208 .flush()
209 .extent_thresh(256 * 1024);
210 assert_eq!(args.start, 4096);
211 assert_eq!(args.len, 1024 * 1024);
212 assert!(args.flush);
213 assert_eq!(args.extent_thresh, 256 * 1024);
214 }
215
216 #[test]
217 fn defrag_args_compress_clears_nocomp() {
218 let args = DefragRangeArgs::new().nocomp().compress(CompressSpec {
219 compress_type: CompressType::Zstd,
220 level: None,
221 });
222 assert!(args.compress.is_some());
223 assert!(!args.nocomp);
224 }
225
226 #[test]
227 fn defrag_args_nocomp_clears_compress() {
228 let args = DefragRangeArgs::new()
229 .compress(CompressSpec {
230 compress_type: CompressType::Zlib,
231 level: Some(3),
232 })
233 .nocomp();
234 assert!(args.compress.is_none());
235 assert!(args.nocomp);
236 }
237
238 #[test]
239 fn defrag_args_default_trait() {
240 let a = DefragRangeArgs::default();
241 let b = DefragRangeArgs::new();
242 assert_eq!(a.start, b.start);
243 assert_eq!(a.len, b.len);
244 }
245}
246
247pub fn defrag_range(fd: BorrowedFd, args: &DefragRangeArgs) -> nix::Result<()> {
253 let mut raw: btrfs_ioctl_defrag_range_args = unsafe { mem::zeroed() };
254
255 raw.start = args.start;
256 raw.len = args.len;
257 raw.extent_thresh = args.extent_thresh;
258
259 if args.flush {
260 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_START_IO);
261 }
262
263 if args.nocomp {
264 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_NOCOMPRESS);
265 } else if let Some(spec) = args.compress {
266 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS);
267 match spec.level {
268 None => {
269 raw.__bindgen_anon_1.compress_type = spec.compress_type as u32;
270 }
271 Some(level) => {
272 raw.flags |= u64::from(BTRFS_DEFRAG_RANGE_COMPRESS_LEVEL);
273 raw.__bindgen_anon_1.compress.type_ = spec.compress_type as u8;
274 raw.__bindgen_anon_1.compress.level = level;
275 }
276 }
277 }
278
279 unsafe { btrfs_ioc_defrag_range(fd.as_raw_fd(), &raw const raw) }?;
280 Ok(())
281}