feat(base64-rs): add GNU-compatible base64 command for Windows
Windows has no base64 command; certutil is file-only, adds PEM markers and cannot be piped. This is a real base64.exe matching GNU coreutils: same flags (-d, -i, -w), stdin/stdout streaming, exit codes and error messages, so existing muscle memory and scripts carry over. Verified against coreutils 9.7 with a 153-case differential test. The subtle behaviours are reproduced: bytes are emitted incrementally so a stream that goes bad still writes its valid prefix, unpadded tails are accepted only when canonically encoded, concatenated streams decode as one, and -w0 suppresses the trailing newline. Deliberate deviation: '\r' is skipped when decoding alongside '\n', as CRLF base64 files are routine on Windows and GNU rejects them. Zero dependencies, so the build works offline.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "1.0.0"
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "base64"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[[bin]]
|
||||
name = "base64"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
strip = true
|
||||
panic = "abort"
|
||||
@@ -0,0 +1,72 @@
|
||||
# base64 for Windows
|
||||
|
||||
Windows has no `base64` command. `certutil -encode` is the usual substitute, but it only works on files, wraps output in `-----BEGIN CERTIFICATE-----` markers, and can't be piped. This is a real `base64.exe` that behaves like the one on a Linux box, so the commands and scripts you already know carry over.
|
||||
|
||||
```powershell
|
||||
base64 image.png > image.b64
|
||||
base64 -d image.b64 > image.png
|
||||
type secrets.json | base64 -w0
|
||||
base64 -di messy.txt > payload.bin
|
||||
```
|
||||
|
||||
## Build and install
|
||||
|
||||
```powershell
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Copy `target\release\base64.exe` somewhere on your PATH — e.g. `%LOCALAPPDATA%\Microsoft\WindowsApps`, or a `C:\tools\bin` folder you've added yourself:
|
||||
|
||||
```powershell
|
||||
mkdir C:\tools\bin
|
||||
copy target\release\base64.exe C:\tools\bin\
|
||||
[Environment]::SetEnvironmentVariable(
|
||||
'Path',
|
||||
[Environment]::GetEnvironmentVariable('Path','User') + ';C:\tools\bin',
|
||||
'User')
|
||||
```
|
||||
|
||||
Open a new terminal afterwards for the PATH change to take effect. No runtime dependencies — it's a single static binary, and the crate itself has zero dependencies, so the build works offline.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage: base64 [OPTION]... [FILE]
|
||||
Base64 encode or decode FILE, or standard input, to standard output.
|
||||
|
||||
With no FILE, or when FILE is -, read standard input.
|
||||
|
||||
-d, --decode decode data
|
||||
-i, --ignore-garbage when decoding, ignore non-alphabet characters
|
||||
-w, --wrap=COLS wrap encoded lines after COLS character (default 76).
|
||||
Use 0 to disable line wrapping
|
||||
--help display this help and exit
|
||||
--version output version information and exit
|
||||
```
|
||||
|
||||
Long options accept unambiguous abbreviations (`--dec`), short options cluster (`-di`), `-w` takes its value joined or separate (`-w0`, `-w 0`, `--wrap=0`), and `--` ends option parsing.
|
||||
|
||||
## Behaviour
|
||||
|
||||
Verified against GNU coreutils 9.7 with a 153-case differential test covering encoding at every wrap width, decode round-trips, malformed padding, truncated input, garbage handling, all the usage errors and file operands. Output, exit codes and error messages match, including the fiddly parts:
|
||||
|
||||
- **Partial output on failure.** Bytes are emitted as soon as enough bits are available, so a stream that goes bad halfway still writes the valid prefix before the error — same as coreutils.
|
||||
- **Unpadded input is accepted** when the encoding is canonical, i.e. the leftover bits are zero. `aGVsbG8` decodes to `hello`; `aGV` is rejected because `V` carries stray bits.
|
||||
- **Concatenated streams** decode as one (`aGVsbG8=aGVsbG8=` → `hellohello`).
|
||||
- **`-w0` emits no trailing newline**; wrapped output always ends with one. Empty input produces no output at all.
|
||||
- **Exit codes**: `0` success, `1` on any error.
|
||||
|
||||
### Deliberate differences
|
||||
|
||||
| | Why |
|
||||
|---|---|
|
||||
| `\r` is skipped when decoding, as `\n` already is | GNU rejects CRLF input as invalid. On Windows a base64 file will routinely have CRLF line endings, and erroring on those would make the tool useless for its main job. Everything else is still rejected unless you pass `-i`. |
|
||||
| `--help` omits GNU's support URLs | This isn't GNU coreutils; pointing at their bug tracker would be wrong. |
|
||||
| Error messages quote with `'ASCII'` | GNU uses `'…'` in a UTF-8 locale. Windows consoles are frequently cp437/cp1252, where those bytes render as mojibake. |
|
||||
|
||||
## Notes
|
||||
|
||||
- Roughly 2× slower than GNU on large inputs (100 MB encodes in ~0.35s vs ~0.18s). GNU uses hand-tuned routines; this is plain portable Rust and fast enough that the difference is invisible for normal use.
|
||||
- Input is streamed, not buffered whole, so encoding a multi-gigabyte file doesn't blow up memory.
|
||||
- Piping to a program that exits early (`base64 big.bin | head`) is handled quietly rather than reporting a broken pipe.
|
||||
- Writing raw binary to a Windows console fails with a clear message telling you to redirect, because consoles take UTF-16 text and would corrupt the bytes. Redirecting to a file or piping is unaffected.
|
||||
@@ -0,0 +1,450 @@
|
||||
//! A drop-in `base64` for Windows, matching GNU coreutils behaviour.
|
||||
//!
|
||||
//! Same flags, same output, same error messages and exit codes as the
|
||||
//! `base64` on a Linux box, so muscle memory and scripts carry over.
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufWriter, ErrorKind, Read, Write};
|
||||
use std::process::ExitCode;
|
||||
|
||||
const PROGRAM: &str = "base64";
|
||||
const VERSION: &str = concat!("base64 (jwtf tools) ", env!("CARGO_PKG_VERSION"));
|
||||
const DEFAULT_WRAP: usize = 76;
|
||||
|
||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const PAD: u8 = b'=';
|
||||
|
||||
/// Reverse lookup for the alphabet; -1 marks a byte that is not a base64 digit.
|
||||
const DECODE: [i8; 256] = {
|
||||
let mut table = [-1i8; 256];
|
||||
let mut i = 0;
|
||||
while i < 64 {
|
||||
table[ALPHABET[i] as usize] = i as i8;
|
||||
i += 1;
|
||||
}
|
||||
table
|
||||
};
|
||||
|
||||
const HELP: &str = "\
|
||||
Usage: base64 [OPTION]... [FILE]
|
||||
Base64 encode or decode FILE, or standard input, to standard output.
|
||||
|
||||
With no FILE, or when FILE is -, read standard input.
|
||||
|
||||
Mandatory arguments to long options are mandatory for short options too.
|
||||
-d, --decode decode data
|
||||
-i, --ignore-garbage when decoding, ignore non-alphabet characters
|
||||
-w, --wrap=COLS wrap encoded lines after COLS character (default 76).
|
||||
Use 0 to disable line wrapping
|
||||
--help display this help and exit
|
||||
--version output version information and exit
|
||||
|
||||
The data are encoded as described for the base64 alphabet in RFC 4648.
|
||||
When decoding, the input may contain newlines in addition to the bytes of
|
||||
the formal base64 alphabet. Use --ignore-garbage to attempt to recover
|
||||
from any other non-alphabet bytes in the encoded stream.";
|
||||
|
||||
const LONG_OPTS: [&str; 5] = ["decode", "ignore-garbage", "wrap", "help", "version"];
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(Fail::Quiet) => ExitCode::SUCCESS,
|
||||
Err(Fail::Usage(msg)) => {
|
||||
eprintln!("{PROGRAM}: {msg}");
|
||||
eprintln!("Try '{PROGRAM} --help' for more information.");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
Err(Fail::Error(msg)) => {
|
||||
eprintln!("{PROGRAM}: {msg}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Fail {
|
||||
/// Usage problem: prints the "Try --help" hint after the message.
|
||||
Usage(String),
|
||||
/// Runtime problem: message only.
|
||||
Error(String),
|
||||
/// Downstream closed the pipe (`base64 f | head`); nothing to report.
|
||||
Quiet,
|
||||
}
|
||||
|
||||
struct Options {
|
||||
decode: bool,
|
||||
ignore_garbage: bool,
|
||||
wrap: usize,
|
||||
file: Option<String>,
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Fail> {
|
||||
let opts = match parse_args(env::args().skip(1).collect())? {
|
||||
Parsed::Run(o) => o,
|
||||
Parsed::Exit(text) => {
|
||||
println!("{text}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Reading is buffered by the encode/decode loops, which pull in large
|
||||
// chunks; stdout is wrapped so small writes do not hit the OS each time.
|
||||
let stdout = io::stdout();
|
||||
let mut out = BufWriter::with_capacity(64 * 1024, stdout.lock());
|
||||
|
||||
let result = match opts.file.as_deref() {
|
||||
None | Some("-") => {
|
||||
let stdin = io::stdin();
|
||||
process(stdin.lock(), &mut out, &opts)
|
||||
}
|
||||
Some(path) => match File::open(path) {
|
||||
Ok(f) => process(f, &mut out, &opts),
|
||||
Err(e) => return Err(Fail::Error(format!("{path}: {}", describe(&e)))),
|
||||
},
|
||||
};
|
||||
|
||||
// Flush even on failure: a partial decode still writes its valid prefix,
|
||||
// and it must land before main() prints the error to stderr.
|
||||
let flushed = out.flush().map_err(write_failure);
|
||||
result.and(flushed)
|
||||
}
|
||||
|
||||
fn process<R: Read, W: Write>(input: R, out: &mut W, opts: &Options) -> Result<(), Fail> {
|
||||
if opts.decode {
|
||||
decode(input, out, opts.ignore_garbage)
|
||||
} else {
|
||||
encode(input, out, opts.wrap)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Argument parsing ────────────────────────────────────────────────── */
|
||||
|
||||
enum Parsed {
|
||||
Run(Options),
|
||||
/// --help / --version: print this and stop.
|
||||
Exit(&'static str),
|
||||
}
|
||||
|
||||
fn parse_args(args: Vec<String>) -> Result<Parsed, Fail> {
|
||||
let mut opts = Options {
|
||||
decode: false,
|
||||
ignore_garbage: false,
|
||||
wrap: DEFAULT_WRAP,
|
||||
file: None,
|
||||
};
|
||||
let mut operands: Vec<String> = Vec::new();
|
||||
let mut only_operands = false;
|
||||
let mut it = args.into_iter().peekable();
|
||||
|
||||
while let Some(arg) = it.next() {
|
||||
if only_operands {
|
||||
operands.push(arg);
|
||||
} else if arg == "--" {
|
||||
only_operands = true;
|
||||
} else if let Some(long) = arg.strip_prefix("--") {
|
||||
// Split --wrap=76 into name and inline value.
|
||||
let (name, inline) = match long.split_once('=') {
|
||||
Some((n, v)) => (n, Some(v.to_string())),
|
||||
None => (long, None),
|
||||
};
|
||||
match resolve_long(name)? {
|
||||
"help" => return Ok(Parsed::Exit(HELP)),
|
||||
"version" => return Ok(Parsed::Exit(VERSION)),
|
||||
"decode" => opts.decode = true,
|
||||
"ignore-garbage" => opts.ignore_garbage = true,
|
||||
"wrap" => {
|
||||
let value = match inline.or_else(|| it.next()) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(Fail::Usage(format!("option '--{name}' requires an argument")))
|
||||
}
|
||||
};
|
||||
opts.wrap = parse_wrap(&value)?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if arg.len() > 1 && arg.starts_with('-') {
|
||||
// Short flags, clusterable: -di, -dw0, -w76
|
||||
let chars: Vec<char> = arg.chars().collect();
|
||||
let mut idx = 1;
|
||||
while idx < chars.len() {
|
||||
match chars[idx] {
|
||||
'd' => opts.decode = true,
|
||||
'i' => opts.ignore_garbage = true,
|
||||
'w' => {
|
||||
// Rest of this argument is the value, else the next one.
|
||||
let rest: String = chars[idx + 1..].iter().collect();
|
||||
let value = if rest.is_empty() {
|
||||
match it.next() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(Fail::Usage(
|
||||
"option requires an argument -- 'w'".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rest
|
||||
};
|
||||
opts.wrap = parse_wrap(&value)?;
|
||||
idx = chars.len();
|
||||
continue;
|
||||
}
|
||||
c => return Err(Fail::Usage(format!("invalid option -- '{c}'"))),
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
} else {
|
||||
operands.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if operands.len() > 1 {
|
||||
return Err(Fail::Usage(format!("extra operand '{}'", operands[1])));
|
||||
}
|
||||
opts.file = operands.into_iter().next();
|
||||
Ok(Parsed::Run(opts))
|
||||
}
|
||||
|
||||
/// GNU accepts any unambiguous abbreviation of a long option.
|
||||
fn resolve_long(name: &str) -> Result<&'static str, Fail> {
|
||||
let hits: Vec<&'static str> = LONG_OPTS
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|o| o.starts_with(name))
|
||||
.collect();
|
||||
match hits.len() {
|
||||
1 => Ok(hits[0]),
|
||||
0 => Err(Fail::Usage(format!("unrecognized option '--{name}'"))),
|
||||
_ => Err(Fail::Usage(format!(
|
||||
"option '--{name}' is ambiguous; possibilities: {}",
|
||||
hits.iter()
|
||||
.map(|o| format!("'--{o}'"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unlike the other usage errors, coreutils prints this one without the
|
||||
/// "Try --help" hint.
|
||||
fn parse_wrap(value: &str) -> Result<usize, Fail> {
|
||||
value
|
||||
.parse::<usize>()
|
||||
.map_err(|_| Fail::Error(format!("invalid wrap size: '{value}'")))
|
||||
}
|
||||
|
||||
/* ── Encode ──────────────────────────────────────────────────────────── */
|
||||
|
||||
fn encode<R: Read, W: Write>(mut input: R, out: &mut W, wrap: usize) -> Result<(), Fail> {
|
||||
let mut inbuf = vec![0u8; 48 * 1024];
|
||||
let mut pending: Vec<u8> = Vec::with_capacity(3);
|
||||
let mut line: Vec<u8> = Vec::with_capacity(64 * 1024);
|
||||
let mut col = 0usize;
|
||||
let mut any = false;
|
||||
|
||||
loop {
|
||||
let n = input.read(&mut inbuf).map_err(read_failure)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
any = true;
|
||||
pending.extend_from_slice(&inbuf[..n]);
|
||||
|
||||
let whole = pending.len() - pending.len() % 3;
|
||||
for group in pending[..whole].chunks_exact(3) {
|
||||
emit(&mut line, encode_triple(group[0], group[1], group[2]), wrap, &mut col);
|
||||
}
|
||||
pending.drain(..whole);
|
||||
|
||||
if line.len() >= 32 * 1024 {
|
||||
out.write_all(&line).map_err(write_failure)?;
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Tail of 1 or 2 bytes gets padded out to a full quad.
|
||||
match pending.len() {
|
||||
1 => {
|
||||
let q = encode_triple(pending[0], 0, 0);
|
||||
emit(&mut line, [q[0], q[1], PAD, PAD], wrap, &mut col);
|
||||
}
|
||||
2 => {
|
||||
let q = encode_triple(pending[0], pending[1], 0);
|
||||
emit(&mut line, [q[0], q[1], q[2], PAD], wrap, &mut col);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// The trailing newline is a line terminator, so it appears only when
|
||||
// wrapping is on and something was actually written.
|
||||
if any && col != 0 && wrap != 0 {
|
||||
line.push(b'\n');
|
||||
}
|
||||
out.write_all(&line).map_err(write_failure)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_triple(a: u8, b: u8, c: u8) -> [u8; 4] {
|
||||
let n = ((a as u32) << 16) | ((b as u32) << 8) | c as u32;
|
||||
[
|
||||
ALPHABET[(n >> 18) as usize & 63],
|
||||
ALPHABET[(n >> 12) as usize & 63],
|
||||
ALPHABET[(n >> 6) as usize & 63],
|
||||
ALPHABET[n as usize & 63],
|
||||
]
|
||||
}
|
||||
|
||||
fn emit(line: &mut Vec<u8>, quad: [u8; 4], wrap: usize, col: &mut usize) {
|
||||
for ch in quad {
|
||||
if wrap != 0 && *col == wrap {
|
||||
line.push(b'\n');
|
||||
*col = 0;
|
||||
}
|
||||
line.push(ch);
|
||||
*col += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Decode ──────────────────────────────────────────────────────────── */
|
||||
|
||||
fn decode<R: Read, W: Write>(mut input: R, out: &mut W, ignore_garbage: bool) -> Result<(), Fail> {
|
||||
let mut obuf: Vec<u8> = Vec::with_capacity(48 * 1024);
|
||||
let outcome = decode_stream(&mut input, out, &mut obuf, ignore_garbage);
|
||||
// Whatever decoded cleanly before the failure still gets written, which
|
||||
// is what coreutils does — the error only stops further output.
|
||||
let flushed = out.write_all(&obuf).map_err(write_failure);
|
||||
outcome.and(flushed)
|
||||
}
|
||||
|
||||
/// Bytes are emitted as soon as enough bits are in hand (one byte after two
|
||||
/// digits, another after three, the last on a full quad), so a stream that
|
||||
/// turns bad part-way still produces the valid prefix.
|
||||
fn decode_stream<R: Read, W: Write>(
|
||||
input: &mut R,
|
||||
out: &mut W,
|
||||
obuf: &mut Vec<u8>,
|
||||
ignore_garbage: bool,
|
||||
) -> Result<(), Fail> {
|
||||
let mut inbuf = vec![0u8; 64 * 1024];
|
||||
let mut d = [0u8; 3];
|
||||
let mut have = 0usize;
|
||||
// A quad holding two digits needs '=' twice; this tracks the second.
|
||||
let mut need_pad = false;
|
||||
|
||||
loop {
|
||||
let n = input.read(&mut inbuf).map_err(read_failure)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
for &byte in &inbuf[..n] {
|
||||
if byte == PAD {
|
||||
if need_pad {
|
||||
need_pad = false;
|
||||
have = 0;
|
||||
continue;
|
||||
}
|
||||
match have {
|
||||
2 if d[1] & 0x0F == 0 => need_pad = true,
|
||||
3 if d[2] & 0x03 == 0 => have = 0,
|
||||
_ => return Err(invalid()),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let digit = DECODE[byte as usize];
|
||||
if digit < 0 {
|
||||
// Line endings are always skipped so CRLF files work; other
|
||||
// junk is only tolerated under --ignore-garbage.
|
||||
if byte == b'\n' || byte == b'\r' || ignore_garbage {
|
||||
continue;
|
||||
}
|
||||
return Err(invalid());
|
||||
}
|
||||
if need_pad {
|
||||
return Err(invalid());
|
||||
}
|
||||
|
||||
let v = digit as u8;
|
||||
match have {
|
||||
0 => {
|
||||
d[0] = v;
|
||||
have = 1;
|
||||
}
|
||||
1 => {
|
||||
d[1] = v;
|
||||
have = 2;
|
||||
obuf.push((d[0] << 2) | (d[1] >> 4));
|
||||
}
|
||||
2 => {
|
||||
d[2] = v;
|
||||
have = 3;
|
||||
obuf.push(((d[1] & 0x0F) << 4) | (d[2] >> 2));
|
||||
}
|
||||
_ => {
|
||||
obuf.push(((d[2] & 0x03) << 6) | v);
|
||||
have = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obuf.len() >= 32 * 1024 {
|
||||
out.write_all(obuf).map_err(write_failure)?;
|
||||
obuf.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if need_pad {
|
||||
return Err(invalid());
|
||||
}
|
||||
// An unpadded tail is only legal if the bits that would have been
|
||||
// dropped are zero, i.e. the input was canonically encoded.
|
||||
match have {
|
||||
0 => Ok(()),
|
||||
2 if d[1] & 0x0F == 0 => Ok(()),
|
||||
3 if d[2] & 0x03 == 0 => Ok(()),
|
||||
_ => Err(invalid()),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid() -> Fail {
|
||||
Fail::Error("invalid input".to_string())
|
||||
}
|
||||
|
||||
/* ── I/O error mapping ───────────────────────────────────────────────── */
|
||||
|
||||
fn read_failure(e: io::Error) -> Fail {
|
||||
Fail::Error(format!("read error: {}", describe(&e)))
|
||||
}
|
||||
|
||||
fn write_failure(e: io::Error) -> Fail {
|
||||
match e.kind() {
|
||||
// `base64 big.bin | head` closes the pipe under us; exit quietly.
|
||||
ErrorKind::BrokenPipe => Fail::Quiet,
|
||||
// Windows consoles take UTF-16 text, so raw bytes cannot be printed
|
||||
// to one. Redirecting to a file or piping sidesteps it entirely.
|
||||
ErrorKind::InvalidData => Fail::Error(
|
||||
"cannot write binary output to the console; redirect to a file (base64 -d in.txt > out.bin)"
|
||||
.to_string(),
|
||||
),
|
||||
_ => Fail::Error(format!("write error: {}", describe(&e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Trims Rust's "(os error N)" suffix so messages read like coreutils'.
|
||||
fn describe(e: &io::Error) -> String {
|
||||
match e.kind() {
|
||||
ErrorKind::NotFound => "No such file or directory".to_string(),
|
||||
ErrorKind::PermissionDenied => "Permission denied".to_string(),
|
||||
ErrorKind::IsADirectory => "Is a directory".to_string(),
|
||||
_ => {
|
||||
let s = e.to_string();
|
||||
match s.find(" (os error ") {
|
||||
Some(i) => s[..i].to_string(),
|
||||
None => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user