Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test-musl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Run tests with musl libc (Alpine Linux)

on:
pull_request:
branches:
- main

jobs:
build:
name: musl libc test
runs-on: ubuntu-latest
env:
CI_CONTAINER: libbytesize-ci-alpine

steps:
- name: Checkout libbytesize repository
uses: actions/checkout@v6

- name: Install podman
run: |
sudo apt -qq update
sudo apt -y -qq install podman

- name: Build the container
run: |
podman build --no-cache -t ${{ env.CI_CONTAINER }} -f misc/alpine.Dockerfile .

- name: Start the container
run: |
podman run -d -t --name ${{ env.CI_CONTAINER }} --volume "$(pwd):/app" --workdir "/app" ${{ env.CI_CONTAINER }}

- name: Build in the container
run: |
podman exec -it ${{ env.CI_CONTAINER }} bash -c "./autogen.sh && LIBS='-lintl' ./configure --with-python3 --without-gtk-doc --without-tools && make"

- name: Run tests in the container
run: |
podman exec -it ${{ env.CI_CONTAINER }} bash -c "top_srcdir=/app top_builddir=/app source tests/testenv.sh && python3 tests/libbytesize_unittest.py"
6 changes: 6 additions & 0 deletions misc/alpine.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM alpine:latest

RUN apk add --no-cache \
gcc make autoconf automake libtool pkgconf musl-dev bash git \
gmp-dev pcre2-dev gettext-dev \
python3
36 changes: 18 additions & 18 deletions po/libbytesize.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: libbytesize 2.12\n"
"Report-Msgid-Bugs-To: vtrefny@redhat.com\n"
"POT-Creation-Date: 2026-01-08 06:02-0800\n"
"POT-Creation-Date: 2026-01-16 12:41+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand All @@ -18,86 +18,86 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"

#. TRANSLATORS: 'B' for bytes
#: src/bs_size.c:52 src/bs_size.c:73
#: src/bs_size.c:54 src/bs_size.c:75
msgid "B"
msgstr ""

#. TRANSLATORS: abbreviation for kibibyte, 2**10 bytes
#: src/bs_size.c:54
#: src/bs_size.c:56
msgid "KiB"
msgstr ""

#. TRANSLATORS: abbreviation for mebibyte, 2**20 bytes
#: src/bs_size.c:56
#: src/bs_size.c:58
msgid "MiB"
msgstr ""

#. TRANSLATORS: abbreviation for gibibyte, 2**30 bytes
#: src/bs_size.c:58
#: src/bs_size.c:60
msgid "GiB"
msgstr ""

#. TRANSLATORS: abbreviation for tebibyte, 2**40 bytes
#: src/bs_size.c:60
#: src/bs_size.c:62
msgid "TiB"
msgstr ""

#. TRANSLATORS: abbreviation for pebibyte, 2**50 bytes
#: src/bs_size.c:62
#: src/bs_size.c:64
msgid "PiB"
msgstr ""

#. TRANSLATORS: abbreviation for exbibyte, 2**60 bytes
#: src/bs_size.c:64
#: src/bs_size.c:66
msgid "EiB"
msgstr ""

#. TRANSLATORS: abbreviation for zebibyte, 2**70 bytes
#: src/bs_size.c:66
#: src/bs_size.c:68
msgid "ZiB"
msgstr ""

#. TRANSLATORS: abbreviation for yobibyte, 2**80 bytes
#: src/bs_size.c:68
#: src/bs_size.c:70
msgid "YiB"
msgstr ""

#. TRANSLATORS: abbreviation for kilobyte, 10**3 bytes
#: src/bs_size.c:75
#: src/bs_size.c:77
msgid "KB"
msgstr ""

#. TRANSLATORS: abbreviation for megabyte, 10**6 bytes
#: src/bs_size.c:77
#: src/bs_size.c:79
msgid "MB"
msgstr ""

#. TRANSLATORS: abbreviation for gigabyte, 10**9 bytes
#: src/bs_size.c:79
#: src/bs_size.c:81
msgid "GB"
msgstr ""

#. TRANSLATORS: abbreviation for terabyte, 10**12 bytes
#: src/bs_size.c:81
#: src/bs_size.c:83
msgid "TB"
msgstr ""

#. TRANSLATORS: abbreviation for petabyte, 10**15 bytes
#: src/bs_size.c:83
#: src/bs_size.c:85
msgid "PB"
msgstr ""

#. TRANSLATORS: abbreviation for exabyte, 10**18 bytes
#: src/bs_size.c:85
#: src/bs_size.c:87
msgid "EB"
msgstr ""

#. TRANSLATORS: abbreviation for zettabyte, 10**21 bytes
#: src/bs_size.c:87
#: src/bs_size.c:89
msgid "ZB"
msgstr ""

#. TRANSLATORS: abbreviation for yottabyte, 10**24 bytes
#: src/bs_size.c:89
#: src/bs_size.c:91
msgid "YB"
msgstr ""
39 changes: 35 additions & 4 deletions src/bs_size.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <ctype.h>
#include <limits.h>
#include <assert.h>
#include <wchar.h>

/* set code unit width to 8 so we can use generic macros like 'pcre2_compile'
* instead of 'pcre2_compile_8'
Expand Down Expand Up @@ -225,6 +226,36 @@ static void strstrip(char *str) {
str[i-begin] = '\0';
}

/* Case-insensitive comparison that handles multibyte UTF-8 (e.g. Cyrillic) */
static int u8_casecmp (const char *s1, const char *s2, size_t n1) {
wchar_t *w1 = NULL;
wchar_t *w2 = NULL;
size_t wlen1, wlen2;
int ret;

wlen1 = mbstowcs (NULL, s1, 0);
wlen2 = mbstowcs (NULL, s2, 0);
if (wlen1 == (size_t) -1 || wlen2 == (size_t) -1)
return strncasecmp (s1, s2, n1);

w1 = malloc ((wlen1 + 1) * sizeof (wchar_t));
w2 = malloc ((wlen2 + 1) * sizeof (wchar_t));
if (!w1 || !w2) {
free (w1);
free (w2);
return strncasecmp (s1, s2, n1);
}

mbstowcs (w1, s1, wlen1 + 1);
mbstowcs (w2, s2, wlen2 + 1);

ret = wcsncasecmp (w1, w2, wlen1);

free (w1);
free (w2);
return ret;
}

static bool multiply_size_by_unit (mpq_t size, char *unit_str) {
BSBunit bunit = BS_BUNIT_UNDEF;
BSDunit dunit = BS_DUNIT_UNDEF;
Expand All @@ -236,7 +267,7 @@ static bool multiply_size_by_unit (mpq_t size, char *unit_str) {
unit_str_len = strlen (unit_str);

for (bunit=BS_BUNIT_B; bunit < BS_BUNIT_UNDEF; bunit++)
if (strncasecmp (unit_str, b_units[bunit-BS_BUNIT_B], unit_str_len) == 0) {
if (u8_casecmp (unit_str, b_units[bunit-BS_BUNIT_B], unit_str_len) == 0) {
pwr = (uint64_t) bunit - BS_BUNIT_B;
mpz_mul_2exp (mpq_numref (size), mpq_numref (size), 10 * pwr);
return true;
Expand All @@ -245,7 +276,7 @@ static bool multiply_size_by_unit (mpq_t size, char *unit_str) {
mpq_init (dec_mul);
mpz_init (pow_1000);
for (dunit=BS_DUNIT_B; dunit < BS_DUNIT_UNDEF; dunit++)
if (strncasecmp (unit_str, d_units[dunit-BS_DUNIT_B], unit_str_len) == 0) {
if (u8_casecmp (unit_str, d_units[dunit-BS_DUNIT_B], unit_str_len) == 0) {
pwr = (uint64_t) (dunit - BS_DUNIT_B);
mpz_ui_pow_ui (pow_1000, 1000, pwr);
mpq_set_z (dec_mul, pow_1000);
Expand All @@ -256,7 +287,7 @@ static bool multiply_size_by_unit (mpq_t size, char *unit_str) {
}

for (bunit=BS_BUNIT_B; bunit < BS_BUNIT_UNDEF; bunit++)
if (strncasecmp (unit_str, _(b_units[bunit-BS_BUNIT_B]), unit_str_len) == 0) {
if (u8_casecmp (unit_str, _(b_units[bunit-BS_BUNIT_B]), unit_str_len) == 0) {
pwr = (uint64_t) bunit - BS_BUNIT_B;
mpz_mul_2exp (mpq_numref (size), mpq_numref (size), 10 * pwr);
mpz_clear (pow_1000);
Expand All @@ -265,7 +296,7 @@ static bool multiply_size_by_unit (mpq_t size, char *unit_str) {
}

for (dunit=BS_DUNIT_B; dunit < BS_DUNIT_UNDEF; dunit++)
if (strncasecmp (unit_str, _(d_units[dunit-BS_DUNIT_B]), unit_str_len) == 0) {
if (u8_casecmp (unit_str, _(d_units[dunit-BS_DUNIT_B]), unit_str_len) == 0) {
pwr = (uint64_t) (dunit - BS_DUNIT_B);
mpz_ui_pow_ui (pow_1000, 1000, pwr);
mpq_set_z (dec_mul, pow_1000);
Expand Down
31 changes: 30 additions & 1 deletion tests/libbytesize_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
import sys
import ctypes
import os

from locale_utils import get_avail_locales, missing_locales, requires_locales

Expand All @@ -17,7 +18,7 @@
except ImportError:
from bytesize.bytesize import SizeStruct

DEFAULT_LOCALE = "en_US.utf8"
DEFAULT_LOCALE = "C"

class SizeTestCase(unittest.TestCase):

Expand All @@ -32,6 +33,7 @@ def setUp(self):
self.skipTest("requires missing locales: %s" % missing)
locale.setlocale(locale.LC_ALL, DEFAULT_LOCALE)
self.addCleanup(self._clean_up)
os.environ["LANGUAGE"] = ""

def _clean_up(self):
locale.setlocale(locale.LC_ALL, DEFAULT_LOCALE)
Expand Down Expand Up @@ -140,6 +142,33 @@ def testNewFromStrLocalePsAF(self):
expected = (1536, -1)
self.assertEqual(actual, expected)

@requires_locales({'ru_RU.UTF-8'})
def testNewFromStrLocaleRuRU(self):
locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8')

# uppercase Cyrillic unit (canonical translation)
actual = SizeStruct.new_from_str('1 МиБ').get_bytes()
expected = (1048576, 1)
self.assertEqual(actual, expected)

# lowercase Cyrillic unit -- case-insensitive matching for non-ASCII
actual = SizeStruct.new_from_str('1 миб').get_bytes()
expected = (1048576, 1)
self.assertEqual(actual, expected)

actual = SizeStruct.new_from_str('2 гиб').get_bytes()
expected = (2147483648, 1)
self.assertEqual(actual, expected)

# ASCII units should still work under Russian locale
actual = SizeStruct.new_from_str('1 MiB').get_bytes()
expected = (1048576, 1)
self.assertEqual(actual, expected)

actual = SizeStruct.new_from_str('1 mib').get_bytes()
expected = (1048576, 1)
self.assertEqual(actual, expected)

#enddef

def testNewFromBytes(self):
Expand Down