diff --git a/README.md b/README.md index 8020c3f1..810f3081 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ count below and mark it as done in this README.md. Thanks! GNU coreutils. They are not 100% compatiable. If you encounter different behaviors, compare against the true GNU coreutils version on the Linux-based tests first. -## Completed (66/109) +## Completed (67/109) | Done | Cmd | Descripton | | :-----: | --------- | ------------------------------------------------ | @@ -127,7 +127,7 @@ compare against the true GNU coreutils version on the Linux-based tests first. | ✓ | sha256sum | Print or check SHA-2 256 bit digests | | ✓ | sha384sum | Print or check SHA-2 384 bit digests | | ✓ | sha512sum | Print or check SHA-2 512 bit digests | -| | shred | Remove files more securely | +| ✓ | shred | Remove files more securely | | ✓ | shuf | Shuffling text | | ✓ | sleep | Delay for a specified time | | ✓ | sort | Sort text files | diff --git a/src/shred/config.v b/src/shred/config.v new file mode 100644 index 00000000..fa8fa4c5 --- /dev/null +++ b/src/shred/config.v @@ -0,0 +1,66 @@ +import common +import flag +import os + +@[name: 'shred'] +@[version: '0.1'] +struct Config { + force bool @[short: f; xdoc: 'change permissions to allow writing if necessary'] + iterations int = 3 @[short: n; xdoc: 'overwrite N times instead of the default (3)'] + random_source string @[xdoc: 'get random bytes from '] + size string @[short: s; xdoc: 'shred this many bytes (suffixes like K, M, G accepted)\n'] + rm bool @[only: u; xdoc: 'deallocate and remove file after overwriting'] + remove_how string @[long: remove; xdoc: 'like -u but give control on to delete; See below'] + verbose bool @[short: v; xdoc: 'show progress'] + exact bool @[short: x; xdoc: 'do not round file sizes up to the next full block; this is the default for non-regular files'] + zero bool @[short: z; xdoc: 'add a final overwrite with zeros to hide shredding'] + show_help bool @[long: help; short: h; xdoc: 'show this help'] + show_version bool @[long: 'version'; xdoc: 'show version and exit'] +} + +fn get_args() (Config, []string) { + config, files := flag.to_struct[Config](os.args, skip: 1) or { panic(err) } + + if config.show_help { + doc := flag.to_doc[Config]( + description: 'Usage: shred [OPTION]... FILE...\n' + + 'Overwrite the specified FILE(s) repeatedly, in order to make it harder\n' + + 'for even very expensive hardware probing to recover the data.' + footer: + '\nDelete FILE(s) if --remove (-u) is specified. The default is not to remove\n' + + 'the files because it is common to operate on device files like /dev/hda,\n' + + 'and those files usually should not be removed.\n\n' + + 'The --remove parameter indicates how to remove a directory entry:\n' + + " 'unlink' => use a standard unlink call.\n" + + " 'wipe' => also first obfuscate bytes in the name.\n" + + " 'wipesync' => also sync each obfuscated byte to the device.\n" + + "The default mode is 'wipesync', but note it can be expensive.\n\n" + + 'CAUTION: shred assumes the file system and hardware overwrite data in place.\n' + + 'Although this is common, many platforms operate otherwise. Also, backups\n' + + 'and mirrors may contain unremovable copies that will let a shredded file\n' + + 'be recovered later.\n' + common.coreutils_footer() + ) or { panic(err) } + println(doc) + exit(0) + } + + if files.len > 0 && files.any(it.starts_with('-')) { + eexit('The following flags could not be mapped to any fields: ${files.filter(it.starts_with('-'))}') + } + + if files.filter(!it.starts_with('-')).len == 0 { + eexit('missing file operand') + } + + if config.iterations <= 0 { + eexit('interations must be greater than zero') + } + + return config, files +} + +@[noreturn] +fn eexit(msg string) { + eprintln(msg) + exit(1) +} diff --git a/src/shred/delete.me b/src/shred/delete.me deleted file mode 100644 index e69de29b..00000000 diff --git a/src/shred/shred.v b/src/shred/shred.v new file mode 100644 index 00000000..1d64f938 --- /dev/null +++ b/src/shred/shred.v @@ -0,0 +1,166 @@ +import os +import rand +import math +import strconv + +enum Fill_Pattern { + random + zeros + source +} + +const block_size = 4096 + +fn main() { + config, files := get_args() + shred(files, config) +} + +fn shred(files []string, config Config) { + for file in files { + total_iterations := if config.zero { config.iterations + 1 } else { config.iterations } + fill_pattern := if config.random_source.len > 0 { + Fill_Pattern.source + } else { + Fill_Pattern.random + } + for iteration in 0 .. config.iterations { + shred_file(file, fill_pattern, iteration + 1, total_iterations, config) + } + if config.zero { + shred_file(file, .zeros, total_iterations, total_iterations, config) + } + if config.rm || config.remove_how.len > 0 { + remove(file, config) + } + } +} + +fn shred_file(file string, fill_pattern Fill_Pattern, iteration int, iterations int, config Config) { + stat := os.lstat(file) or { eexit(err.msg()) } + size_arg := convert_to_number(config.size) + mut block := if stat.size >= u64(block_size) { block_size } else { int(stat.size) } + mut fp := open_file_for_write(file, config) + mut written := u64(0) + for { + pattern := match fill_pattern { + .random { rand.bytes(block) or { panic(err) } } + .source { random_from_source(config.random_source) } + .zeros { []u8{len: block, init: 0} } + } + wrote := fp.write(pattern) or { panic(err) } + written += u64(wrote) + sz := math.min(size_arg, stat.size) + if written >= sz { + break + } + remaining := stat.size - written + block = if remaining > block_size { block_size } else { int(remaining) } + } + fp.close() + if config.verbose { + show_shred_progress(file, iteration, iterations, fill_pattern) + } +} + +fn open_file_for_write(file string, config Config) os.File { + return os.create(file) or { + if !config.force { + eexit(err.msg()) + } + os.chmod(file, 0o600) or { eexit(err.msg()) } + return os.create(file) or { eexit(err.msg()) } + } +} + +fn show_shred_progress(file string, iteration int, iterations int, fill_pattern Fill_Pattern) { + pattern := match fill_pattern { + .random { 'random' } + .source { 'source' } + .zeros { '000000' } + } + println('${progress_prefix(file)}: pass ${iteration} of ${iterations} (${pattern})...') +} + +fn progress_prefix(file string) string { + return 'shred ${file}' +} + +fn remove(file string, config Config) { + match config.remove_how { + '', 'wipesync', 'wipe' { + name := rename(file) + if config.verbose { + println('${progress_prefix(file)}: renamed to ${name}') + } + os.rm(name) or { panic(err) } + if config.verbose { + println('${progress_prefix(file)}: removed') + } + } + 'unlink' { + os.rm(file) or { panic(err) } + if config.verbose { + println('${progress_prefix(file)}: removed') + } + } + else { + eexit('unrecognized --remove option') + } + } +} + +fn rename(file string) string { + for _ in 0 .. 10 { + name := file + '${rand.u32()}' + os.rename(file, name) or {} + if os.exists(name) { + return name + } + } + eexit('can not rename file ${file}') +} + +fn random_from_source(file string) []u8 { + stat := os.lstat(file) or { eexit(err.msg()) } + file_len := stat.size - 1 + if file_len == 0 { + panic('zero length source file detected') + } + mut fp := os.open(file) or { panic(err) } + mut buf := []u8{} + for _ in 0 .. block_size { + pos := rand.u64n(file_len) or { panic(err) } + b := fp.read_bytes_at(1, pos) + buf << b + } + fp.close() + return buf +} + +fn convert_to_number(input string) u64 { + if input.len == 0 { + return max_u64 + } + if input.ends_with('T') { + number := to_u64(input[..input.len - 1]) + return number * 1024 * 1024 * 1024 * 1024 + } + if input.ends_with('G') { + number := to_u64(input[..input.len - 1]) + return number * 1024 * 1024 * 1024 + } + if input.ends_with('M') { + number := to_u64(input[..input.len - 1]) + return number * 1024 * 1024 + } + if input.ends_with('K') { + number := to_u64(input[..input.len - 1]) + return number * 1024 + } + return to_u64(input) +} + +fn to_u64(s string) u64 { + return strconv.common_parse_uint(s, 0, 64, true, true) or { eexit(err.msg()) } +}