///|
priv suberror CliError {
CliError(String)
}
///|
fn hex_digit(n : Int) -> Char {
if n < 10 {
(0x30 + n).unsafe_to_char()
} else {
(0x61 + n - 10).unsafe_to_char()
}
}
///|
fn write_hex_byte(sb : StringBuilder, b : Byte) -> Unit {
sb.write_char(hex_digit(b.to_int() >> 4))
sb.write_char(hex_digit(b.to_int() & 0xF))
}
///|
fn hex_val(byte : Byte) -> Int {
let value = byte.to_int()
if value >= 0x30 && value <= 0x39 {
value - 0x30
} else if value >= 0x61 && value <= 0x66 {
value - 0x61 + 10
} else if value >= 0x41 && value <= 0x46 {
value - 0x41 + 10
} else {
-1
}
}
///|
fn dump_line(data : ArrayView[Byte], cols : Int, offset : Int) -> String {
let sb = StringBuilder()
for k in 0..<8 {
sb.write_char(hex_digit((offset >> ((7 - k) * 4)) & 0xF))
}
sb.write_string(": ")
for index in 0.. 0 && index % 2 == 0 {
sb.write_char(' ')
}
if index < data.length() {
write_hex_byte(sb, data[index])
} else {
sb.write_string(" ")
}
}
sb.write_string(" ")
for byte in data {
let code = byte.to_int()
if code >= 0x20 && code <= 0x7E {
sb.write_char(code.unsafe_to_char())
} else {
sb.write_char('.')
}
}
sb.write_char('\n')
sb.to_string()
}
///|
fn limited_length(length : Int, remaining : Int?) -> Int {
match remaining {
Some(value) => if value < length { value } else { length }
None => length
}
}
///|
async fn dump_stream(
reader : &@io.Reader,
plain : Bool,
cols : Int,
limit : Int?,
) -> Unit {
let pending : Array[Byte] = []
let mut offset = 0
let mut plain_column = 0
let mut remaining = limit
while remaining != Some(0) && @stream.read_chunk(reader) is Some(chunk) {
let take = limited_length(chunk.length(), remaining)
if remaining is Some(value) {
remaining = Some(value - take)
}
if plain {
let out = StringBuilder()
for byte in chunk[0:take] {
write_hex_byte(out, byte)
plain_column += 1
if plain_column == cols {
out.write_char('\n')
plain_column = 0
}
}
@stdio.stdout.write(out.to_string())
} else {
let data : Array[Byte] = []
data.append(pending)
pending.clear()
for byte in chunk[0:take] {
data.push(byte)
}
let mut index = 0
while index + cols <= data.length() {
@stdio.stdout.write(dump_line(data[index:index + cols], cols, offset))
index += cols
offset += cols
}
for byte in data[index:] {
pending.push(byte)
}
}
}
if plain {
if plain_column != 0 {
@stdio.stdout.write("\n")
}
} else if !pending.is_empty() {
@stdio.stdout.write(dump_line(pending, cols, offset))
}
}
///|
fn reverse_dump_line(line : Bytes) -> Bytes {
let out : Array[Byte] = []
let mut i = 0
while i < line.length() && line[i] != b':' {
i += 1
}
if i >= line.length() {
return b""
}
i += 1
while i < line.length() {
if line[i] == b' ' {
if i + 1 < line.length() && line[i + 1] == b' ' {
break
}
i += 1
continue
}
if i + 1 >= line.length() {
break
}
let high = hex_val(line[i])
let low = hex_val(line[i + 1])
if high < 0 || low < 0 {
break
}
out.push(((high << 4) | low).to_byte())
i += 2
}
Bytes::from_array(out)
}
///|
async fn reverse_plain_stream(reader : &@io.Reader) -> Unit {
let mut high = -1
while @stream.read_chunk(reader) is Some(chunk) {
let out : Array[Byte] = []
for byte in chunk {
if byte is (b' ' | b'\t' | b'\n' | b'\r') {
continue
}
let value = hex_val(byte)
if value < 0 {
raise CliError(
"xxd: invalid hex character: '\{byte.to_int().unsafe_to_char()}'",
)
}
if high < 0 {
high = value
} else {
out.push(((high << 4) | value).to_byte())
high = -1
}
}
if !out.is_empty() {
@stdio.stdout.write(Bytes::from_array(out))
}
}
if high >= 0 {
raise CliError("xxd: odd number of hex digits")
}
}
///|
async fn reverse_dump_stream(reader : &@io.Reader) -> Unit {
let scanner = @stream.LineScanner::new(reader)
while scanner.next() is Some(line) {
@stdio.stdout.write(reverse_dump_line(line.data))
}
}
///|
fn parse_number(
parsed : @cli.ParsedArgs,
name : String,
minimum? : Int = 1,
) -> Int? raise CliError {
match parsed.last_value(name) {
Some(text) => {
let n = @string.parse_int(text) catch {
_ => raise CliError("xxd: invalid -\{name} value: '\{text}'")
}
if n < minimum {
raise CliError("xxd: invalid -\{name} value: '\{text}'")
}
Some(n)
}
None => None
}
}
///|
async fn main {
let args = @env.args()[1:]
let parsed = @cli.parse(args, [
@cli.flag("plain", short='p'),
@cli.flag("reverse", short='r'),
@cli.option("cols", short='c'),
@cli.option("len", short='l'),
@cli.flag("help"),
]) catch {
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("xxd: \{message}: '\{option}'\n")
@sys.exit(2)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write("Usage: xxd [-pr] [-c COLS] [-l LEN] [FILE]\n")
return
}
if parsed.operands.length() > 1 {
@stdio.stderr.write("xxd: extra operand: '\{parsed.operands[1]}'\n")
@sys.exit(2)
return
}
let plain = parsed.contains("plain")
let reverse = parsed.contains("reverse")
let (cols_opt, len_opt) = (
parse_number(parsed, "cols"),
parse_number(parsed, "len", minimum=0),
) catch {
CliError(msg) => {
@stdio.stderr.write("\{msg}\n")
@sys.exit(2)
return
}
}
let cols = match cols_opt {
Some(n) => n
None => if plain { 30 } else { 16 }
}
let inputs = parsed.operands
let path = if inputs.is_empty() { "-" } else { inputs[0] }
try {
if path == "-" {
if reverse {
if plain {
reverse_plain_stream(@stdio.stdin)
} else {
reverse_dump_stream(@stdio.stdin)
}
} else {
dump_stream(@stdio.stdin, plain, cols, len_opt)
}
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
if reverse {
if plain {
reverse_plain_stream(file)
} else {
reverse_dump_stream(file)
}
} else {
dump_stream(file, plain, cols, len_opt)
}
}
} catch {
CliError(msg) => {
@stdio.stderr.write("\{msg}\n")
@sys.exit(1)
return
}
err => {
@stdio.stderr.write("xxd: \{err}\n")
@sys.exit(1)
return
}
}
}