A size with a binary unit can only be printed to the console with
print_size(). Code which builds a string, such as a boot menu entry,
has to format the size itself.

Add a Z suffix to the %u conversion which prints the value as a size in
bytes with a binary unit, as print_size() does, so that sizes can be
printed with printf() and its variants:

        printf("%lluZ\n", 0x113000ULL);         /* 1 MiB */
        printf("%.3lluZ\n", 0x113000ULL);       /* 1.074 MiB */

The precision is the number of decimal places, with the last one rounded
and trailing zeros left out, so that "%.1lluZ" prints the same as
print_size(). The Z only has this meaning after %u, since an existing
format string has "%dZ" followed by other text. Compilers which check
format strings see the Z as an ordinary character, so this does not
cause warnings.

Document the format and add tests, including one which checks that
"%.1lluZ" prints the same as print_size() for a range of sizes.

Suggested-by: Heinrich Schuchardt <[email protected]>
Signed-off-by: Padmarao Begari <[email protected]>
---
Changes in v3:
- New patch, replacing "lib: display_options: add snprint_size()":
  print sizes with a Z suffix to the %u conversion, as suggested by
  Heinrich Schuchardt

checkpatch reports 'Please use a blank line after
function/struct/union/enum declarations' for each COMMON_TEST() line.
COMMON_TEST() is placed directly after its test function, as in the
other tests.

 doc/develop/printf.rst | 30 +++++++++++++
 lib/vsprintf.c         | 98 ++++++++++++++++++++++++++++++++++++++++++
 test/common/print.c    | 97 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 225 insertions(+)

diff --git a/doc/develop/printf.rst b/doc/develop/printf.rst
index 99d05061b14..33f2e027fa0 100644
--- a/doc/develop/printf.rst
+++ b/doc/develop/printf.rst
@@ -127,6 +127,36 @@ size_t              %zu, %zx, %zX
 ssize_t             %zd, %zx, %zX
 =================== ==================
 
+Sizes
+-----
+
+%uZ, %luZ, %lluZ, %zuZ
+       prints an unsigned integer as a size in bytes with a binary unit:
+       "xxx Bytes" below 1 KiB, otherwise "xxx KiB", "xxx MiB" and so on up
+       to "xxx EiB", using the largest unit which gives at least 1.
+
+       The precision is the number of decimal places, at most 9. The last
+       one is rounded and trailing zeros are left out. Without a precision,
+       the size is rounded to a whole number of the unit. A field width pads
+       the whole string with spaces.
+
+       "%.1lluZ" prints a size the same way as print_size().
+
+       The Z only has this meaning after %u. After other conversions, such
+       as %d or %x, it is printed as a normal character. The SPL and TPL
+       tiny printf() does not support it.
+
+=================== ======================= ==============
+Format              Value                   Output
+=================== ======================= ==============
+%lluZ               0x113000                1 MiB
+%.1lluZ             0x113000                1.1 MiB
+%.3lluZ             0x113000                1.074 MiB
+%.3lluZ             0x100000                1 MiB
+%zuZ                512                     512 Bytes
+%10lluZ             1024                    "     1 KiB"
+=================== ======================= ==============
+
 Characters
 ----------
 
diff --git a/lib/vsprintf.c b/lib/vsprintf.c
index 49dc9c38c65..a14f8c600b7 100644
--- a/lib/vsprintf.c
+++ b/lib/vsprintf.c
@@ -151,6 +151,7 @@ static noinline char *put_dec(char *buf, uint64_t num)
 #define SMALL  32              /* Must be 32 == 0x20 */
 #define SPECIAL        64              /* 0x */
 #define ERRSTR 128             /* %dE showing error string if enabled */
+#define SIZEUNIT       256             /* %uZ showing a size with a unit */
 
 /*
  * Macro to add a new character to our output string, but only if it will
@@ -281,6 +282,94 @@ static char *string(char *buf, char *end, const char *s, 
int field_width,
        return buf;
 }
 
+/* Most decimal places which %uZ shows */
+#define SIZE_MAX_DECIMALS      9
+
+/**
+ * size_string() - Print a size with a binary unit, for %uZ
+ *
+ * Print a size in bytes as "xxx Bytes" below 1 KiB, and otherwise as
+ * "xxx KiB", "xxx MiB", ... "xxx EiB", with the largest unit that gives at
+ * least 1. The precision is the number of decimal places, at most
+ * SIZE_MAX_DECIMALS, with the last one rounded and trailing zeros left out,
+ * so that "%.1lluZ" prints the same as print_size(). There are no decimal
+ * places without a precision.
+ *
+ * @buf:       Buffer to write to
+ * @end:       End of the buffer
+ * @size:      Size in bytes
+ * @field_width:       Minimum width of the output, padded with spaces
+ * @precision: Number of decimal places, -1 for none
+ * @flags:     Formatting flags; only LEFT is used
+ * Return:     Pointer to the end of the output in @buf
+ */
+static char *size_string(char *buf, char *end, u64 size, int field_width,
+                        int precision, int flags)
+{
+       static const char units[] = "KMGTPE";
+       char tmp[32], *p, *tmp_end = tmp + sizeof(tmp);
+       int unit, shift, decimals, i;
+       u64 whole, rem, mask;
+       u32 frac, scale;
+
+       /* Find the largest unit which gives at least 1 */
+       for (unit = -1; unit + 1 < (int)ARRAY_SIZE(units) - 1; unit++) {
+               if (!(size >> (10 * (unit + 2))))
+                       break;
+       }
+
+       if (unit < 0) {
+               p = number(tmp, tmp_end, size, 10, -1, -1, 0);
+               strcpy(p, " Bytes");
+
+               return string(buf, end, tmp, field_width, -1, flags);
+       }
+
+       shift = 10 * (unit + 1);
+       mask = (1ULL << shift) - 1;
+       whole = size >> shift;
+       rem = size & mask;
+       decimals = precision < 0 ? 0 : min(precision, SIZE_MAX_DECIMALS);
+
+       /* Work out each decimal place in turn, then round the last one */
+       frac = 0;
+       scale = 1;
+       for (i = 0; i < decimals; i++) {
+               rem *= 10;
+               frac = frac * 10 + (rem >> shift);
+               rem &= mask;
+               scale *= 10;
+       }
+       if (rem >> (shift - 1))
+               frac++;
+
+       /* Rounding can carry into the whole part, and on into the next unit */
+       if (frac >= scale) {
+               frac -= scale;
+               whole++;
+               if (whole == 1024 && units[unit + 1]) {
+                       whole = 1;
+                       unit++;
+               }
+       }
+
+       while (decimals > 0 && !(frac % 10)) {
+               frac /= 10;
+               decimals--;
+       }
+
+       p = number(tmp, tmp_end, whole, 10, -1, -1, 0);
+       if (decimals) {
+               *p++ = '.';
+               p = number(p, tmp_end, frac, 10, -1, decimals, 0);
+       }
+       *p++ = ' ';
+       *p++ = units[unit];
+       strcpy(p, "iB");
+
+       return string(buf, end, tmp, field_width, -1, flags);
+}
+
 /* U-Boot uses UTF-16 strings in the EFI context only. */
 static __maybe_unused char *string16(char *buf, char *end, u16 *s,
                                     int field_width, int precision, int flags)
@@ -726,6 +815,10 @@ repeat:
                        flags |= SIGN;
                /* fallthrough */
                case 'u':
+                       if (*fmt == 'u' && fmt[1] == 'Z') {
+                               flags |= SIZEUNIT;
+                               fmt++;
+                       }
                        break;
 
                default:
@@ -755,6 +848,11 @@ repeat:
                        if (flags & SIGN)
                                num = (signed int) num;
                }
+               if (flags & SIZEUNIT) {
+                       str = size_string(str, end, num, field_width, precision,
+                                         flags);
+                       continue;
+               }
                str = number(str, end, num, base, field_width, precision,
                             flags);
                if (IS_ENABLED(CONFIG_ERRNO_STR) && (flags & ERRSTR)) {
diff --git a/test/common/print.c b/test/common/print.c
index 27fdb444d0d..1d2ee8f3839 100644
--- a/test/common/print.c
+++ b/test/common/print.c
@@ -164,6 +164,103 @@ static int print_printf(struct unit_test_state *uts)
 }
 COMMON_TEST(print_printf, 0);
 
+/* Test printing sizes with %uZ */
+static int print_size_unit(struct unit_test_state *uts)
+{
+       char str[40];
+
+       /* The integer types */
+       snprintf(str, sizeof(str), "%uZ", 0x100000);
+       ut_asserteq_str("1 MiB", str);
+       snprintf(str, sizeof(str), "%luZ", 0x100000UL);
+       ut_asserteq_str("1 MiB", str);
+       snprintf(str, sizeof(str), "%lluZ", 0x100000ULL);
+       ut_asserteq_str("1 MiB", str);
+       snprintf(str, sizeof(str), "%zuZ", (size_t)0x100000);
+       ut_asserteq_str("1 MiB", str);
+
+       /* Below 1 KiB, and each unit */
+       snprintf(str, sizeof(str), "%lluZ", 0ULL);
+       ut_asserteq_str("0 Bytes", str);
+       snprintf(str, sizeof(str), "%lluZ", 1023ULL);
+       ut_asserteq_str("1023 Bytes", str);
+       snprintf(str, sizeof(str), "%lluZ", 1ULL << 10);
+       ut_asserteq_str("1 KiB", str);
+       snprintf(str, sizeof(str), "%lluZ", 1ULL << 30);
+       ut_asserteq_str("1 GiB", str);
+       snprintf(str, sizeof(str), "%lluZ", 1ULL << 40);
+       ut_asserteq_str("1 TiB", str);
+       snprintf(str, sizeof(str), "%lluZ", 1ULL << 50);
+       ut_asserteq_str("1 PiB", str);
+       snprintf(str, sizeof(str), "%lluZ", 1ULL << 60);
+       ut_asserteq_str("1 EiB", str);
+       snprintf(str, sizeof(str), "%.1lluZ", ~0ULL);
+       ut_asserteq_str("16 EiB", str);
+
+       /* The precision is the number of decimal places */
+       snprintf(str, sizeof(str), "%lluZ", 0x113000ULL);
+       ut_asserteq_str("1 MiB", str);
+       snprintf(str, sizeof(str), "%.1lluZ", 0x113000ULL);
+       ut_asserteq_str("1.1 MiB", str);
+       snprintf(str, sizeof(str), "%.3lluZ", 0x113000ULL);
+       ut_asserteq_str("1.074 MiB", str);
+       snprintf(str, sizeof(str), "%.*lluZ", 3, 0x113000ULL);
+       ut_asserteq_str("1.074 MiB", str);
+
+       /* Leading zeros of the decimals are kept, trailing ones left out */
+       snprintf(str, sizeof(str), "%.3lluZ", 0x101000ULL);
+       ut_asserteq_str("1.004 MiB", str);
+       snprintf(str, sizeof(str), "%.3lluZ", 0x100000ULL);
+       ut_asserteq_str("1 MiB", str);
+       snprintf(str, sizeof(str), "%.3lluZ", 0x180000ULL);
+       ut_asserteq_str("1.5 MiB", str);
+
+       /* Rounding, also into the next unit */
+       snprintf(str, sizeof(str), "%lluZ", 0x180000ULL);
+       ut_asserteq_str("2 MiB", str);
+       snprintf(str, sizeof(str), "%.1lluZ", (1ULL << 30) - 1);
+       ut_asserteq_str("1 GiB", str);
+
+       /* Field width */
+       snprintf(str, sizeof(str), "[%10lluZ]", 1ULL << 10);
+       ut_asserteq_str("[     1 KiB]", str);
+       snprintf(str, sizeof(str), "[%-10lluZ]", 1ULL << 10);
+       ut_asserteq_str("[1 KiB     ]", str);
+
+       /* Only %u takes the Z */
+       snprintf(str, sizeof(str), "%dZ %xZ", 0x100000, 0x100000);
+       ut_asserteq_str("1048576Z 100000Z", str);
+
+       /* A size which does not fit is truncated like any other output */
+       ut_asserteq(9, snprintf(str, 5, "%.3lluZ", 0x113000ULL));
+       ut_asserteq_str("1.07", str);
+
+       return 0;
+}
+COMMON_TEST(print_size_unit, 0);
+
+/* Test that %.1lluZ prints the same as print_size() */
+static int print_size_unit_compat(struct unit_test_state *uts)
+{
+       static const u64 sizes[] = {
+               0, 1, 999, 1023, 1024, 1025, 1536, 1588, 1638, 0x3ffff,
+               0xfffff, 0x100000, 0x113000, 0x1f00000, 0x7e00000,
+               0x3fffffff, 0x40000000, 0x76c0000000ULL, 0xe8d4a51000ULL,
+               0xfffffffffffULL, 1ULL << 40, (1ULL << 50) + (1ULL << 49),
+               (1ULL << 60) - 1, 3ULL << 60, ~0ULL,
+       };
+       int i;
+
+       for (i = 0; i < ARRAY_SIZE(sizes); i++) {
+               print_size(sizes[i], "\n");
+               ut_assert_nextline("%.1lluZ", sizes[i]);
+       }
+       ut_assert_console_end();
+
+       return 0;
+}
+COMMON_TEST(print_size_unit_compat, UTF_CONSOLE);
+
 static int print_display_buffer(struct unit_test_state *uts)
 {
        u8 *buf;
-- 
2.34.1

Reply via email to