diff --git a/libc/src/stdio/printf_core/converter.cpp b/libc/src/stdio/printf_core/converter.cpp --- a/libc/src/stdio/printf_core/converter.cpp +++ b/libc/src/stdio/printf_core/converter.cpp @@ -49,7 +49,7 @@ return; case 'x': case 'X': - // convert_hex(writer, to_conv); + convert_hex(writer, to_conv); return; // TODO(michaelrj): add a flag to disable float point values here case 'f': diff --git a/libc/src/stdio/printf_core/converter_atlas.h b/libc/src/stdio/printf_core/converter_atlas.h --- a/libc/src/stdio/printf_core/converter_atlas.h +++ b/libc/src/stdio/printf_core/converter_atlas.h @@ -22,6 +22,7 @@ // defines convert_int // defines convert_oct // defines convert_hex +#include "src/stdio/printf_core/hex_converter.h" // TODO(michaelrj): add a flag to disable float point values here // defines convert_float_decimal diff --git a/libc/src/stdio/printf_core/hex_converter.h b/libc/src/stdio/printf_core/hex_converter.h new file mode 100644 --- /dev/null +++ b/libc/src/stdio/printf_core/hex_converter.h @@ -0,0 +1,160 @@ +//===-- Hexadecimal Converter for printf ------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/stdio/printf_core/core_structs.h" +#include "src/stdio/printf_core/writer.h" + +#include +#include + +namespace __llvm_libc { +namespace printf_core { + +template constexpr uintmax_t type_mask() { + if (sizeof(T) == sizeof(uintmax_t)) + return ~uintmax_t(0); + else + return (uintmax_t(1) << (sizeof(T) * 8)) - 1; +} + +void convert_hex(Writer *writer, FormatSection to_conv) { + // This approximates the number of digits it takes to represent a hexadecimal + // value of a certain number of bits. Each hex digit represents 4 bits, so the + // exact value is the number of bytes multiplied by 2. + static constexpr size_t BUFF_LEN = sizeof(uintmax_t) * 2; + uintmax_t num = to_conv.conv_val_raw; + char buffer[BUFF_LEN]; + + // All of the characters will be defined relative to variable a, which will be + // the appropriate case based on the name of the conversion. + char a; + if (to_conv.conv_name == 'x') + a = 'a'; + else + a = 'A'; + + switch (to_conv.length_modifier) { + case LengthModifier::none: + num = num & type_mask(); + break; + + case LengthModifier::l: + num = num & type_mask(); + break; + case LengthModifier::ll: + case LengthModifier::L: + num = num & type_mask(); + break; + case LengthModifier::h: + num = num & type_mask(); + break; + case LengthModifier::hh: + num = num & type_mask(); + break; + case LengthModifier::z: + num = num & type_mask(); + break; + case LengthModifier::t: + num = num & type_mask(); + break; + case LengthModifier::j: + // j is intmax, so no mask is necessary. + break; + } + + // buff_cur can never reach 0, since the buffer is sized to always be able to + // contain the whole integer. This means that bounds checking it should be + // unnecessary. + size_t buff_cur = BUFF_LEN; + for (; num > 0 /* && buff_cur > 0 */; --buff_cur, num /= 16) + buffer[buff_cur - 1] = + ((num % 16) > 9) ? ((num % 16) - 10 + a) : ((num % 16) + '0'); + + size_t digits_written = BUFF_LEN - buff_cur; + + // these are signed to prevent underflow due to negative values. The eventual + // values will always be non-negative. + int zeroes; + int spaces; + + // prefix is "0x" + int prefix_len; + char prefix[2]; + if ((to_conv.flags & FormatFlags::ALTERNATE_FORM) == + FormatFlags::ALTERNATE_FORM) { + prefix_len = 2; + prefix[0] = '0'; + prefix[1] = a + ('x' - 'a'); + } else { + prefix_len = 0; + prefix[0] = 0; + } + + // negative precision indicates that it was not specified. + if (to_conv.precision < 0) { + if ((to_conv.flags & + (FormatFlags::LEADING_ZEROES | FormatFlags::LEFT_JUSTIFIED)) == + FormatFlags::LEADING_ZEROES) { + // if this conv has flag 0 but not - and no specified precision, it's + // padded with 0's instead of spaces identically to if precision = + // min_width - (2 if prefix). For example: ("%#04x", 15) -> "0x0f" + zeroes = to_conv.min_width - digits_written - prefix_len; + if (zeroes < 0) + zeroes = 0; + spaces = 0; + } else if (digits_written < 1) { + // if no precision is specified, precision defaults to 1. This means that + // if the integer passed to the conversion is 0, a 0 will be printed. + // Example: ("%3x", 0) -> " 0" + zeroes = 1; + spaces = to_conv.min_width - zeroes - prefix_len; + } else { + // If there are enough digits to pass over the precision, just write the + // number, padded by spaces. + zeroes = 0; + spaces = to_conv.min_width - digits_written - prefix_len; + } + } else { + // if precision was specified, possibly write zeroes, and possibly write + // spaces. Example: ("%5.4x", 0x10000) -> "10000" + // If the check for if zeroes is negative was not there, spaces would be + // incorrectly evaluated as 1. + zeroes = to_conv.precision - digits_written; // a negative value means 0 + if (zeroes < 0) + zeroes = 0; + spaces = to_conv.min_width - zeroes - digits_written - prefix_len; + } + if (spaces < 0) + spaces = 0; + + if ((to_conv.flags & FormatFlags::LEFT_JUSTIFIED) == + FormatFlags::LEFT_JUSTIFIED) { + // if left justified it goes prefix zeroes digits spaces + if (prefix[0] != 0) + writer->write(prefix, 2); + if (zeroes > 0) + writer->write_chars('0', zeroes); + if (digits_written > 0) + writer->write(buffer + buff_cur, digits_written); + if (spaces > 0) + writer->write_chars(' ', spaces); + } else { + // else it goes spaces prefix zeroes digits + if (spaces > 0) + writer->write_chars(' ', spaces); + if (prefix[0] != 0) + writer->write(prefix, 2); + if (zeroes > 0) + writer->write_chars('0', zeroes); + if (digits_written > 0) + writer->write(buffer + buff_cur, digits_written); + } +} + +} // namespace printf_core +} // namespace __llvm_libc diff --git a/libc/test/src/stdio/printf_core/converter_test.cpp b/libc/test/src/stdio/printf_core/converter_test.cpp --- a/libc/test/src/stdio/printf_core/converter_test.cpp +++ b/libc/test/src/stdio/printf_core/converter_test.cpp @@ -187,3 +187,31 @@ ASSERT_STREQ(str, "DEF456xy 789ghi "); ASSERT_EQ(writer.get_chars_written(), 16ull); } + +// This needs to be switched to the new testing layout, but that's still in the +// int patch so I need to land that first. This is what I get for not keeping my +// patches small and focused. +TEST(LlvmLibcPrintfConverterTest, HexConversion) { + char str[20]; + __llvm_libc::printf_core::StringWriter str_writer(str); + __llvm_libc::printf_core::Writer writer( + reinterpret_cast(&str_writer), + __llvm_libc::printf_core::write_to_string); + + __llvm_libc::printf_core::FormatSection left_justified_conv; + left_justified_conv.has_conv = true; + left_justified_conv.raw_string = "%#018x"; + left_justified_conv.raw_len = 6; + left_justified_conv.conv_name = 'x'; + left_justified_conv.flags = + static_cast<__llvm_libc::printf_core::FormatFlags>( + __llvm_libc::printf_core::FormatFlags::ALTERNATE_FORM | + __llvm_libc::printf_core::FormatFlags::LEADING_ZEROES); + left_justified_conv.min_width = 18; + left_justified_conv.conv_val_raw = 0x123456ab; + __llvm_libc::printf_core::convert(&writer, left_justified_conv); + + str_writer.terminate(); + ASSERT_STREQ(str, "0x00000000123456ab"); + ASSERT_EQ(writer.get_chars_written(), 18ull); +}