]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
sbuff: Add the ability to process escape character sequences like \x<hex><hex> and...
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Mon, 27 Jul 2020 16:44:25 +0000 (12:44 -0400)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Mon, 27 Jul 2020 16:44:29 +0000 (12:44 -0400)
src/lib/util/sbuff.c
src/lib/util/sbuff.h
src/lib/util/sbuff_tests.c

index f96a3c6acbea85479b364772a04eea8b641d16bd..1874cc69c28a5d7030d30444017d45b598a32141 100644 (file)
@@ -299,8 +299,7 @@ int fr_sbuff_trim_talloc(fr_sbuff_t *sbuff, size_t len)
  */
 #define FILL_OR_GOTO_DONE(_out, _in, _len) \
 do { \
-       ssize_t _copied; \
-       _copied = fr_sbuff_in_bstrncpy(_out, fr_sbuff_current(_in), _len); \
+       ssize_t _copied = fr_sbuff_in_bstrncpy(_out, fr_sbuff_current(_in), _len); \
        if (_copied < 0) { \
                fr_sbuff_advance(_in, fr_sbuff_in_bstrncpy(_out, fr_sbuff_current(_in), _len + _copied)); \
                goto done;\
@@ -455,14 +454,14 @@ done:
  * @param[in] in               Where to copy from.  Will copy len bytes from current position in buffer.
  * @param[in] len              How many bytes to copy.  If SIZE_MAX the entire buffer will be copied.
  * @param[in] until            Characters which stop the copy operation.
- * @param[in] escape           If not '\0', ignore characters in the until set when
+ * @param[in] escape_chr       If not '\0', ignore characters in the until set when
  *                             prefixed with this escape character.
  * @return
  *     - 0 no bytes copied.
  *     - >0 the number of bytes copied.
  */
 size_t fr_sbuff_out_bstrncpy_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
-                                  bool const until[static UINT8_MAX + 1], char escape)
+                                  bool const until[static UINT8_MAX + 1], char escape_chr)
 {
        fr_sbuff_t      our_in = FR_SBUFF_COPY(in);
        bool            do_escape = false;      /* Track state across extensions */
@@ -478,13 +477,13 @@ size_t fr_sbuff_out_bstrncpy_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
                p = our_in.p;
                end = CONSTRAINED_END(&our_in, len, fr_sbuff_used_total(&our_in));
 
-               if (escape == '\0') {
+               if (escape_chr == '\0') {
                        while((p < end) && !until[(uint8_t)*p]) p++;
                } else {
                        while (p < end) {
                                if (do_escape) {
                                        do_escape = false;
-                               } else if (*p == escape) {
+                               } else if (*p == escape_chr) {
                                        do_escape = true;
                                } else if (until[(uint8_t)*p]) {
                                        break;
@@ -515,23 +514,20 @@ done:
  * @param[in] in               Where to copy from.  Will copy len bytes from current position in buffer.
  * @param[in] len              How many bytes to copy.  If SIZE_MAX the entire buffer will be copied.
  * @param[in] until            Characters which stop the copy operation.
- * @param[in] escape           Ignore characters in the until set when prefixed
- *                             with this escape character. Must be specified.
- * @param[in] escape_subs      Escape characters and their substitutions.
+ * @param[in] rules            for processing escape sequences.
  * @return
  *     - 0 no bytes copied.
  *     - >0 the number of bytes copied including escape sequences.
  */
 size_t fr_sbuff_out_unescape_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
                                  bool const until[static UINT8_MAX + 1],
-                                 char escape,
-                                 char const escape_subs[static UINT8_MAX + 1])
+                                 fr_sbuff_escape_rules_t const *rules)
 {
        fr_sbuff_t      our_in = FR_SBUFF_COPY(in);
        bool            do_escape = false;      /* Track state across extensions */
 
        CHECK_SBUFF_INIT(in);
-       if (unlikely(escape == '\0')) return 0;
+       if (unlikely(rules->chr == '\0')) return 0;
 
        do {
                char    *p;
@@ -547,9 +543,58 @@ size_t fr_sbuff_out_unescape_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
                                do_escape = false;
 
                                /*
-                                *      Not an actual escape seq
+                                *      Check for \x<hex><hex>
                                 */
-                               if (escape_subs[(uint8_t)*p] == '\0') goto next;
+                               if (rules->do_hex && (*p == 'x')) {
+                                       uint8_t                 escape;
+                                       fr_sbuff_marker_t       m;
+
+                                       fr_sbuff_marker(&m, &our_in);           /* allow for backtrack */
+                                       fr_sbuff_set(&our_in, ++p);             /* sync sbuff */
+
+                                       if (fr_sbuff_out_uint8_hex(NULL, &escape, &our_in, false) != 2) {
+                                               fr_sbuff_set_to_marker(&m);     /* backtrack */
+                                               p = m.p;
+                                               goto check_subs;
+                                       }
+
+                                       if (fr_sbuff_in_char(out, escape) <= 0) {
+                                               fr_sbuff_set_to_marker(&m);     /* backtrack */
+                                               goto done;
+                                       }
+                                       p = our_in.p;
+                                       continue;
+                               }
+
+                               /*
+                                *      Check for \<oct><oct><oct>
+                                */
+                               if (rules->do_oct && isdigit(*p)) {
+                                       uint8_t                 escape;
+                                       fr_sbuff_marker_t       m;
+
+                                       fr_sbuff_marker(&m, &our_in);           /* allow for backtrack */
+                                       fr_sbuff_set(&our_in, p);               /* sync sbuff */
+
+                                       if (fr_sbuff_out_uint8_oct(NULL, &escape, &our_in, false) != 3) {
+                                               fr_sbuff_set_to_marker(&m);     /* backtrack */
+                                               p = m.p;
+                                               goto check_subs;
+                                       }
+
+                                       if (fr_sbuff_in_char(out, escape) <= 0) {
+                                               fr_sbuff_set_to_marker(&m);     /* backtrack */
+                                               goto done;
+                                       }
+                                       p = our_in.p;
+                                       continue;
+                               }
+
+                       check_subs:
+                               /*
+                                *      Not a recognised escape sequence.
+                                */
+                               if (rules->subs[(uint8_t)*p] == '\0') goto next;
 
                                /*
                                 *      We already copied everything up
@@ -557,18 +602,17 @@ size_t fr_sbuff_out_unescape_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
                                 *      write the substituted char to
                                 *      the output buffer.
                                 */
-                               if (fr_sbuff_in_char(out, escape_subs[(uint8_t)*p]) <= 0) goto done;
+                               if (fr_sbuff_in_char(out, rules->subs[(uint8_t)*p++]) <= 0) goto done;
 
                                /*
                                 *      ...and advance past the entire
                                 *      escape seq in the input buffer.
                                 */
                                fr_sbuff_advance(&our_in, 2);
-                               p++;
                                continue;
                        }
 
-                       if (*p == escape) {
+                       if (*p == rules->chr) {
                                /*
                                 *      Copy out any data we got before
                                 *      we hit the escape char.
@@ -1105,11 +1149,11 @@ size_t fr_sbuff_adv_past_allowed(fr_sbuff_t *sbuff, size_t len, bool const allow
  * @param[in] sbuff            sbuff to search in.
  * @param[in] len              Maximum amount to advance by. Unconstrained if SIZE_MAX.
  * @param[in] until            character set.
- * @param[in] escape           If not '\0', ignore characters in the until set when
+ * @param[in] escape_chr       If not '\0', ignore characters in the until set when
  *                             prefixed with this escape character.
  * @return how many bytes we advanced.
  */
-size_t fr_sbuff_adv_until(fr_sbuff_t *sbuff, size_t len, bool const until[static UINT8_MAX + 1], char escape)
+size_t fr_sbuff_adv_until(fr_sbuff_t *sbuff, size_t len, bool const until[static UINT8_MAX + 1], char escape_chr)
 {
        size_t          total = 0;
        char const      *p;
@@ -1125,13 +1169,13 @@ size_t fr_sbuff_adv_until(fr_sbuff_t *sbuff, size_t len, bool const until[static
                end = CONSTRAINED_END(sbuff, len, total);
                p = sbuff->p;
 
-               if (escape == '\0') {
+               if (escape_chr == '\0') {
                        while ((p < end) && !until[(uint8_t)*p]) p++;
                } else {
                        while (p < end) {
                                if (do_escape) {
                                        do_escape = false;
-                               } else if (*p == escape) {
+                               } else if (*p == escape_chr) {
                                        do_escape = true;
                                } else if (until[(uint8_t)*p]) {
                                        break;
index 35469eaf6b2cd28da2731f92e98c91baa4c306d2..125779b48a33a5768ee6f57aa017cd9b946f43ff 100644 (file)
@@ -672,12 +672,21 @@ size_t    fr_sbuff_out_bstrncpy_allowed(fr_sbuff_t *out, fr_sbuff_t *in, size_t len
                                      bool const allowed[static UINT8_MAX + 1]);
 
 size_t fr_sbuff_out_bstrncpy_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
-                                   bool const until[static UINT8_MAX + 1], char escape);
+                                   bool const until[static UINT8_MAX + 1], char escape_chr);
+
+/** Set of parsing rules for *unescape_until functions
+ *
+ */
+typedef struct {
+       char            chr;                            //!< Character at the start of an escape sequence.
+       char const      subs[UINT8_MAX + 1];            //!< Special characters and their substitutions.
+       bool            do_hex;                         //!< Process hex sequences i.e. \x<hex><hex>.
+       bool            do_oct;                         //!< Process oct sequences i.e. \<oct><oct><oct>.
+} fr_sbuff_escape_rules_t;
 
 size_t fr_sbuff_out_unescape_until(fr_sbuff_t *out, fr_sbuff_t *in, size_t len,
-                                 bool const until[static UINT8_MAX + 1],
-                                 char escape,
-                                 char const escape_subs[static UINT8_MAX + 1]);
+                                  bool const until[static UINT8_MAX + 1],
+                                  fr_sbuff_escape_rules_t const *rules);
 
 /** Find the longest prefix in an sbuff
  *
@@ -733,8 +742,13 @@ static inline size_t fr_sbuff_out_abstrncpy_allowed(TALLOC_CTX *ctx, char **out,
 SBUFF_OUT_TALLOC_FUNC_DEF(fr_sbuff_out_bstrncpy_allowed, in, len, allowed);
 
 static inline size_t fr_sbuff_out_abstrncpy_until(TALLOC_CTX *ctx, char **out, fr_sbuff_t *in, size_t len,
-                                                   bool const until[static UINT8_MAX + 1], char escape)
-SBUFF_OUT_TALLOC_FUNC_DEF(fr_sbuff_out_bstrncpy_until, in, len, until, escape);
+                                                   bool const until[static UINT8_MAX + 1], char escape_chr)
+SBUFF_OUT_TALLOC_FUNC_DEF(fr_sbuff_out_bstrncpy_until, in, len, until, escape_chr);
+
+static inline size_t fr_sbuff_out_aunescape_until(TALLOC_CTX *ctx, char **out, fr_sbuff_t *in, size_t len,
+                                                 bool const until[static UINT8_MAX + 1],
+                                                 fr_sbuff_escape_rules_t const *rules)
+SBUFF_OUT_TALLOC_FUNC_DEF(fr_sbuff_out_unescape_until, in, len, until, rules);
 /** @} */
 
 /** @name Look for a token in a particular format, parse it, and write it to the output pointer
@@ -752,19 +766,11 @@ size_t fr_sbuff_out_uint16(fr_sbuff_parse_error_t *err, uint16_t *out, fr_sbuff_
 size_t fr_sbuff_out_uint32(fr_sbuff_parse_error_t *err, uint32_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint64(fr_sbuff_parse_error_t *err, uint64_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 
-size_t fr_sbuff_out_int8_oct(fr_sbuff_parse_error_t *err, int8_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int16_oct(fr_sbuff_parse_error_t *err, int16_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int32_oct(fr_sbuff_parse_error_t *err, int32_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int64_oct(fr_sbuff_parse_error_t *err, int64_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint8_oct(fr_sbuff_parse_error_t *err, uint8_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint16_oct(fr_sbuff_parse_error_t *err, uint16_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint32_oct(fr_sbuff_parse_error_t *err, uint32_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint64_oct(fr_sbuff_parse_error_t *err, uint64_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 
-size_t fr_sbuff_out_int8_hex(fr_sbuff_parse_error_t *err, int8_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int16_hex(fr_sbuff_parse_error_t *err, int16_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int32_hex(fr_sbuff_parse_error_t *err, int32_t *out, fr_sbuff_t *sbuff, bool no_trailing);
-size_t fr_sbuff_out_int64_hex(fr_sbuff_parse_error_t *err, int64_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint8_hex(fr_sbuff_parse_error_t *err, uint8_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint16_hex(fr_sbuff_parse_error_t *err, uint16_t *out, fr_sbuff_t *sbuff, bool no_trailing);
 size_t fr_sbuff_out_uint32_hex(fr_sbuff_parse_error_t *err, uint32_t *out, fr_sbuff_t *sbuff, bool no_trailing);
@@ -815,7 +821,7 @@ size_t      fr_sbuff_adv_past_whitespace(fr_sbuff_t *sbuff, size_t len);
 
 size_t fr_sbuff_adv_past_allowed(fr_sbuff_t *sbuff, size_t len, bool const allowed[static UINT8_MAX + 1]);
 
-size_t fr_sbuff_adv_until(fr_sbuff_t *sbuff, size_t len, bool const until[static UINT8_MAX + 1], char escape);
+size_t fr_sbuff_adv_until(fr_sbuff_t *sbuff, size_t len, bool const until[static UINT8_MAX + 1], char escape_chr);
 
 char   *fr_sbuff_adv_to_chr_utf8(fr_sbuff_t *in, size_t len, char const *chr);
 
@@ -890,6 +896,12 @@ static inline bool fr_sbuff_is_space(fr_sbuff_t *sbuff)
        if (FR_SBUFF_CANT_EXTEND(sbuff)) return false;
        return isspace(*sbuff->p);
 }
+
+static inline bool fr_sbuff_is_hex(fr_sbuff_t *sbuff)
+{
+       if (FR_SBUFF_CANT_EXTEND(sbuff)) return false;
+       return (isdigit(*sbuff->p) || ((tolower(*sbuff->p) >= 'a') && (tolower(*sbuff->p) <= 'f')));
+}
 /** @} */
 
 #ifdef __cplusplus
index 93805ff7f057894f20aae17c2a588a115762bae2..3067b42cd1c27dba2d8f619be887a0dad7796465 100644 (file)
@@ -387,13 +387,21 @@ static void test_bstrncpy_until(void)
 
 static void test_unescape_until(void)
 {
-       char const      in[] = "i am a test string";
-       char const      in_long[] = "i am a longer test string";
-       char const      in_escapes[] = "i am a |t|est strin|g";
-       char            out[18 + 1];
-       char            escape_out[20 + 1];
-       fr_sbuff_t      sbuff;
-       ssize_t         slen;
+       char const              in[] = "i am a test string";
+       char const              in_long[] = "i am a longer test string";
+       char const              in_escapes[] = "i am a |t|est strin|g";
+       char const              in_escapes_seq[] = "i |x|0am a |t|est strin|g|x20|040";
+       char                    out[18 + 1];
+       char                    escape_out[20 + 1];
+
+       fr_sbuff_t              sbuff;
+       ssize_t                 slen;
+       fr_sbuff_escape_rules_t rules = { .chr = '\\' };
+       fr_sbuff_escape_rules_t pipe_rules = { .chr = '|', .subs = { ['g'] = 'g' } };
+       fr_sbuff_escape_rules_t pipe_rules_sub = { .chr = '|', .subs = { ['g'] = 'h' } };
+       fr_sbuff_escape_rules_t pipe_rules_sub_hex = { .chr = '|', .subs = { ['g'] = 'h' }, .do_hex = true };
+       fr_sbuff_escape_rules_t pipe_rules_sub_oct = { .chr = '|', .subs = { ['g'] = 'h' }, .do_oct = true };
+       fr_sbuff_escape_rules_t pipe_rules_both = { .chr = '|', .subs = { ['g'] = 'h' }, .do_hex = true, .do_oct = true };
 
        fr_sbuff_init(&sbuff, in, sizeof(in));
 
@@ -404,14 +412,14 @@ static void test_unescape_until(void)
         */
        TEST_CASE("Copy 5 bytes to out");
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff, 5,
-                                          (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(5, slen);
        TEST_CHECK_STRCMP("i am ", out);
        TEST_CHECK_STRCMP("a test string", sbuff.p);
 
        TEST_CASE("Copy 13 bytes to out");
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff, 13,
-                                          (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(13, slen);
        TEST_CHECK_STRCMP("a test string", out);
        TEST_CHECK_STRCMP("", sbuff.p);
@@ -419,7 +427,7 @@ static void test_unescape_until(void)
 
        TEST_CASE("Copy would overrun input");
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff, 1,
-                                          (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(0, slen);
        TEST_CHECK(sbuff.p == sbuff.end);
 
@@ -427,7 +435,7 @@ static void test_unescape_until(void)
        fr_sbuff_init(&sbuff, in_long, sizeof(in_long));
 
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff,
-                                          SIZE_MAX, (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          SIZE_MAX, (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(18, slen);
        TEST_CHECK_STRCMP("i am a longer test", out);
 
@@ -435,7 +443,7 @@ static void test_unescape_until(void)
        fr_sbuff_set_to_start(&sbuff);
        out[0] = 'a';
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, (size_t)1), &sbuff,
-                                          SIZE_MAX, (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          SIZE_MAX, (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(0, slen);
        TEST_CHECK(out[0] == '\0');     /* should be set to \0 */
        TEST_CHECK(sbuff.p == sbuff.start);
@@ -444,7 +452,7 @@ static void test_unescape_until(void)
        fr_sbuff_set_to_start(&sbuff);
        out[0] = 'a';
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff, 0,
-                                          (bool[UINT8_MAX + 1]){ }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ }, &rules);
        TEST_CHECK_SLEN(0, slen);
        TEST_CHECK(out[0] == '\0');     /* should be set to \0 */
        TEST_CHECK(sbuff.p == sbuff.start);
@@ -455,28 +463,28 @@ static void test_unescape_until(void)
        TEST_CASE("Copy until first t");
        fr_sbuff_set_to_start(&sbuff);
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, sizeof(out)), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, &rules);
        TEST_CHECK_SLEN(14, slen);
        TEST_CHECK_STRCMP("i am a longer ", out);
 
        TEST_CASE("Copy until first t with length constraint (same len as token)");
        fr_sbuff_set_to_start(&sbuff);
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, 15), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, &rules);
        TEST_CHECK_SLEN(14, slen);
        TEST_CHECK_STRCMP("i am a longer ", out);
 
        TEST_CASE("Copy until first t with length constraint (one shorter than token)");
        fr_sbuff_set_to_start(&sbuff);
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, 14), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ ['t'] = true }, &rules);
        TEST_CHECK_SLEN(13, slen);
        TEST_CHECK_STRCMP("i am a longer", out);
 
        TEST_CASE("Zero length token (should still be terminated)");
        fr_sbuff_set_to_start(&sbuff);
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(out, 14), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['i'] = true }, '\\', (char[UINT8_MAX + 1]){});
+                                          (bool[UINT8_MAX + 1]){ ['i'] = true }, &rules);
        TEST_CHECK_SLEN(0, slen);
        TEST_CHECK_STRCMP("", out);
 
@@ -486,9 +494,7 @@ static void test_unescape_until(void)
        TEST_CASE("Escape with substition to same char");
        fr_sbuff_init(&sbuff, in_escapes, sizeof(in_escapes));
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(escape_out, sizeof(escape_out)), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['g'] = true },
-                                          '|',
-                                          (char[UINT8_MAX + 1]){ ['g'] = 'g' });
+                                          (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules);
        TEST_CHECK_SLEN(21, slen);
        TEST_CHECK_STRCMP("i am a |t|est string", escape_out);
        TEST_CHECK_STRCMP("", sbuff.p);
@@ -496,12 +502,75 @@ static void test_unescape_until(void)
        TEST_CASE("Escape with substition to different char");
        fr_sbuff_init(&sbuff, in_escapes, sizeof(in_escapes));
        slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(escape_out, sizeof(escape_out)), &sbuff, SIZE_MAX,
-                                          (bool[UINT8_MAX + 1]){ ['g'] = true },
-                                          '|',
-                                          (char[UINT8_MAX + 1]){ ['g'] = 'h' });
+                                          (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_sub);
        TEST_CHECK_SLEN(21, slen);
        TEST_CHECK_STRCMP("i am a |t|est strinh", escape_out);
        TEST_CHECK_STRCMP("", sbuff.p);
+
+       {
+               char    tmp_out[24 + 1];
+
+               TEST_CASE("Escape with hex substitutions (insufficient output space)");
+               fr_sbuff_init(&sbuff, in_escapes_seq, sizeof(in_escapes_seq));
+               slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(tmp_out, sizeof(tmp_out)),
+                                                  &sbuff, SIZE_MAX,
+                                                  (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_sub_hex);
+               TEST_CHECK_SLEN(25, slen);
+               TEST_CHECK_STRCMP("i |x|0am a |t|est strinh", tmp_out);
+               TEST_CHECK_STRCMP("|x20|040", sbuff.p);
+       }
+
+       {
+               char    tmp_out[25 + 1];
+
+               TEST_CASE("Escape with hex substitutions (sufficient output space)");
+               fr_sbuff_init(&sbuff, in_escapes_seq, sizeof(in_escapes_seq));
+               slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(tmp_out, sizeof(tmp_out)),
+                                                  &sbuff, SIZE_MAX,
+                                                  (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_sub_hex);
+               TEST_CHECK_SLEN(29, slen);
+               TEST_CHECK_STRCMP("i |x|0am a |t|est strinh ", tmp_out);
+               TEST_CHECK_STRCMP("|040", sbuff.p);
+       }
+
+       {
+               char    tmp_out[28 + 1];
+
+               TEST_CASE("Escape with oct substitutions (insufficient output space)");
+               fr_sbuff_init(&sbuff, in_escapes_seq, sizeof(in_escapes_seq));
+               slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(tmp_out, sizeof(tmp_out)),
+                                                  &sbuff, SIZE_MAX,
+                                                  (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_sub_oct);
+               TEST_CHECK_SLEN(29, slen);
+               TEST_CHECK_STRCMP("i |x|0am a |t|est strinh|x20", tmp_out);
+               TEST_CHECK_STRCMP("|040", sbuff.p);
+       }
+
+       {
+               char    tmp_out[29 + 1];
+
+               TEST_CASE("Escape with oct substitutions (sufficient output space)");
+               fr_sbuff_init(&sbuff, in_escapes_seq, sizeof(in_escapes_seq));
+               slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(tmp_out, sizeof(tmp_out)),
+                                                  &sbuff, SIZE_MAX,
+                                                  (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_sub_oct);
+               TEST_CHECK_SLEN(33, slen);
+               TEST_CHECK_STRCMP("i |x|0am a |t|est strinh|x20 ", tmp_out);
+               TEST_CHECK_STRCMP("", sbuff.p);
+       }
+
+       {
+               char    tmp_out[26 + 1];
+
+               TEST_CASE("Escape with hex and oct substitutions (sufficient output space)");
+               fr_sbuff_init(&sbuff, in_escapes_seq, sizeof(in_escapes_seq));
+               slen = fr_sbuff_out_unescape_until(&FR_SBUFF_TMP(tmp_out, sizeof(tmp_out)),
+                                                  &sbuff, SIZE_MAX,
+                                                  (bool[UINT8_MAX + 1]){ ['g'] = true }, &pipe_rules_both);
+               TEST_CHECK_SLEN(33, slen);
+               TEST_CHECK_STRCMP("i |x|0am a |t|est strinh  ", tmp_out);
+               TEST_CHECK_STRCMP("", sbuff.p);
+       }
 }
 
 static void test_no_advance(void)
@@ -1160,7 +1229,7 @@ TEST_LIST = {
        { "fr_sbuff_out_bstrncpy",              test_bstrncpy },
        { "fr_sbuff_out_bstrncpy_allowed",      test_bstrncpy_allowed },
        { "fr_sbuff_out_bstrncpy_until",        test_bstrncpy_until },
-       { "fr_sbuff_out_descape_until",         test_unescape_until },
+       { "fr_sbuff_out_unescape_until",        test_unescape_until },
 
        /*
         *      Extending buffer