summary refs log tree commit diff
path: root/src/libnmc-base
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2026-07-03 19:53:18 +0200
committerMichael Biebl <biebl@debian.org>2026-07-03 19:53:18 +0200
commit537bfce2bda471c92caabd388589230200891509 (patch)
treeaedeccfaf0ba52c238ecf51fc009c0db5d4b60f0 /src/libnmc-base
parent869e9027026cdbb15d4e4a6327ff41d2697858eb (diff)
New upstream version 1.58~rc1 upstream/1.58_rc1
Diffstat (limited to 'src/libnmc-base')
-rw-r--r--src/libnmc-base/nm-client-utils.c152
-rw-r--r--src/libnmc-base/nm-client-utils.h12
-rw-r--r--src/libnmc-base/tests/meson.build26
-rw-r--r--src/libnmc-base/tests/test-client-utils.c126
4 files changed, 304 insertions, 12 deletions
diff --git a/src/libnmc-base/nm-client-utils.c b/src/libnmc-base/nm-client-utils.c
index 10c2c295..9c825994 100644
--- a/src/libnmc-base/nm-client-utils.c
+++ b/src/libnmc-base/nm-client-utils.c
@@ -697,10 +697,29 @@ NM_PRAGMA_WARNING_DISABLE("-Wdeclaration-after-statement")
 NM_PRAGMA_WARNING_REENABLE
 #pragma GCC visibility pop
 
+/* Encode @str into @qrcode (sized qrcodegen_BUFFER_LEN_FOR_VERSION(VERSION_MAX)).
+ * Returns the QR side length in modules, or -1 if @str is too long to encode. */
+static int
+_qr_encode(const char *str, uint8_t *qrcode)
+{
+    uint8_t tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
+
+    if (!qrcodegen_encodeText(str,
+                              tempBuffer,
+                              qrcode,
+                              qrcodegen_Ecc_LOW,
+                              qrcodegen_VERSION_MIN,
+                              qrcodegen_VERSION_MAX,
+                              qrcodegen_Mask_AUTO,
+                              FALSE))
+        return -1;
+
+    return qrcodegen_getSize(qrcode);
+}
+
 void
 nmc_print_qrcode(const char *str)
 {
-    uint8_t  tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
     uint8_t  qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
     gboolean term_linux;
     int      size;
@@ -711,18 +730,9 @@ nmc_print_qrcode(const char *str)
     if (!term_linux && !can_show_graphics())
         return;
 
-    if (!qrcodegen_encodeText(str,
-                              tempBuffer,
-                              qrcode,
-                              qrcodegen_Ecc_LOW,
-                              qrcodegen_VERSION_MIN,
-                              qrcodegen_VERSION_MAX,
-                              qrcodegen_Mask_AUTO,
-                              FALSE)) {
+    size = _qr_encode(str, qrcode);
+    if (size < 0)
         return;
-    }
-
-    size = qrcodegen_getSize(qrcode);
 
     g_print("\n");
 
@@ -756,6 +766,124 @@ nmc_print_qrcode(const char *str)
     }
 }
 
+static void
+_qr_append_mecard(GString *string, const char *tag, const char *text)
+{
+    const char *p;
+    bool        is_hex = TRUE;
+    int         start;
+
+    if (!text)
+        return;
+
+    g_string_append(string, tag);
+    start = string->len;
+
+    for (p = text; *p; p++) {
+        if (!g_ascii_isxdigit(*p))
+            is_hex = FALSE;
+        if (strchr("\\\":;,", *p))
+            g_string_append_c(string, '\\');
+        g_string_append_c(string, *p);
+    }
+
+    if (is_hex && text[0]) {
+        g_string_insert_c(string, start, '\"');
+        g_string_append_c(string, '\"');
+    }
+    g_string_append_c(string, ';');
+}
+
+/**
+ * nmc_wifi_qr_uri_new:
+ * @ssid: the network SSID (UTF-8)
+ * @key_mgmt: the wireless security key management, or %NULL for an open network
+ * @psk: the pre-shared key, or %NULL
+ * @hidden: whether the network is hidden
+ *
+ * Builds a "WIFI:" provisioning URI (the format encoded in Wi-Fi QR codes).
+ *
+ * Returns: (transfer full): a newly-allocated URI string.
+ */
+char *
+nmc_wifi_qr_uri_new(const char *ssid, const char *key_mgmt, const char *psk, gboolean hidden)
+{
+    GString    *string;
+    const char *type = NULL;
+
+    string = g_string_sized_new(64);
+    g_string_append(string, "WIFI:");
+
+    if (key_mgmt == NULL)
+        type = "nopass";
+    else if (NM_IN_STRSET(key_mgmt, "none", "ieee8021x"))
+        type = "WEP";
+    else if (NM_IN_STRSET(key_mgmt, "wpa-none", "wpa-psk", "sae"))
+        type = "WPA";
+    else if (nm_streq(key_mgmt, "owe"))
+        type = "nopass";
+
+    _qr_append_mecard(string, "T:", type);
+    _qr_append_mecard(string, "S:", ssid);
+    _qr_append_mecard(string, "P:", psk);
+
+    if (hidden)
+        g_string_append(string, "H:true;");
+
+    g_string_append_c(string, ';');
+
+    return g_string_free(string, FALSE);
+}
+
+/**
+ * nmc_wifi_qr_render_string:
+ * @str: the text to encode
+ *
+ * Encodes @str as a QR code and renders it into a newline-joined string of
+ * Unicode half-block glyphs. Dark modules are drawn as foreground glyphs and
+ * light modules as spaces, so the result is scannable on a black-on-white
+ * surface (unlike nmc_print_qrcode(), which prints ANSI-colored output to a
+ * terminal). Two module rows are packed per character row.
+ *
+ * Returns: (transfer full): the rendered string, or %NULL if @str is too long
+ *   to encode.
+ */
+char *
+nmc_wifi_qr_render_string(const char *str)
+{
+    uint8_t  qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)];
+    GString *gstr;
+    int      size;
+    int      x;
+    int      y;
+
+    size = _qr_encode(str, qrcode);
+    if (size < 0)
+        return NULL;
+
+    gstr = g_string_sized_new(2048);
+
+    for (y = -2; y < size + 2; y += 2) {
+        for (x = -2; x < size + 2; x++) {
+            bool top    = qrcodegen_getModule(qrcode, x, y);
+            bool bottom = qrcodegen_getModule(qrcode, x, y + 1);
+
+            if (top && bottom)
+                g_string_append(gstr, "█");
+            else if (top)
+                g_string_append(gstr, "▀");
+            else if (bottom)
+                g_string_append(gstr, "▄");
+            else
+                g_string_append_c(gstr, ' ');
+        }
+        if (y + 2 < size + 2)
+            g_string_append_c(gstr, '\n');
+    }
+
+    return g_string_free(gstr, FALSE);
+}
+
 /**
  * nmc_utils_read_passwd_file:
  * @passwd_file: file with passwords to parse
diff --git a/src/libnmc-base/nm-client-utils.h b/src/libnmc-base/nm-client-utils.h
index 7d6ec707..c90219a5 100644
--- a/src/libnmc-base/nm-client-utils.h
+++ b/src/libnmc-base/nm-client-utils.h
@@ -62,6 +62,18 @@ const char *nmc_password_subst_char(void);
 
 void nmc_print_qrcode(const char *str);
 
+char *nmc_wifi_qr_uri_new(const char *ssid, const char *key_mgmt, const char *psk, gboolean hidden);
+
+char *nmc_wifi_qr_render_string(const char *str);
+
+/* Whether @key_mgmt denotes a passphrase-protected network (a "WPA"-type QR
+ * code), i.e. one whose QR code is useless without the password. */
+static inline gboolean
+nmc_wifi_key_mgmt_uses_psk(const char *key_mgmt)
+{
+    return NM_IN_STRSET(key_mgmt, "wpa-psk", "sae", "wpa-none");
+}
+
 GHashTable *nmc_utils_parse_passwd_file(char *contents, gssize *out_error_line, GError **error);
 
 GHashTable *
diff --git a/src/libnmc-base/tests/meson.build b/src/libnmc-base/tests/meson.build
new file mode 100644
index 00000000..e3119563
--- /dev/null
+++ b/src/libnmc-base/tests/meson.build
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-client-utils',
+  'test-client-utils.c',
+  dependencies: [
+    libnm_dep,
+    glib_dep,
+  ],
+  link_with: [
+    libnmc_base,
+    libnm_core_aux_extern,
+    libnm_core_aux_intern,
+    libnm_base,
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+)
+
+test(
+  'src/libnmc-base/tests/test-client-utils',
+  test_script,
+  args: test_args + [exe.full_path()],
+)
diff --git a/src/libnmc-base/tests/test-client-utils.c b/src/libnmc-base/tests/test-client-utils.c
new file mode 100644
index 00000000..41ea7262
--- /dev/null
+++ b/src/libnmc-base/tests/test-client-utils.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "libnmc-base/nm-client-utils.h"
+
+#include "libnm-glib-aux/nm-test-utils.h"
+
+/*****************************************************************************/
+
+static void
+_check_uri(const char *ssid,
+           const char *key_mgmt,
+           const char *psk,
+           gboolean    hidden,
+           const char *expected)
+{
+    gs_free char *uri = nmc_wifi_qr_uri_new(ssid, key_mgmt, psk, hidden);
+
+    g_assert_cmpstr(uri, ==, expected);
+}
+
+static void
+test_wifi_qr_uri_type(void)
+{
+    _check_uri("MyNet", NULL, NULL, FALSE, "WIFI:T:nopass;S:MyNet;;");
+    _check_uri("MyNet", "none", NULL, FALSE, "WIFI:T:WEP;S:MyNet;;");
+    _check_uri("MyNet", "ieee8021x", NULL, FALSE, "WIFI:T:WEP;S:MyNet;;");
+    _check_uri("MyNet", "wpa-psk", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;");
+    _check_uri("MyNet", "wpa-none", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;");
+    _check_uri("MyNet", "sae", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;");
+    _check_uri("MyNet", "owe", NULL, FALSE, "WIFI:T:nopass;S:MyNet;;");
+
+    /* Unhandled key_mgmt: no T: tag is emitted. */
+    _check_uri("MyNet", "wpa-eap", "passXY", FALSE, "WIFI:S:MyNet;P:passXY;;");
+}
+
+static void
+test_wifi_qr_uri_hidden(void)
+{
+    _check_uri("MyNet", "wpa-psk", "passXY", TRUE, "WIFI:T:WPA;S:MyNet;P:passXY;H:true;;");
+}
+
+static void
+test_wifi_qr_uri_escaping(void)
+{
+    /* Each of \ " : ; , is backslash-escaped. */
+    _check_uri("a;b:c,d\\e\"f", NULL, NULL, FALSE, "WIFI:T:nopass;S:a\\;b\\:c\\,d\\\\e\\\"f;;");
+
+    /* An all-hex value is wrapped in double quotes (per the MECARD format). */
+    _check_uri("MyNet", "wpa-psk", "deadbeef", FALSE, "WIFI:T:WPA;S:MyNet;P:\"deadbeef\";;");
+
+    /* A 64-hex-character PSK is also quoted. */
+    _check_uri("MyNet",
+               "wpa-psk",
+               "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+               FALSE,
+               "WIFI:T:WPA;S:MyNet;P:\"0123456789abcdef0123456789abcdef0123456789abcdef012345"
+               "6789abcdef\";;");
+
+    /* Mixing hex and non-hex disables quoting. */
+    _check_uri("MyNet", "wpa-psk", "dead beef", FALSE, "WIFI:T:WPA;S:MyNet;P:dead beef;;");
+
+    /* An empty value is not quoted (it is not "all-hex"). */
+    _check_uri("", NULL, NULL, FALSE, "WIFI:T:nopass;S:;;");
+}
+
+/*****************************************************************************/
+
+static void
+test_wifi_qr_render(void)
+{
+    gs_free char      *qr    = nmc_wifi_qr_render_string("WIFI:T:WPA;S:MyNet;P:passXY;;");
+    gs_strfreev char **lines = NULL;
+
+    g_assert(qr);
+    /* Multiple rows packed two modules per character. */
+    lines = g_strsplit(qr, "\n", -1);
+    g_assert_cmpint(g_strv_length(lines), >, 1);
+}
+
+static void
+test_wifi_qr_render_too_long(void)
+{
+    /* Lowercase forces byte mode (capacity 2953 at version 40), so 4000 bytes
+     * exceed it and encoding fails. */
+    gs_free char *str = g_strnfill(4000, 'x');
+    gs_free char *qr  = nmc_wifi_qr_render_string(str);
+
+    g_assert(!qr);
+}
+
+static void
+test_wifi_key_mgmt_uses_psk(void)
+{
+    g_assert(nmc_wifi_key_mgmt_uses_psk("wpa-psk"));
+    g_assert(nmc_wifi_key_mgmt_uses_psk("sae"));
+    g_assert(nmc_wifi_key_mgmt_uses_psk("wpa-none"));
+
+    g_assert(!nmc_wifi_key_mgmt_uses_psk(NULL));
+    g_assert(!nmc_wifi_key_mgmt_uses_psk("none"));
+    g_assert(!nmc_wifi_key_mgmt_uses_psk("owe"));
+    g_assert(!nmc_wifi_key_mgmt_uses_psk("wpa-eap"));
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init(&argc, &argv, TRUE);
+
+    g_test_add_func("/client-utils/wifi-qr/uri/type", test_wifi_qr_uri_type);
+    g_test_add_func("/client-utils/wifi-qr/uri/hidden", test_wifi_qr_uri_hidden);
+    g_test_add_func("/client-utils/wifi-qr/uri/escaping", test_wifi_qr_uri_escaping);
+    g_test_add_func("/client-utils/wifi-qr/render", test_wifi_qr_render);
+    g_test_add_func("/client-utils/wifi-qr/render-too-long", test_wifi_qr_render_too_long);
+    g_test_add_func("/client-utils/wifi/key-mgmt-uses-psk", test_wifi_key_mgmt_uses_psk);
+
+    return g_test_run();
+}