commit eead1fd2f7193bed2bbece8aeebfa04571030f6e
Author: Alexander Zubkov <green@qrator.net>
Date:   Wed Jun 14 00:08:23 2023 +0200

    Conf: new bytestring syntax hex("..."), allow multiline strings

diff --git a/conf/cf-lex.l b/conf/cf-lex.l
index 9025a84d..dd607380 100644
--- a/conf/cf-lex.l
+++ b/conf/cf-lex.l
@@ -256,37 +256,26 @@ WHITE [ \t]
 }
 
 ({XIGIT}{2}){16,}|{XIGIT}{2}(:{XIGIT}{2}){15,}|hex:({XIGIT}{2}(:?{XIGIT}{2})*)? {
-  char *s, *sb = yytext;
-  size_t len = 0, i;
+  char *str = yytext;
+  size_t len;
   struct bytestring *bytes;
-  byte *b;
 
   /* skip 'hex:' prefix */
-  if (sb[0] == 'h' && sb[1] == 'e' && sb[2] == 'x' && sb[3] == ':')
-    sb += 4;
-
-  s = sb;
-  while (*s) {
-    len++;
-    s += 2;
-    if (*s == ':')
-      s++;
-  }
+  if (str[0] == 'h' && str[1] == 'e' && str[2] == 'x' && str[3] == ':')
+    str += 4;
+
+  errno = 0;
+  len = bstrhextobin(str, 0);
+  if (errno || len == (size_t)-1)
+    cf_error("Invalid hex string");
+
   bytes = cfg_allocz(sizeof(*bytes) + len);
 
   bytes->length = len;
-  b = &bytes->data[0];
-  s = sb;
-  errno = 0;
-  for (i = 0; i < len; i++) {
-    *b = bstrtobyte16(s);
-    if (errno == ERANGE)
-      cf_error("Invalid hex string");
-    b++;
-    s += 2;
-    if (*s == ':')
-      s++;
-  }
+  bstrhextobin(str, bytes->data);
+  if (errno)
+    cf_error("Invalid hex string");
+
   cf_lval.bs = bytes;
   return BYTESTRING;
 }
@@ -361,7 +350,6 @@ else: {
   quoted_buffer_init();
 }
 
-<QUOTED>\n	cf_error("Unterminated string");
 <QUOTED><<EOF>> cf_error("Unterminated string");
 <QUOTED>["]	{
   BEGIN(INITIAL);
@@ -370,7 +358,7 @@ else: {
   return TEXT;
 }
 
-<QUOTED>.	BUFFER_PUSH(quoted_buffer) = yytext[0];
+<QUOTED>(.|\n)	BUFFER_PUSH(quoted_buffer) = yytext[0];
 
 <INITIAL,COMMENT><<EOF>>	{ if (check_eof()) return END; }
 
diff --git a/conf/confbase.Y b/conf/confbase.Y
index 3e8f5807..4ee628de 100644
--- a/conf/confbase.Y
+++ b/conf/confbase.Y
@@ -10,6 +10,7 @@ CF_HDR
 
 #define PARSER 1
 
+#include <errno.h>
 #include "nest/bird.h"
 #include "conf/conf.h"
 #include "lib/resource.h"
@@ -107,6 +108,7 @@ CF_DECLS
 %token <s> CF_SYM_KNOWN CF_SYM_UNDEFINED
 %token <t> TEXT
 %token <bs> BYTESTRING
+%type <bs> bytestring
 %type <iface> ipa_scope
 
 %type <i> expr bool pxlen4
@@ -131,9 +133,12 @@ CF_DECLS
 %start config
 
 CF_KEYWORDS(DEFINE, ON, OFF, YES, NO, S, MS, US, PORT, VPN, MPLS, FROM)
+CF_KEYWORDS(HEX)
 
 CF_GRAMMAR
 
+kw_sym: HEX ;
+
 /* Basic config file structure */
 
 config: conf_entries END { return 0; }
@@ -395,6 +400,27 @@ opttext:
  | /* empty */ { $$ = NULL; }
  ;
 
+bytestring:
+   BYTESTRING
+ | HEX '(' TEXT ')' {
+     size_t len;
+     struct bytestring *bytes;
+
+     errno = 0;
+     len = bstrhextobin($3, 0);
+     if (errno || len == (size_t)-1)
+       cf_error("Invalid hex string");
+
+     bytes = cfg_allocz(sizeof(*bytes) + len);
+     $$ = bytes;
+
+     bytes->length = len;
+     bstrhextobin($3, bytes->data);
+     if (errno)
+       cf_error("Invalid hex string");
+   }
+ ;
+
 
 CF_CODE
 
diff --git a/lib/Makefile b/lib/Makefile
index 812f721c..f83a34eb 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -1,4 +1,4 @@
-src := bitmap.c bitops.c blake2s.c blake2b.c checksum.c event.c flowspec.c idm.c ip.c lists.c mac.c md5.c mempool.c net.c patmatch.c printf.c resource.c sha1.c sha256.c sha512.c slab.c slists.c strtoul.c tbf.c timer.c xmalloc.c
+src := bitmap.c bitops.c blake2s.c blake2b.c checksum.c event.c flowspec.c idm.c ip.c lists.c mac.c md5.c mempool.c net.c patmatch.c printf.c resource.c sha1.c sha256.c sha512.c slab.c slists.c strtobin.c strtoul.c tbf.c timer.c xmalloc.c
 obj := $(src-o-files)
 $(all-daemon)
 
diff --git a/lib/string.h b/lib/string.h
index 2829943d..51c407c1 100644
--- a/lib/string.h
+++ b/lib/string.h
@@ -33,6 +33,8 @@ u64 bstrtoul10(const char *str, char **end);
 u64 bstrtoul16(const char *str, char **end);
 byte bstrtobyte16(const char *str);
 
+int bstrhextobin(const char *s, byte *d);
+
 int patmatch(const byte *pat, const byte *str);
 
 static inline char *xbasename(const char *str)
diff --git a/lib/strtobin.c b/lib/strtobin.c
new file mode 100644
index 00000000..7a4a04a6
--- /dev/null
+++ b/lib/strtobin.c
@@ -0,0 +1,52 @@
+/*
+ *	BIRD Library -- Parse binary sequences
+ *
+ *	(c) 2023 TODO
+ *
+ *	Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#include "nest/bird.h"
+#include "lib/string.h"
+
+#include <errno.h>
+
+static int
+is_alnum(char c)
+{
+  if (c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')
+    return 1;
+  else
+    return 0;
+}
+
+int
+bstrhextobin(const char *s, byte *d)
+{
+  size_t len = 0;
+
+  while (*s) {
+    if (!is_alnum(s[0])) {
+      s++;
+      continue;
+    }
+
+    if (!is_alnum(s[1])) {
+      errno = ERANGE;
+      return -1;
+    }
+
+    if (d) {
+      errno = 0;
+      *d = bstrtobyte16(s);
+      if (errno)
+        return -1;
+      d++;
+    }
+
+    s += 2;
+    len++;
+  }
+
+  return len;
+}
diff --git a/nest/config.Y b/nest/config.Y
index c83c715b..6721dd6f 100644
--- a/nest/config.Y
+++ b/nest/config.Y
@@ -126,6 +126,7 @@ CF_KEYWORDS(GRACEFUL, RESTART, WAIT, MAX, FLUSH, AS)
 CF_KEYWORDS(MIN, IDLE, RX, TX, INTERVAL, MULTIPLIER, PASSIVE)
 CF_KEYWORDS(CHECK, LINK)
 CF_KEYWORDS(SORTED, TRIE, MIN, MAX, SETTLE, TIME, GC, THRESHOLD, PERIOD)
+CF_KEYWORDS(HEX)
 
 /* For r_args_channel */
 CF_KEYWORDS(IPV4, IPV4_MC, IPV4_MPLS, IPV6, IPV6_MC, IPV6_MPLS, IPV6_SADR, VPN4, VPN4_MC, VPN4_MPLS, VPN6, VPN6_MC, VPN6_MPLS, ROA4, ROA6, FLOW4, FLOW6, MPLS, PRI, SEC)
@@ -154,7 +155,7 @@ CF_ENUM_PX(T_ENUM_AF, AF_, AFI_, IPV4, IPV6)
 
 CF_GRAMMAR
 
-kw_sym: MIN MAX ;
+kw_sym: MIN | MAX ;
 
 /* Setting of router ID */
 
@@ -548,7 +549,7 @@ pass_key: PASSWORD | KEY;
 
 password_item_begin:
     pass_key text { init_password_list(); init_password($2, strlen($2), password_id++); }
-  | pass_key BYTESTRING { init_password_list(); init_password($2->data, $2->length, password_id++); }
+  | pass_key bytestring { init_password_list(); init_password($2->data, $2->length, password_id++); }
 ;
 
 password_item_params:
diff --git a/proto/radv/config.Y b/proto/radv/config.Y
index 5c213d50..8f5fb2d0 100644
--- a/proto/radv/config.Y
+++ b/proto/radv/config.Y
@@ -71,7 +71,7 @@ radv_proto_item:
  | PREFIX radv_prefix { add_tail(&RADV_CFG->pref_list, NODE this_radv_prefix); }
  | RDNSS { init_list(&radv_dns_list); } radv_rdnss { add_tail_list(&RADV_CFG->rdnss_list, &radv_dns_list); }
  | DNSSL { init_list(&radv_dns_list); } radv_dnssl { add_tail_list(&RADV_CFG->dnssl_list, &radv_dns_list); }
- | OTHER TYPE expr BYTESTRING { radv_add_to_custom_list(&RADV_CFG->custom_list, $3, $4); }
+ | OTHER TYPE expr bytestring { radv_add_to_custom_list(&RADV_CFG->custom_list, $3, $4); }
  | TRIGGER net_ip6 { RADV_CFG->trigger = $2; }
  | PROPAGATE ROUTES bool { RADV_CFG->propagate_routes = $3; }
  ;
@@ -136,7 +136,7 @@ radv_iface_item:
  | PREFIX radv_prefix { add_tail(&RADV_IFACE->pref_list, NODE this_radv_prefix); }
  | RDNSS { init_list(&radv_dns_list); } radv_rdnss { add_tail_list(&RADV_IFACE->rdnss_list, &radv_dns_list); }
  | DNSSL { init_list(&radv_dns_list); } radv_dnssl { add_tail_list(&RADV_IFACE->dnssl_list, &radv_dns_list); }
- | OTHER TYPE expr BYTESTRING { radv_add_to_custom_list(&RADV_IFACE->custom_list, $3, $4); }
+ | OTHER TYPE expr bytestring { radv_add_to_custom_list(&RADV_IFACE->custom_list, $3, $4); }
  | RDNSS LOCAL bool { RADV_IFACE->rdnss_local = $3; }
  | DNSSL LOCAL bool { RADV_IFACE->dnssl_local = $3; }
  | OTHER LOCAL bool { RADV_IFACE->custom_local = $3; }
