Skip to content

Commit e0fb85b

Browse files
committed
Replace MPFR with GMP-only solution for correct decimal rounding
MPFR is GPLv3 licensed, which is inconvenient for embedded projects. This commit removes the MPFR dependency and replaces it with a GMP-only solution. The issue: mpz_set_f() truncates binary representation, causing e.g. 0.1 * 1000 to become 99 instead of 100. We need round-towards-zero logic. To deal with this we add a new helper that converts mpf_t to decimal string, truncates at decimal point, then parses back to mpz_t. This provides correct decimal rounding equivalent to MPFR's mpfr_get_z() with MPFR_RNDZ. All tests pass with the GMP-only implementation. Signed-off-by: Jon Oster <[email protected]>
1 parent e36cf1a commit e0fb85b

File tree

6 files changed

+87
-46
lines changed

6 files changed

+87
-46
lines changed

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ LIBBYTESIZE_PKG_CHECK_MODULES([PCRE2], [libpcre2-8])
4646

4747
AC_CHECK_LIB(gmp, __gmpz_init)
4848

49-
AC_CHECK_HEADERS([langinfo.h gmp.h mpfr.h stdint.h stdbool.h stdarg.h string.h stdio.h ctype.h],
49+
AC_CHECK_HEADERS([langinfo.h gmp.h stdint.h stdbool.h stdarg.h string.h stdio.h ctype.h],
5050
[],
5151
[LIBBYTESIZE_SOFT_FAILURE([Header file $ac_header not found.])],
5252
[])

dist/libbytesize.spec.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ Source0: https://github.com/storaged-project/libbytesize/releases/download/%
2626
BuildRequires: make
2727
BuildRequires: gcc
2828
BuildRequires: gmp-devel
29-
BuildRequires: mpfr-devel
3029
BuildRequires: pcre2-devel
3130
BuildRequires: gettext-devel
3231
%if %{with_python3}

po/libbytesize.pot

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: libbytesize 2.12\n"
1010
"Report-Msgid-Bugs-To: [email protected]\n"
11-
"POT-Creation-Date: 2025-12-04 12:49+0100\n"
11+
"POT-Creation-Date: 2025-12-11 13:09-0800\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <[email protected]>\n"
@@ -18,86 +18,86 @@ msgstr ""
1818
"Content-Transfer-Encoding: 8bit\n"
1919

2020
#. TRANSLATORS: 'B' for bytes
21-
#: src/bs_size.c:52 src/bs_size.c:73
21+
#: src/bs_size.c:54 src/bs_size.c:75
2222
msgid "B"
2323
msgstr ""
2424

2525
#. TRANSLATORS: abbreviation for kibibyte, 2**10 bytes
26-
#: src/bs_size.c:54
26+
#: src/bs_size.c:56
2727
msgid "KiB"
2828
msgstr ""
2929

3030
#. TRANSLATORS: abbreviation for mebibyte, 2**20 bytes
31-
#: src/bs_size.c:56
31+
#: src/bs_size.c:58
3232
msgid "MiB"
3333
msgstr ""
3434

3535
#. TRANSLATORS: abbreviation for gibibyte, 2**30 bytes
36-
#: src/bs_size.c:58
36+
#: src/bs_size.c:60
3737
msgid "GiB"
3838
msgstr ""
3939

4040
#. TRANSLATORS: abbreviation for tebibyte, 2**40 bytes
41-
#: src/bs_size.c:60
41+
#: src/bs_size.c:62
4242
msgid "TiB"
4343
msgstr ""
4444

4545
#. TRANSLATORS: abbreviation for pebibyte, 2**50 bytes
46-
#: src/bs_size.c:62
46+
#: src/bs_size.c:64
4747
msgid "PiB"
4848
msgstr ""
4949

5050
#. TRANSLATORS: abbreviation for exbibyte, 2**60 bytes
51-
#: src/bs_size.c:64
51+
#: src/bs_size.c:66
5252
msgid "EiB"
5353
msgstr ""
5454

5555
#. TRANSLATORS: abbreviation for zebibyte, 2**70 bytes
56-
#: src/bs_size.c:66
56+
#: src/bs_size.c:68
5757
msgid "ZiB"
5858
msgstr ""
5959

6060
#. TRANSLATORS: abbreviation for yobibyte, 2**80 bytes
61-
#: src/bs_size.c:68
61+
#: src/bs_size.c:70
6262
msgid "YiB"
6363
msgstr ""
6464

6565
#. TRANSLATORS: abbreviation for kilobyte, 10**3 bytes
66-
#: src/bs_size.c:75
66+
#: src/bs_size.c:77
6767
msgid "KB"
6868
msgstr ""
6969

7070
#. TRANSLATORS: abbreviation for megabyte, 10**6 bytes
71-
#: src/bs_size.c:77
71+
#: src/bs_size.c:79
7272
msgid "MB"
7373
msgstr ""
7474

7575
#. TRANSLATORS: abbreviation for gigabyte, 10**9 bytes
76-
#: src/bs_size.c:79
76+
#: src/bs_size.c:81
7777
msgid "GB"
7878
msgstr ""
7979

8080
#. TRANSLATORS: abbreviation for terabyte, 10**12 bytes
81-
#: src/bs_size.c:81
81+
#: src/bs_size.c:83
8282
msgid "TB"
8383
msgstr ""
8484

8585
#. TRANSLATORS: abbreviation for petabyte, 10**15 bytes
86-
#: src/bs_size.c:83
86+
#: src/bs_size.c:85
8787
msgid "PB"
8888
msgstr ""
8989

9090
#. TRANSLATORS: abbreviation for exabyte, 10**18 bytes
91-
#: src/bs_size.c:85
91+
#: src/bs_size.c:87
9292
msgid "EB"
9393
msgstr ""
9494

9595
#. TRANSLATORS: abbreviation for zettabyte, 10**21 bytes
96-
#: src/bs_size.c:87
96+
#: src/bs_size.c:89
9797
msgid "ZB"
9898
msgstr ""
9999

100100
#. TRANSLATORS: abbreviation for yottabyte, 10**24 bytes
101-
#: src/bs_size.c:89
101+
#: src/bs_size.c:91
102102
msgid "YB"
103103
msgstr ""

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ LDADD = $(LIBINTL)
33

44
lib_LTLIBRARIES = libbytesize.la
55
libbytesize_la_CFLAGS = -Wall -Wextra -Werror -Wno-overflow -D_GNU_SOURCE
6-
libbytesize_la_LIBADD = -lgmp -lmpfr $(PCRE2_LIBS)
6+
libbytesize_la_LIBADD = -lgmp $(PCRE2_LIBS)
77
libbytesize_la_LDFLAGS = -version-info 1:0:0
88
libbytesize_la_SOURCES = bs_size.c bs_size.h gettext.h
99

src/bs_size.c

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#include <gmp.h>
2-
#include <mpfr.h>
32
#include <langinfo.h>
43
#include <stdarg.h>
54
#include <stdio.h>
@@ -224,30 +223,66 @@ static void strstrip(char *str) {
224223
str[i-begin] = '\0';
225224
}
226225

227-
static bool multiply_size_by_unit (mpfr_t size, char *unit_str) {
226+
/* Helper function to convert mpf_t to mpz_t with round-toward-zero (truncate decimal) */
227+
static void mpz_set_f_round_zero(mpz_t rop, const mpf_t op) {
228+
char *str;
229+
const char *radix_char;
230+
char *dot;
231+
char *exp;
232+
233+
/* Convert to decimal string with enough precision */
234+
gmp_asprintf(&str, "%.100Ff", op);
235+
236+
/* Get the locale's decimal separator */
237+
radix_char = nl_langinfo(RADIXCHAR);
238+
239+
/* Find decimal point (using locale's separator) and truncate */
240+
dot = strchr(str, radix_char[0]);
241+
if (dot) {
242+
*dot = '\0';
243+
}
244+
245+
/* Remove any exponent notation */
246+
exp = strchr(str, 'e');
247+
if (!exp) exp = strchr(str, 'E');
248+
if (exp) *exp = '\0';
249+
250+
mpz_set_str(rop, str, 10);
251+
free(str);
252+
}
253+
254+
static bool multiply_size_by_unit (mpf_t size, char *unit_str) {
228255
BSBunit bunit = BS_BUNIT_UNDEF;
229256
BSDunit dunit = BS_DUNIT_UNDEF;
230257
uint64_t pwr = 0;
231-
mpfr_t dec_mul;
258+
mpf_t dec_mul;
259+
mpf_t base;
232260
size_t unit_str_len = 0;
261+
uint64_t i;
233262

234263
unit_str_len = strlen (unit_str);
235264

236265
for (bunit=BS_BUNIT_B; bunit < BS_BUNIT_UNDEF; bunit++)
237266
if (strncasecmp (unit_str, b_units[bunit-BS_BUNIT_B], unit_str_len) == 0) {
238267
pwr = (uint64_t) bunit - BS_BUNIT_B;
239-
mpfr_mul_2exp (size, size, 10 * pwr, MPFR_RNDN);
268+
mpf_mul_2exp (size, size, 10 * pwr);
240269
return true;
241270
}
242271

243-
mpfr_init2 (dec_mul, BS_FLOAT_PREC_BITS);
244-
mpfr_set_ui (dec_mul, 1000, MPFR_RNDN);
272+
mpf_init2 (dec_mul, BS_FLOAT_PREC_BITS);
273+
mpf_init2 (base, BS_FLOAT_PREC_BITS);
274+
mpf_set_ui (base, 1000);
245275
for (dunit=BS_DUNIT_B; dunit < BS_DUNIT_UNDEF; dunit++)
246276
if (strncasecmp (unit_str, d_units[dunit-BS_DUNIT_B], unit_str_len) == 0) {
247277
pwr = (uint64_t) (dunit - BS_DUNIT_B);
248-
mpfr_pow_ui (dec_mul, dec_mul, pwr, MPFR_RNDN);
249-
mpfr_mul (size, size, dec_mul, MPFR_RNDN);
250-
mpfr_clear (dec_mul);
278+
/* Compute 1000^pwr using repeated multiplication */
279+
mpf_set_ui (dec_mul, 1);
280+
for (i = 0; i < pwr; i++) {
281+
mpf_mul (dec_mul, dec_mul, base);
282+
}
283+
mpf_mul (size, size, dec_mul);
284+
mpf_clear (dec_mul);
285+
mpf_clear (base);
251286
return true;
252287
}
253288

@@ -256,21 +291,27 @@ static bool multiply_size_by_unit (mpfr_t size, char *unit_str) {
256291
for (bunit=BS_BUNIT_B; bunit < BS_BUNIT_UNDEF; bunit++)
257292
if (strncasecmp (unit_str, _(b_units[bunit-BS_BUNIT_B]), unit_str_len) == 0) {
258293
pwr = (uint64_t) bunit - BS_BUNIT_B;
259-
mpfr_mul_2exp (size, size, 10 * pwr, MPFR_RNDN);
294+
mpf_mul_2exp (size, size, 10 * pwr);
260295
return true;
261296
}
262297

263-
mpfr_init2 (dec_mul, BS_FLOAT_PREC_BITS);
264-
mpfr_set_ui (dec_mul, 1000, MPFR_RNDN);
298+
mpf_set_ui (base, 1000);
265299
for (dunit=BS_DUNIT_B; dunit < BS_DUNIT_UNDEF; dunit++)
266300
if (strncasecmp (unit_str, _(d_units[dunit-BS_DUNIT_B]), unit_str_len) == 0) {
267301
pwr = (uint64_t) (dunit - BS_DUNIT_B);
268-
mpfr_pow_ui (dec_mul, dec_mul, pwr, MPFR_RNDN);
269-
mpfr_mul (size, size, dec_mul, MPFR_RNDN);
270-
mpfr_clear (dec_mul);
302+
/* Compute 1000^pwr using repeated multiplication */
303+
mpf_set_ui (dec_mul, 1);
304+
for (i = 0; i < pwr; i++) {
305+
mpf_mul (dec_mul, dec_mul, base);
306+
}
307+
mpf_mul (size, size, dec_mul);
308+
mpf_clear (dec_mul);
309+
mpf_clear (base);
271310
return true;
272311
}
273312

313+
mpf_clear (dec_mul);
314+
mpf_clear (base);
274315
return false;
275316
}
276317

@@ -452,7 +493,7 @@ BSSize bs_size_new_from_str (const char *size_str, BSError **error) {
452493
const char *radix_char = NULL;
453494
char *loc_size_str = NULL;
454495
mpf_t parsed_size;
455-
mpfr_t size;
496+
mpf_t size;
456497
int status = 0;
457498
BSSize ret = NULL;
458499
PCRE2_UCHAR *substring = NULL;
@@ -518,8 +559,7 @@ BSSize bs_size_new_from_str (const char *size_str, BSError **error) {
518559
return NULL;
519560
}
520561

521-
/* parse the number using GMP because it knows how to handle localization
522-
much better than MPFR */
562+
/* parse the number using GMP - it handles localization correctly */
523563
mpf_init2 (parsed_size, BS_FLOAT_PREC_BITS);
524564
status = mpf_set_str (parsed_size, *substring == '+' ? (const char *) substring+1 : (const char *) substring, 10);
525565
pcre2_substring_free (substring);
@@ -531,9 +571,9 @@ BSSize bs_size_new_from_str (const char *size_str, BSError **error) {
531571
mpf_clear (parsed_size);
532572
return NULL;
533573
}
534-
/* but use MPFR from now on because GMP thinks 0.1*1000 = 99 */
535-
mpfr_init2 (size, BS_FLOAT_PREC_BITS);
536-
mpfr_set_f (size, parsed_size, MPFR_RNDN);
574+
/* Use GMP mpf_t directly - we'll handle rounding correctly when converting to integer */
575+
mpf_init2 (size, BS_FLOAT_PREC_BITS);
576+
mpf_set (size, parsed_size);
537577
mpf_clear (parsed_size);
538578

539579
status = pcre2_substring_get_byname (match_data, (PCRE2_SPTR) "rest", &substring, &substring_len);
@@ -545,7 +585,7 @@ BSSize bs_size_new_from_str (const char *size_str, BSError **error) {
545585
pcre2_match_data_free (match_data);
546586
pcre2_code_free (regex);
547587
free (loc_size_str);
548-
mpfr_clear (size);
588+
mpf_clear (size);
549589
return NULL;
550590
}
551591
}
@@ -554,10 +594,12 @@ BSSize bs_size_new_from_str (const char *size_str, BSError **error) {
554594
pcre2_code_free (regex);
555595

556596
ret = bs_size_new ();
557-
mpfr_get_z (ret->bytes, size, MPFR_RNDZ);
597+
/* Convert from mpf_t to mpz_t with round-toward-zero using string conversion
598+
to ensure correct decimal rounding (mpz_set_f truncates binary, giving wrong results) */
599+
mpz_set_f_round_zero (ret->bytes, size);
558600

559601
free (loc_size_str);
560-
mpfr_clear (size);
602+
mpf_clear (size);
561603

562604
return ret;
563605
}

src/bytesize.pc.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ URL: https://github.com/storaged-project/libbytesize
99
Version: @VERSION@
1010
Cflags: -I${includedir}/bytesize
1111
Libs: -lbytesize
12-
Libs.private: -lgmp -lmpfr
12+
Libs.private: -lgmp

0 commit comments

Comments
 (0)