///|
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?,
start_offset : Int64,
) -> Unit {
let pending : Array[Byte] = []
let mut offset = start_offset.to_int()
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) -> (Int, Bytes)? {
let out : Array[Byte] = []
if line.length() < 9 {
return None
}
let mut offset = 0
for index in 0..<8 {
let value = hex_val(line[index])
if value < 0 {
return None
}
offset = (offset << 4) | value
}
let mut i = 8
while i < line.length() && line[i] != b':' {
i += 1
}
if i >= line.length() {
return None
}
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() {
return None
}
let high = hex_val(line[i])
let low = hex_val(line[i + 1])
if high < 0 || low < 0 {
return None
}
out.push(((high << 4) | low).to_byte())
i += 2
}
Some((offset, Bytes::from_array(out)))
}
///|
fn loose_reverse_offset(line : Bytes) -> Int? {
let mut started = false
let mut offset = 0
let mut delimiter = -1
for index, byte in line {
let value = hex_val(byte)
if !started {
if value >= 0 {
started = true
offset = value
}
} else if delimiter < 0 {
if value >= 0 {
if offset > 16 * 1024 * 1024 / 16 {
return Some(16 * 1024 * 1024 + 1)
}
offset = (offset << 4) | value
} else {
delimiter = index
}
}
}
if delimiter >= 0 && delimiter + 1 < line.length() {
Some(offset)
} else {
None
}
}
///|
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)
let output : Array[Byte] = []
while scanner.next() is Some(line) {
match reverse_dump_line(line.data) {
Some((line_offset, bytes)) => {
if line_offset > 16 * 1024 * 1024 {
raise CliError("xxd: addressed offset is too large")
}
let mut offset = line_offset
while output.length() < offset {
output.push(b'\x00')
}
for byte in bytes {
if offset < output.length() {
output[offset] = byte
} else {
output.push(byte)
}
offset += 1
}
}
None =>
if loose_reverse_offset(line.data) is Some(line_offset) {
if line_offset > 16 * 1024 * 1024 {
raise CliError("xxd: addressed offset is too large")
}
while output.length() < line_offset {
output.push(b'\x00')
}
}
}
}
@stdio.stdout.write(Bytes::from_array(output))
}
///|
fn include_name(path : String, symbol_name : String?) -> String {
if symbol_name is Some(value) {
return value
}
let raw = if path == "-" { "" } else { path }
let normalized = StringBuilder()
for char in raw {
if char is ('a'..='z' | 'A'..='Z' | '0'..='9' | '_') {
normalized.write_char(char)
} else {
normalized.write_char('_')
}
}
let raw = normalized.to_string()
let result = raw
if result == "" {
"data"
} else {
result
}
}
///|
fn include_output(path : String, data : Bytes, symbol_name : String?) -> String {
if path == "-" && symbol_name is None {
let out = StringBuilder()
for index, byte in data {
if index == 0 {
out.write_string(" 0x")
} else {
out.write_string(", 0x")
}
write_hex_byte(out, byte)
}
if !data.is_empty() {
out.write_char('\n')
}
return out.to_string()
}
let name = include_name(path, symbol_name)
let out = StringBuilder()
out.write_string("unsigned char \{name}[] = {")
for index, byte in data {
if index % 12 == 0 {
out.write_string("\n ")
}
out.write_string("0x")
write_hex_byte(out, byte)
if index + 1 < data.length() {
out.write_string(", ")
}
}
if !data.is_empty() {
out.write_char('\n')
}
out.write_string("};\nunsigned int \{name}_len = \{data.length()};\n")
out.to_string()
}
///|
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
}
}
///|
fn parse_seek(parsed : @cli.ParsedArgs) -> Int64? raise CliError {
match parsed.last_value("seek") {
Some(text) => {
let value = @string.parse_int64(text) catch {
_ => raise CliError("xxd: invalid seek offset: '\{text}'")
}
if value < 0L {
raise CliError("xxd: negative seek offset is not supported")
}
if value > 2147483647L {
raise CliError("xxd: seek offset is too large for this target")
}
Some(value)
}
None => None
}
}
///|
async fn skip_bytes(reader : &@io.Reader, count : Int64) -> Int64 {
let buffer : FixedArray[Byte] = FixedArray::make(65536, 0)
let mut remaining = count
while remaining > 0L {
let limit = remaining.min(buffer.length().to_int64()).to_int()
let n = reader.read(buffer, max_len=limit)
if n == 0 {
break
}
remaining -= n.to_int64()
}
count - remaining
}
///|
async fn write_zero_prefix(count : Int64) -> Unit {
let mut remaining = count
while remaining > 0L {
let length = remaining.min(65536L).to_int()
@stdio.stdout.write(Bytes::make(length, b'\x00'))
remaining -= length.to_int64()
}
}
///|
async fn main {
let args = @env.args()[1:].map(arg => {
if arg == "--revert" {
"--reverse"
} else {
arg
}
})
let parsed = @cli.parse(args, [
@cli.flag("include", short='i'),
@cli.flag("plain", short='p'),
@cli.flag("reverse", short='r'),
@cli.option("cols", short='c'),
@cli.option("len", short='l'),
@cli.option("seek", short='s'),
@cli.option("name", short='n'),
@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 [-ipr] [-c COLS] [-l LEN] [-s OFFSET] [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 include_mode = parsed.contains("include")
let (cols_opt, len_opt, seek_opt) = (
parse_number(parsed, "cols"),
parse_number(parsed, "len", minimum=0),
parse_seek(parsed),
) 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 include_mode {
let data = if path == "-" {
@stdio.stdin.read_all().binary()
} else {
@fs.read_file(path).binary()
}
@stdio.stdout.write(include_output(path, data, parsed.last_value("name")))
} else if path == "-" {
if reverse {
if plain {
write_zero_prefix(seek_opt.unwrap_or(0L).max(0L))
reverse_plain_stream(@stdio.stdin)
} else {
reverse_dump_stream(@stdio.stdin)
}
} else {
let skipped = skip_bytes(@stdio.stdin, seek_opt.unwrap_or(0L))
dump_stream(@stdio.stdin, plain, cols, len_opt, skipped)
}
} else {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
if reverse {
if plain {
write_zero_prefix(seek_opt.unwrap_or(0L).max(0L))
reverse_plain_stream(file)
} else {
reverse_dump_stream(file)
}
} else {
let skipped = skip_bytes(file, seek_opt.unwrap_or(0L))
dump_stream(file, plain, cols, len_opt, skipped)
}
}
} catch {
CliError(msg) => {
@stdio.stderr.write("\{msg}\n")
@sys.exit(1)
return
}
err => {
@stdio.stderr.write("xxd: \{err}\n")
@sys.exit(1)
return
}
}
}