--- /dev/null
+#!/bin/bash
+
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Copyright (c) 2024 Robin Jarry
+#
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="bits"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command "$TS_CMD_BITS"
+ts_cd "$TS_OUTDIR"
+
+ts_init_subtest "default"
+$TS_CMD_BITS 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "mask"
+$TS_CMD_BITS --mask 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "grouped-mask"
+$TS_CMD_BITS --grouped-mask 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "list"
+$TS_CMD_BITS --list 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "binary"
+$TS_CMD_BITS --binary 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "truncate"
+$TS_CMD_BITS -l 1,10000 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "width"
+$TS_CMD_BITS -w 16384 -l 10000 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "width-truncate"
+$TS_CMD_BITS -w 32 -l 11,22,33,44 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "parse-mask"
+$TS_CMD_BITS -l 0x0badcaca >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "parse-range"
+$TS_CMD_BITS -g 50-100 75-150 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "parse-grouped-mask"
+$TS_CMD_BITS -l ,9000000,00000000,0c000000,00000000 >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "or"
+$TS_CMD_BITS -l 50-100 '|75-150' >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "and"
+$TS_CMD_BITS -l 50-100 '&75-150' >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "xor"
+$TS_CMD_BITS -l 50-100 '^75-150' >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "not"
+$TS_CMD_BITS -l 50-100 '~75-150' >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_init_subtest "stdin"
+{
+ echo 11,22,33,44
+ echo ^22
+} | $TS_CMD_BITS --list >> $TS_OUTPUT 2>> $TS_ERRLOG
+ts_finalize_subtest
+
+ts_finalize
--- /dev/null
+//po4a: entry man manual
+////
+
+SPDX-License-Identifier: GPL-2.0-or-later
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+Copyright (c) 2024 Robin Jarry
+
+////
+= bits(1)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: bits
+
+== NAME
+
+bits - convert bit masks from/to various formats
+
+== SYNOPSIS
+
+*bits* [*-h*] [*-V*] [*-w* _<NUM>_] [_<MODE>_] [_<MASK_OR_LIST>_...]
+
+== DESCRIPTION
+
+The *bits* utility converts bit masks into various formats. It supports
+combining multiple masks together using bitwise operations.
+
+== POSITIONAL ARGUMENTS
+
+_<MASK_OR_LIST>_::
+A set of bits specified as a hexadecimal mask value (e.g. _0xeec2_) or as
+a comma-separated list of bit IDs.
+
+If no argument is specified, the sets of bits will be read from standard input;
+one group per line.
+
+Consecutive ids can be compressed as ranges (e.g. _5,6,7,8,9,10_ -> _5-10_).
+
+Optionally, if an argument starts with a comma, it will be parsed as a single
+hexadecimal mask split in 32bit groups (e.g. _,00014000,00000000,00020000_ ->
+_17,78,80_).
+
+By default all groups will be OR'ed together. If a group has one of the
+following prefixes, it will be combined with the resulting mask using
+a different binary operation:
+
+**&**__<MASK_OR_LIST>__::
+The group will be combined with a binary AND operation. I.e. all bits that are
+set to 1 in the group AND the combined groups so far will be preserved to 1.
+All other bits will be reset to 0.
+
+**^**__<MASK_OR_LIST>__::
+The group will be combined with a binary XOR operation. I.e. all bits that are
+set to 1 in the group AND to 0 the combined groups so far (or the other way
+around) will be set to 1. Bits that are both to 1 or both to 0 will be reset to
+0.
+
+**~**__<MASK_OR_LIST>__::
+All bits set to 1 in the group will be cleared (reset to 0) in the combined
+groups so far.
+
+== OPTIONS
+
+include::man-common/help-version.adoc[]
+
+*-w* __<NUM>__, *--width* __<NUM>__::
+Maximum number of bits in the masks handled by *bits* (default __8192__). Any
+bit larger than this number will be truncated.
+
+== CONVERSION MODE
+
+One of the following conversion modes can be specified. If not specified, it
+defaults to *-m*, *--mask*.
+
+*-m*, *--mask*::
+Print the combined args as a hexadecimal mask value (default).
+
+*-g*, *--grouped-mask*::
+Print the combined args as a hexadecimal mask value in 32bit comma separated
+groups.
+
+*-b*, *--binary*::
+Print the combined args as a binary mask value.
+
+*-l*, *--list*::
+Print the combined args as a list of bit IDs. Consecutive IDs are compressed as
+ranges.
+
+== EXAMPLES
+
+....
+~$ bits --mask 4,5-8 16,30
+0x400101f0
+
+~$ bits --list 0xeec2
+1,6,7,9-11,13-15
+
+~$ bits --binary 4,5-8 16,30
+0b100_0000_0000_0001_0000_0001_1111_0000
+
+~$ bits --list ,00300000,03000000,30000003
+0,1,28,29,56,57,84,85
+
+~$ bits --list 1,2,3,4 ~3-10
+1,2
+
+~$ bits --list 1,2,3,4 ^3-10
+1,2,5-10
+
+~$ bits --grouped-mask 2,22,74,79
+8400,00000000,00400004
+
+~$ bits --width 64 --list 2,22,74,79
+2,22
+....
+
+== AUTHORS
+
+Robin Jarry.
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
--- /dev/null
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Copyright (c) 2024 Robin Jarry
+ *
+ * bits - convert bit masks from/to various formats
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "c.h"
+#include "closestream.h"
+#include "cpuset.h"
+#include "nls.h"
+#include "strutils.h"
+#include "strv.h"
+#include "xalloc.h"
+
+static void parse_mask_or_list(const char *cmdline_arg,
+ cpu_set_t *all_bits, size_t width)
+{
+ cpu_set_t *bits, *copy;
+ char bitwise_op = '|';
+ const char *arg;
+ size_t size, n;
+
+ arg = cmdline_arg;
+
+ /* strip optional operator first */
+ if (startswith(arg, "&")) {
+ bitwise_op = '&';
+ arg++;
+ } else if (startswith(arg, "^")) {
+ bitwise_op = '^';
+ arg++;
+ } else if (startswith(arg, "~")) {
+ bitwise_op = '~';
+ arg++;
+ } else if (startswith(arg, "|")) {
+ arg++;
+ }
+
+ bits = cpuset_alloc(width, &size, NULL);
+ if (bits == NULL)
+ errx(EXIT_FAILURE, _("error: cannot allocate bit mask"));
+
+ if (startswith(arg, ",") || startswith(arg, "0x")) {
+ if (startswith(arg, ","))
+ arg++;
+ if (cpumask_parse(arg, bits, size) < 0)
+ errx(EXIT_FAILURE, _("error: invalid bit mask: %s"), cmdline_arg);
+ } else {
+ if (cpulist_parse(arg, bits, size, 1) < 0)
+ errx(EXIT_FAILURE, _("error: invalid bit list: %s"), cmdline_arg);
+ }
+
+ /* truncate all bits beyond the requested mask size */
+ for (n = cpuset_nbits(size) - 1; n >= width; n--)
+ CPU_CLR_S(n, size, bits);
+
+ copy = cpuset_alloc(width, &size, NULL);
+ if (copy == NULL)
+ errx(EXIT_FAILURE, _("error: cannot allocate bit mask"));
+ memcpy(copy, all_bits, size);
+
+ switch (bitwise_op) {
+ case '&':
+ CPU_AND_S(size, all_bits, copy, bits);
+ break;
+ case '|':
+ CPU_OR_S(size, all_bits, copy, bits);
+ break;
+ case '^':
+ CPU_XOR_S(size, all_bits, copy, bits);
+ break;
+ case '~':
+ for (n = 0; n < width; n++) {
+ if (CPU_ISSET_S(n, size, bits))
+ CPU_CLR_S(n, size, all_bits);
+ }
+ break;
+ }
+
+ cpuset_free(bits);
+ cpuset_free(copy);
+}
+
+static size_t num_digits(size_t value)
+{
+ size_t digits = 1;
+ while (value > 9) {
+ digits++;
+ value /= 10;
+ }
+ return digits;
+}
+
+enum output_mode {
+ MODE_BINARY,
+ MODE_GROUPED_MASK,
+ MODE_LIST,
+ MODE_MASK,
+};
+
+static void print_bits(cpu_set_t *bits, size_t width, enum output_mode mode)
+{
+ const size_t size = CPU_ALLOC_SIZE(width);
+ bool started = false;
+ char *buf = NULL;
+ size_t buf_size;
+ ssize_t n = 0;
+
+ if (CPU_COUNT_S(size, bits) == 0) {
+ switch (mode) {
+ case MODE_MASK:
+ printf("0x0\n");
+ break;
+ case MODE_GROUPED_MASK:
+ printf("0\n");
+ break;
+ case MODE_BINARY:
+ printf("0b0\n");
+ break;
+ case MODE_LIST:
+ break;
+ }
+ return;
+ }
+
+ switch (mode) {
+ case MODE_MASK:
+ /* fit 4 bits per character plus terminating nul byte */
+ buf_size = ((cpuset_nbits(size) + 3) / 4) + 1;
+ buf = xmalloc(buf_size);
+ cpumask_create(buf, buf_size, bits, size);
+
+ /* strip leading zeroes */
+ while (buf[n] == '0')
+ n++;
+ printf("0x%s\n", buf + n);
+ break;
+
+ case MODE_GROUPED_MASK:
+ /* fit 4 bits per character plus terminating nul byte */
+ buf_size = ((cpuset_nbits(size) + 3) / 4) + 1;
+ buf = xmalloc(buf_size);
+ cpumask_create(buf, buf_size, bits, size);
+
+ /* strip leading zeroes */
+ while (buf[n] == '0')
+ n++;
+
+ while (buf[n] != '\0') {
+ if (started && (n % 8) == 0)
+ printf(",");
+ if (buf[n] != '0')
+ started = true;
+ printf("%c", buf[n]);
+ n++;
+ }
+ printf("\n");
+ break;
+
+ case MODE_BINARY:
+ printf("0b");
+ for (n = width - 1; n >= 0; n--) {
+ if (started && ((n + 1) % 4) == 0)
+ printf("_");
+ if (CPU_ISSET_S(n, size, bits)) {
+ started = true;
+ printf("1");
+ } else if (started) {
+ printf("0");
+ }
+ }
+ printf("\n");
+ break;
+
+ case MODE_LIST:
+ /* Maximum number of digits (larger bit number) plus 1
+ * to account for a separating comma, times the number of bits
+ * set to 1. */
+ buf_size = (num_digits(width - 1) + 1) * CPU_COUNT_S(size, bits);
+ buf = xmalloc(buf_size);
+ cpulist_create(buf, buf_size, bits, size);
+ printf("%s\n", buf);
+ break;
+ }
+
+ free(buf);
+}
+
+static void __attribute__((__noreturn__)) usage(void)
+{
+ fputs(USAGE_HEADER, stdout);
+ fprintf(stdout, _(" %s [options] [<mask_or_list>...]\n"),
+ program_invocation_short_name);
+
+ fputs(USAGE_SEPARATOR, stdout);
+ fputsln(_("Convert bit masks from/to various formats."), stdout);
+
+ fputs(USAGE_ARGUMENTS, stdout);
+ fputsln(_(" <mask_or_list> A set of bits specified as a hex mask value (e.g. 0xeec2)\n"
+ " or as a comma-separated list of bit IDs."),
+ stdout);
+ fputs(USAGE_SEPARATOR, stdout);
+ fputsln(_(" If not specified, arguments will be read from stdin."),
+ stdout);
+
+ fputs(USAGE_OPTIONS, stdout);
+ fprintf(stdout, USAGE_HELP_OPTIONS(21));
+ fputsln(_(" -w <num>, --width <num>\n"
+ " Maximum width of bit masks handled by this tool (default 8192)."),
+ stdout);
+
+ fputs(_("\nMode:\n"), stdout);
+ fputsln(_(" -m, --mask Print the combined args as a hex mask value (default)."),
+ stdout);
+ fputsln(_(" -g, --grouped-mask Print the combined args as a hex mask value in 32bit\n"
+ " comma separated groups."), stdout);
+ fputsln(_(" -b, --binary Print the combined args as a binary mask value."),
+ stdout);
+ fputsln(_(" -l, --list Print the combined args as a compressed list of bit IDs."),
+ stdout);
+
+ fprintf(stdout, USAGE_MAN_TAIL("bits(1)"));
+ exit(EXIT_SUCCESS);
+}
+
+int main(int argc, char **argv)
+{
+ enum output_mode mode = MODE_MASK;
+ char **stdin_lines = NULL;
+ cpu_set_t *bits = NULL;
+ size_t width = 8192;
+ size_t alloc_size;
+ int c;
+
+#define FLAGS "Vhw:mgbl"
+ static const struct option longopts[] = {
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ { "width", required_argument, NULL, 'w' },
+ { "mask", no_argument, NULL, 'm' },
+ { "grouped-mask", no_argument, NULL, 'g' },
+ { "binary", no_argument, NULL, 'b' },
+ { "list", no_argument, NULL, 'l' },
+ { NULL, 0, NULL, 0 }
+ };
+
+ setlocale(LC_ALL, "");
+ bindtextdomain(PACKAGE, LOCALEDIR);
+ textdomain(PACKAGE);
+ close_stdout_atexit();
+
+ while ((c = getopt_long(argc, argv, FLAGS, longopts, NULL)) != -1) {
+ switch (c) {
+ case 'm':
+ mode = MODE_MASK;
+ break;
+ case 'g':
+ mode = MODE_GROUPED_MASK;
+ break;
+ case 'b':
+ mode = MODE_BINARY;
+ break;
+ case 'l':
+ mode = MODE_LIST;
+ break;
+ case 'w':
+ /* allow up to 128k masks */
+ width = str2unum_or_err(optarg,
+ 10, "invalid --width", 128 * 1024);
+ break;
+ case 'V':
+ print_version(EXIT_SUCCESS);
+ case 'h':
+ usage();
+ default:
+ errtryhelp(EXIT_FAILURE);
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ if (argc == 0) {
+ /* no arguments provided, read lines from stdin */
+ char buf[LINE_MAX];
+
+ while (fgets(buf, sizeof(buf), stdin)) {
+ /* strip LF, CR, CRLF, LFCR */
+ rtrim_whitespace((unsigned char *)buf);
+ if (strv_push(&stdin_lines, xstrdup(buf)) < 0)
+ errx(EXIT_FAILURE, _("cannot allocate memory"));
+ }
+
+ argc = strv_length(stdin_lines);
+ argv = stdin_lines;
+ }
+
+ bits = cpuset_alloc(width, &alloc_size, NULL);
+ if (bits == NULL)
+ errx(EXIT_FAILURE, _("cannot allocate memory"));
+
+ /* start off with all bits set to 0 */
+ memset(bits, 0, alloc_size);
+
+ for (; argc > 0; argc--, argv++)
+ parse_mask_or_list(*argv, bits, width);
+
+ strv_free(stdin_lines);
+
+ print_bits(bits, width, mode);
+
+ cpuset_free(bits);
+
+ return EXIT_SUCCESS;
+}