Skip to content

Commit

Permalink
Utilities: Add the xxd utility
Browse files Browse the repository at this point in the history
  • Loading branch information
Ollrogge committed Jan 17, 2024
1 parent bdf8acd commit ff41d93
Show file tree
Hide file tree
Showing 2 changed files with 374 additions and 0 deletions.
1 change: 1 addition & 0 deletions Userland/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ target_link_libraries(wasm PRIVATE LibFileSystem LibJS LibLine LibWasm)
target_link_libraries(watch PRIVATE LibFileSystem)
target_link_libraries(wsctl PRIVATE LibGUI LibIPC)
target_link_libraries(xml PRIVATE LibFileSystem LibXML)
target_link_libraries(xxd PRIVATE LibUnicode )
target_link_libraries(xzcat PRIVATE LibCompress)
target_link_libraries(zip PRIVATE LibArchive LibFileSystem)

Expand Down
373 changes: 373 additions & 0 deletions Userland/Utilities/xxd.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
/*
* Copyright (c) 2020-2024, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/AllOf.h>
#include <AK/CharacterTypes.h>
#include <AK/StdLibExtras.h>
#include <AK/String.h>
#include <AK/StringUtils.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibMain/Main.h>

static constexpr size_t BYTES_PER_LINE_HEX = 16;
static constexpr size_t BYTES_PER_LINE_C = 12;
static constexpr size_t BYTES_PER_LINE_BITS = 6;
static constexpr size_t BYTES_PER_LINE_PLAIN_HEX = 30;
static constexpr size_t BYTES_PER_LINE_MAX = 256;

static constexpr size_t GROUP_SIZE_HEX = 2;
static constexpr size_t GROUP_SIZE_HEX_LITTLE_ENDIAN = 4;
static constexpr size_t GROUP_SIZE_BITS = 1;
static constexpr size_t GROUP_SIZE_PLAIN_HEX = 0;

enum class DisplayStyle {
Hex,
PlainHex,
HexLittleEndian,
CStyle,
Bits
};

static void print_ascii(Bytes line)
{
for (auto const& byte : line) {
if (is_ascii_printable(byte)) {
putchar(byte);
} else {
putchar('.');
}
}
}

static void print_line_hex(Bytes line, size_t line_length_config, size_t group_size, bool uppercase)
{
for (size_t i = 0; i < line_length_config; ++i) {
if (i < line.size()) {
if (uppercase) {
out("{:02X}", line[i]);
} else {

out("{:02x}", line[i]);
}
} else {
out(" ");
}

if (group_size != 0 && (i + 1) % group_size == 0) {
out(" ");
}
}

out(" ");
}

static void print_line_little_endian_hex(Bytes line, size_t line_length_config, size_t group_size, bool uppercase)
{
if (group_size == 1) {
print_line_hex(line, line_length_config, group_size, uppercase);
return;
}

if (group_size == 0 || group_size > BYTES_PER_LINE_HEX) {
group_size = BYTES_PER_LINE_HEX;
}

for (size_t i = 0; i < line_length_config; i += group_size) {
if (i < line.size()) {
size_t size = i + group_size < line.size() ? group_size : line.size() - i;
auto group = line.slice(i, size);
if (size < group_size) {
for (size_t i = 0; i < group_size - size; ++i) {
out(" ");
}
}
for (ssize_t i = group.size() - 1; i >= 0; --i) {
if (uppercase) {
out("{:02X}", group[i]);
} else {

out("{:02x}", group[i]);
}
}
} else {
for (size_t i = 0; i < group_size; ++i) {
out(" ");
}
}

out(" ");
}

out(" ");
}

static void print_line_bits(Bytes line, size_t line_length_config, size_t group_size)
{
auto print_byte = [](u8 byte) {
for (ssize_t i = 7; i >= 0; --i) {
out("{}", (byte >> i) & 1 ? '1' : '0');
}
};

for (size_t i = 0; i < line_length_config; ++i) {
if (i < line.size()) {
print_byte(line[i]);
} else {
out(" ");
}

if (group_size > 0 && (i + 1) % group_size == 0) {
out(" ");
}
}

out(" ");
}

static void print_line_c_style(Bytes line)
{
out(" ");
for (size_t i = 0; i < line.size() - 1; ++i) {
out("0x{:02x}, ", line[i]);
}
out("0x{:02x}", line[line.size() - 1]);
}

static ErrorOr<String> path_to_variable_name(StringView path)
{
auto work = path.to_byte_string();

work = work.replace("."sv, "_"sv, ReplaceMode::All);
work = work.replace("/"sv, "_"sv, ReplaceMode::All);

return TRY(String::from_byte_string(work));
}

ErrorOr<int> serenity_main(Main::Arguments args)
{
TRY(Core::System::pledge("stdio rpath"));

Core::ArgsParser args_parser;
StringView path;
bool autoskip = false;
bool c_include_file_style = false;
bool capitalize_c_include_file_style = false;
bool binary_digit_formatting = false;
bool little_endian_hexdump = false;
bool offset_in_decimal = false;
bool plain_hexdump_style = false;
bool uppercase_hex = false;
bool revert = false;
Optional<size_t> line_length_option;
Optional<size_t> group_size_option;
Optional<size_t> max_bytes;
Optional<size_t> position_offset;
Optional<off_t> seek_to;
String c_include_file_style_variable_name;
StringView colorize_output_option;

args_parser.add_positional_argument(path, "Input file", "input", Core::ArgsParser::Required::No);
args_parser.add_option(autoskip, "Replace nul-lines with '*'", "autoskip", 'a');
args_parser.add_option(binary_digit_formatting, "Binary digit formatting", "bits", 'b');
args_parser.add_option(capitalize_c_include_file_style, "Capitalize C include file style (-i).", "capitalize", 'C');
args_parser.add_option(line_length_option, "Amount of bytes shown per line (max 256)", "cols", 'c', "cols");
args_parser.add_option(offset_in_decimal, "Show file offset in decimal", "decimal", 'd');
args_parser.add_option(little_endian_hexdump, "Little-endian hex dump", nullptr, 'e');
args_parser.add_option(group_size_option, "Separate the output of every amount bytes", "groupsize", 'g', "amount");
args_parser.add_option(c_include_file_style, "Output in C include file style", "include", 'i');
args_parser.add_option(max_bytes, "Truncate to fixed number of bytes", "len", 'l', "bytes");
args_parser.add_option(c_include_file_style_variable_name, "Set variable name used in C include ouput (-i)", "name", 'n', "include_style");
args_parser.add_option(position_offset, "Add offset to displayed file position", nullptr, 'o', "offset");
args_parser.add_option(plain_hexdump_style, "Output in plain hex dump style", "plain", 'p');
args_parser.add_option(revert, "Patch hex dump into binary", "revert", 'r');
args_parser.add_option(colorize_output_option, "Colorize output", nullptr, 'R', "when");
args_parser.add_option(seek_to, "Seek to a byte offset", "seek", 's', "[-]offset");
args_parser.add_option(uppercase_hex, "Use upper case hex letters", nullptr, 'u');

args_parser.parse(args);

auto file = TRY(Core::File::open_file_or_standard_stream(path, Core::File::OpenMode::Read));

auto display_style = DisplayStyle::Hex;
size_t line_length_config = BYTES_PER_LINE_HEX;
size_t group_size = GROUP_SIZE_HEX;

auto formatting_options_provided = 0x0;

if (c_include_file_style) {
formatting_options_provided++;
display_style = DisplayStyle::CStyle;
line_length_config = BYTES_PER_LINE_C;

if (c_include_file_style_variable_name.is_empty()) {
c_include_file_style_variable_name = TRY(path_to_variable_name(path));
}

if (capitalize_c_include_file_style) {
c_include_file_style_variable_name = TRY(c_include_file_style_variable_name.to_uppercase());
}

if (file->fd() != STDIN_FILENO) {
outln("u8 {}[] = {{", c_include_file_style_variable_name);
}
}

if (little_endian_hexdump) {
formatting_options_provided++;
display_style = DisplayStyle::HexLittleEndian;
group_size = GROUP_SIZE_HEX_LITTLE_ENDIAN;
}

if (plain_hexdump_style) {
formatting_options_provided++;
display_style = DisplayStyle::PlainHex;
line_length_config = BYTES_PER_LINE_PLAIN_HEX;
group_size = GROUP_SIZE_PLAIN_HEX;
}

if (binary_digit_formatting) {
formatting_options_provided++;
display_style = DisplayStyle::Bits;
group_size = GROUP_SIZE_BITS;
line_length_config = BYTES_PER_LINE_BITS;
}

if (formatting_options_provided > 1) {
warnln("Only one of the following flags can be used at a time: -i, -e, -p, -b");
return 1;
}

if (line_length_option.has_value() && line_length_option.value() > 0) {
line_length_config = line_length_option.value();

if (line_length_config > BYTES_PER_LINE_MAX && !plain_hexdump_style) {
warnln("Invalid number of columns (max is 256).");
return 1;
}
}

if (group_size_option.has_value()) {
group_size = group_size_option.value();

if (little_endian_hexdump) {
if (group_size != 0 && !is_power_of_two(group_size)) {
warnln("Group size must be a power of 2 with -e");
return 1;
}
}
}

// TODO: colorize output
if (!colorize_output_option.is_null()) {
warnln("Colorizing output is not supported");
}

if (revert) {
warnln("Patching is not supported");
return 1;
}

Array<u8, BUFSIZ> contents;
Bytes bytes;
size_t total_bytes_read = 0x0;
const size_t max_read_size = contents.size() - (contents.size() % line_length_config);
bool is_input_remaining = true;

// TODO: seek relative to current stdin file position
if (seek_to.has_value()) {
auto file_size = 0x0;
if (auto size = file->size(); !size.is_error()) {
file_size = size.value();
}

auto offset = seek_to.value();
total_bytes_read = offset < 0 ? file_size + offset : offset;
TRY(file->seek(total_bytes_read, SeekMode::SetPosition));
}

while (is_input_remaining) {
auto bytes_to_read = max_read_size - bytes.size();

bytes = contents.span().slice(0, bytes_to_read);
bytes = TRY(file->read_some(bytes));

if (bytes.size() < bytes_to_read && file->fd() != STDIN_FILENO) {
is_input_remaining = false;
}

while (bytes.size() > 0) {
auto line_length = bytes.size() > line_length_config ? line_length_config : bytes.size();

if (max_bytes.has_value()) {
auto bytes_remaining = max_bytes.value() - total_bytes_read;
if (bytes_remaining < line_length) {
line_length = bytes_remaining;
}
}

auto current_line = bytes.slice(0, line_length);
bytes = bytes.slice(line_length);

if (autoskip && all_of(bytes, [](auto& b) { return b == 0x0; })) {
outln("*");
continue;
}

if (display_style != DisplayStyle::CStyle && display_style != DisplayStyle::PlainHex) {
auto offset = 0;
if (position_offset.has_value()) {
offset = position_offset.value();
}

if (offset_in_decimal) {
out("{:08}: ", total_bytes_read + offset);
} else {
out("{:08x}: ", total_bytes_read + offset);
}
}

switch (display_style) {
case DisplayStyle::Hex:
print_line_hex(current_line, line_length_config, group_size, uppercase_hex);
print_ascii(current_line);
break;
case DisplayStyle::PlainHex:
print_line_hex(current_line, line_length_config, group_size, uppercase_hex);
break;
case DisplayStyle::HexLittleEndian:
print_line_little_endian_hex(current_line, line_length_config, group_size, uppercase_hex);
print_ascii(current_line);
break;
case DisplayStyle::Bits:
print_line_bits(current_line, line_length_config, group_size);
print_ascii(current_line);
break;
case DisplayStyle::CStyle:
print_line_c_style(current_line);
break;
}

putchar('\n');

total_bytes_read += line_length_config;

if (max_bytes.has_value() && total_bytes_read >= max_bytes.value()) {
is_input_remaining = false;
break;
}
}
}

if (display_style == DisplayStyle::CStyle) {
outln("}};");
auto postfix = capitalize_c_include_file_style ? "LEN" : "len";
outln("size_t {}_{} = {};", c_include_file_style_variable_name, postfix, total_bytes_read);
}

return 0;
}

0 comments on commit ff41d93

Please sign in to comment.