1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
use std::process::Command;
use thiserror::Error;
/// Enum for errors that can occur when interacting with the 'dd' command.
#[derive(Error, Debug)]
pub enum DdError {
/// Error when the subprocess for 'dd' cannot be created.
#[error("Failed to spawn the 'dd' process: {0}")]
ProcessSpawnError(std::io::Error),
/// Error when the 'dd' binary is missing or corrupted.
#[error("'dd' binary is missing or corrupted.")]
BinaryMissing,
/// Error when converting stdout bytes to a UTF-8 string fails.
#[error("Failed to decode the stdout output from 'dd' as UTF-8.")]
StdoutUTF8DecodeError,
/// Error for invalid output returned from the 'dd' command.
#[error("Unexpected or invalid output from 'dd' command.")]
InvalidCommandOutput,
/// Error when the 'dd' binary version is older than the minimum required.
#[error("'dd' version is older than the required minimum version.")]
VersionTooOld,
/// Error when no input is provided to the 'dd' command.
#[error("No input file specified for 'dd' command.")]
MissingInputFile,
}
/// Struct representing the configuration for running the 'dd' command.
pub struct Dd {
binary: String, // Path to the 'dd' binary.
min_version: Option<(u16, u16)>, // Optional minimum version for 'dd'.
input: Option<String>, // Optional input file.
output: Option<String>, // Optional output file.
options: Vec<String>, // Additional arguments for the 'dd' command.
}
impl Dd {
/// Constructs a new `Dd` instance with the given binary path.
///
/// # Parameters
/// - `binary`: Path to the 'dd' binary executable.
///
/// # Returns
/// A new instance of `Dd`.
pub fn new(binary: &str) -> Self {
Self {
binary: binary.to_string(),
input: None,
output: None,
min_version: None,
options: Vec::new(),
}
}
/// Checks if the 'dd' binary exists and is functional by querying its version.
///
/// This method also ensures the binary is at least the minimum required version,
/// if specified.
///
/// # Returns
/// - `Ok(())` if the binary is found and the version is valid.
/// - `Err(DdError)` if any error occurs (e.g., binary not found, version too old, etc.).
fn check(&self) -> Result<(), DdError> {
// Run the 'dd' command with the '--version' argument to check its version.
let cmd = Command::new(&self.binary).arg("--version").output();
match cmd {
// If the command fails to run (for example, if 'dd' is missing), return an error.
Err(_) => Err(DdError::BinaryMissing),
Ok(output) => {
// If the command ran but was not successful, handle stderr output.
if !output.status.success() {
let stderr =
String::from_utf8(output.stderr).map_err(|_| DdError::StdoutUTF8DecodeError)?;
return Err(DdError::ProcessSpawnError(std::io::Error::new(
std::io::ErrorKind::Other,
stderr,
)));
}
// Convert stdout (which contains the version info) into a String.
let stdout = String::from_utf8(output.stdout);
if stdout.is_err() {
return Err(DdError::StdoutUTF8DecodeError);
}
// Parse the version from stdout.
let version_str = stdout.unwrap();
let version_parts: Vec<&str> = version_str
.split_whitespace()
.nth(2) // Get the version string from the third element.
.ok_or(DdError::InvalidCommandOutput)?
.split('.')
.collect();
// If the version format is incorrect, return an error.
if version_parts.len() != 2 {
return Err(DdError::InvalidCommandOutput);
}
// Parse major and minor version from the version string.
let version = (
version_parts[0].parse::<u16>().unwrap_or(0),
version_parts[1].parse::<u16>().unwrap_or(0),
);
// Check if the 'dd' binary meets the minimum version requirement.
if let Some(min_version) = self.min_version {
if version < min_version {
return Err(DdError::VersionTooOld);
}
}
Ok(()) // The 'dd' binary is valid.
}
}
}
/// Helper method to add key-value arguments to the 'dd' command.
///
/// # Parameters
/// - `key`: The argument key (e.g., "bs").
/// - `value`: The corresponding argument value (e.g., "64K").
fn arg(&mut self, key: &str, value: &str) {
self.options.push(format!("{key}={value}"));
}
/// Helper method to add arguments to the given `Command` struct.
///
/// This method checks if `input` and `output` are set, and then adds
/// the appropriate arguments to the command. It also adds any extra
/// options stored in `self.options`.
///
/// # Parameters
/// - `cmd`: The `Command` struct to which arguments will be added.
fn set_args(&mut self, cmd: &mut Command) -> Result<(), DdError> {
// If input file is specified, add it to the command as 'if' (input file).
if let Some(input) = &self.input {
cmd.arg(format!("if={}", input));
} else {
return Err(DdError::MissingInputFile); // 'dd' requires an input file.
}
// If output file is specified, add it to the command as 'of' (output file).
if let Some(output) = &self.output {
cmd.arg(format!("of={}", output));
}
// Add any extra options (block size, conversion, etc.) from the options vector.
for option in &self.options {
cmd.arg(option);
}
Ok(())
}
/// Sets the minimum version required for the 'dd' binary.
///
/// This method allows setting a specific minimum version (major, minor) for 'dd'.
/// The version will be compared against the installed version during the `check()` method.
///
/// # Parameters
/// - `version`: A tuple representing the minimum major and minor version.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn min_version(&mut self, version: (u16, u16)) -> &mut Self {
self.min_version = Some(version);
self
}
/// Sets the input file for the 'dd' command.
///
/// # Parameters
/// - `input`: Path to the input file.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn input(&mut self, input: &str) -> &mut Self {
self.input = Some(String::from(input));
self
}
/// Sets the output file for the 'dd' command.
///
/// # Parameters
/// - `output`: Path to the output file.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn output(&mut self, output: &str) -> &mut Self {
self.output = Some(String::from(output));
self
}
/// Sets the block size (bs) argument for the 'dd' command.
///
/// The `bs` argument defines the block size for reading and writing.
/// For example, "64K" would mean 64 kilobytes per block.
///
/// # Parameters
/// - `value`: The block size value (e.g., "64K").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn bs(&mut self, value: &str) -> &mut Self {
self.arg("bs", value);
self
}
/// Sets the conversion block size (cbs) argument for the 'dd' command.
///
/// This argument specifies the size of blocks for conversion operations.
/// It may be used alongside the `conv` argument to specify data formatting.
pub fn cbs(&mut self, value: &str) -> &mut Self {
self.arg("cbs", value);
self
}
/// Sets the count argument for the 'dd' command.
///
/// The `count` argument specifies how many blocks will be copied from the input file.
///
/// # Parameters
/// - `value`: The number of blocks to copy.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn count(&mut self, value: u64) -> &mut Self {
self.arg("count", &value.to_string());
self
}
/// Sets the seek argument for the 'dd' command.
///
/// The `seek` argument skips a specified number of blocks in the output file before writing.
///
/// # Parameters
/// - `value`: The number of blocks to skip in the output file.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn seek(&mut self, value: u64) -> &mut Self {
self.arg("seek", &value.to_string());
self
}
/// Sets the skip argument for the 'dd' command.
///
/// The `skip` argument skips a specified number of blocks in the input file before starting to copy.
///
/// # Parameters
/// - `value`: The number of blocks to skip in the input file.
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn skip(&mut self, value: u64) -> &mut Self {
self.arg("skip", &value.to_string());
self
}
/// Sets the status argument for the 'dd' command.
///
/// The `status` argument controls the verbosity of the output. For example, it can show progress during execution.
///
/// # Parameters
/// - `value`: The desired status (e.g., "progress").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn status(&mut self, value: &str) -> &mut Self {
self.arg("status", value);
self
}
/// Sets the conversion argument (conv) for the 'dd' command.
///
/// The `conv` argument specifies conversions to perform on the data as it is copied.
/// For example, `noerror` can be used to continue reading even if errors are encountered.
///
/// # Parameters
/// - `value`: The conversion to perform (e.g., "noerror").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn conv(&mut self, value: &str) -> &mut Self {
self.arg("conv", value);
self
}
/// Sets the input block size (ibs) argument for the 'dd' command.
///
/// This specifies the block size for reading from the input file.
///
/// # Parameters
/// - `value`: The input block size (e.g., "64K").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn ibs(&mut self, value: &str) -> &mut Self {
self.arg("ibs", value);
self
}
/// Sets the input flag (iflag) argument for the 'dd' command.
///
/// The `iflag` argument defines input-specific flags (e.g., `fullblock` to ensure all input is read).
///
/// # Parameters
/// - `value`: The flag value (e.g., "fullblock").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn iflag(&mut self, value: &str) -> &mut Self {
self.arg("iflag", value);
self
}
/// Sets the output block size (obs) argument for the 'dd' command.
///
/// This specifies the block size for writing to the output file.
///
/// # Parameters
/// - `value`: The output block size (e.g., "64K").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn obs(&mut self, value: &str) -> &mut Self {
self.arg("obs", value);
self
}
/// Sets the output flag (oflag) argument for the 'dd' command.
///
/// The `oflag` argument defines output-specific flags (e.g., `append` to append data).
///
/// # Parameters
/// - `value`: The flag value (e.g., "append").
///
/// # Returns
/// - `&mut Self` to allow for method chaining.
pub fn oflag(&mut self, value: &str) -> &mut Self {
self.arg("oflag", value);
self
}
/// Spawns and runs the 'dd' command with the configured options.
///
/// This method first checks the binary, then constructs the 'dd' command
/// with the provided input, output, and options. It returns the command's
/// stdout output if successful.
///
/// # Returns
/// - `Ok(String)` containing the command output if the process runs successfully.
/// - `Err(DdError)` if an error occurs at any stage.
pub fn spawn(&mut self) -> Result<String, DdError> {
// Check if 'dd' binary is available and valid.
self.check()?;
let mut cmd = Command::new(&self.binary);
self.set_args(&mut cmd)?;
// Execute the command.
let output = cmd.output();
match output {
Ok(output) if output.status.success() => {
// Convert stdout to String and return.
String::from_utf8(output.stdout).map_err(|_| DdError::StdoutUTF8DecodeError)
}
Ok(output) => Err(DdError::ProcessSpawnError(std::io::Error::new(
std::io::ErrorKind::Other,
String::from_utf8(output.stderr)
.unwrap_or(String::from("unable to get stderr from 'dd'")),
))),
Err(e) => Err(DdError::ProcessSpawnError(e)),
}
}
}