/*
 * Copyright The async-profiler authors
 * SPDX-License-Identifier: Apache-2.0
 */

// Code for demangling Rust symbols. This code is mostly
// a line-by-line translation of the Rust code in `rustc-demangle`.

// you can find the latest version of this code in https://github.com/rust-lang/rustc-demangle

#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdbool.h>
#include <sys/param.h>
#include <stdio.h>

#include "rustDemangle.h"

#if defined(__GNUC__) || defined(__clang__)
#define NODISCARD __attribute__((warn_unused_result))
#else
#define NODISCARD
#endif

#define MAX_DEPTH 500

typedef enum {
    DemangleOk,
    DemangleInvalid,
    DemangleRecursed,
    DemangleBug,
} demangle_status;

struct demangle_v0 {
    const char *mangled;
    size_t mangled_len;
};

struct demangle_legacy {
    const char *mangled;
    size_t mangled_len;
    size_t elements;
};

// private version of memrchr to avoid _GNU_SOURCE
static void *demangle_memrchr(const void *s, int c, size_t n) {
    const uint8_t *s_ = (uint8_t *)s;
    for (; n != 0; n--) {
        if (s_[n-1] == c) {
            return (void*)&s_[n-1];
        }
    }
    return NULL;
}


static bool unicode_iscontrol(uint32_t ch) {
    // this is *technically* a unicode table, but
    // some unicode properties are simpler than you might think
    return ch < 0x20 || (ch >= 0x7f && ch < 0xa0);
}

// "good enough" tables, the only consequence is that when printing
// *constant strings*, some characters are printed as `\u{abcd}` rather than themselves.
//
// I'm leaving these here to allow easily replacing them with actual
// tables if desired.
static bool unicode_isprint(uint32_t ch) {
    if (ch < 0x20) {
        return false;
    }
    if (ch < 0x7f) {
        return true;
    }
    return false;
}

static bool unicode_isgraphemextend(uint32_t ch) {
    (void)ch;
    return false;
}

static bool str_isascii(const char *s, size_t s_len) {
    for (size_t i = 0; i < s_len; i++) {
        if (s[i] & 0x80) {
            return false;
        }
    }

    return true;
}

typedef enum {
    PunycodeOk,
    PunycodeError
} punycode_status;

struct parser {
    // the parser assumes that `sym` has a safe "terminating byte". It might be NUL,
    // but it might also be something else if a symbol is "truncated".
    const char *sym;
    size_t sym_len;
    size_t next;
    uint32_t depth;
};

struct printer {
    demangle_status status; // if status == 0 parser is valid
    struct parser parser;
    char *out; // NULL for no output [in which case out_len is not decremented]
    size_t out_len;
    uint32_t bound_lifetime_depth;
    bool alternate;
};

static NODISCARD overflow_status printer_print_path(struct printer *printer, bool in_value);
static NODISCARD overflow_status printer_print_type(struct printer *printer);
static NODISCARD overflow_status printer_print_const(struct printer *printer, bool in_value);

static NODISCARD demangle_status try_parse_path(struct parser *parser) {
    struct printer printer = {
        DemangleOk,
        *parser,
        NULL,
        SIZE_MAX,
        0,
        false
    };
    overflow_status ignore = printer_print_path(&printer, false); // can't fail since no output
    (void)ignore;
    *parser = printer.parser;
    return printer.status;
}

NODISCARD static demangle_status rust_demangle_v0_demangle(const char *s, size_t s_len, struct demangle_v0 *res, const char **rest) {
    if (s_len > strlen(s)) {
        // s_len only exists to shorten the string, this is not a buffer API
        return DemangleInvalid;
    }

    const char *inner;
    size_t inner_len;
    if (s_len >= 2 && !strncmp(s, "_R", strlen("_R"))) {
        inner = s+2;
        inner_len = s_len - 2;
    } else if (s_len >= 1 && !strncmp(s, "R", strlen("R"))) {
        // On Windows, dbghelp strips leading underscores, so we accept "R..."
        // form too.
        inner = s+1;
        inner_len = s_len - 1;
    } else if (s_len >= 3 && !strncmp(s, "__R", strlen("__R"))) {
        // On OSX, symbols are prefixed with an extra _
        inner = s+3;
        inner_len = s_len - 3;
    } else {
        return DemangleInvalid;
    }

    // Paths always start with uppercase characters.
    if (*inner < 'A' || *inner > 'Z') {
        return DemangleInvalid;
    }

    if (!str_isascii(inner, inner_len)) {
        return DemangleInvalid;
    }

    struct parser parser = { inner, inner_len, 0, 0 };

    demangle_status status = try_parse_path(&parser);
    if (status != DemangleOk) return status;
    char next = parser.sym[parser.next];

    // Instantiating crate (paths always start with uppercase characters).
    if (parser.next < parser.sym_len && next >= 'A' && next <= 'Z') {
        status = try_parse_path(&parser);
        if (status != DemangleOk) return status;
    }

    res->mangled = inner;
    res->mangled_len = inner_len;
    if (rest) {
        *rest = parser.sym + parser.next;
    }

    return DemangleOk;
}

// This might require `len` to be up to 3 characters bigger than the real output len in case of utf-8
NODISCARD static overflow_status rust_demangle_v0_display_demangle(struct demangle_v0 res, char *out, size_t len, bool alternate) {
    struct printer printer = {
        DemangleOk,
        {
            res.mangled,
            res.mangled_len,
            0,
            0
        },
        out,
        len,
        0,
        alternate
    };
    if (printer_print_path(&printer, true) == OverflowOverflow) {
        return OverflowOverflow;
    }
    if (printer.out_len < OVERFLOW_MARGIN) {
        return OverflowOverflow;
    }
    *printer.out = '\0';
    return OverflowOk;
}

static size_t code_to_utf8(unsigned char *buffer, uint32_t code)
{
    if (code <= 0x7F) {
        buffer[0] = code;
        return 1;
    }
    if (code <= 0x7FF) {
        buffer[0] = 0xC0 | (code >> 6);            /* 110xxxxx */
        buffer[1] = 0x80 | (code & 0x3F);          /* 10xxxxxx */
        return 2;
    }
    if (code <= 0xFFFF) {
        buffer[0] = 0xE0 | (code >> 12);           /* 1110xxxx */
        buffer[1] = 0x80 | ((code >> 6) & 0x3F);   /* 10xxxxxx */
        buffer[2] = 0x80 | (code & 0x3F);          /* 10xxxxxx */
        return 3;
    }
    if (code <= 0x10FFFF) {
        buffer[0] = 0xF0 | (code >> 18);           /* 11110xxx */
        buffer[1] = 0x80 | ((code >> 12) & 0x3F);  /* 10xxxxxx */
        buffer[2] = 0x80 | ((code >> 6) & 0x3F);   /* 10xxxxxx */
        buffer[3] = 0x80 | (code & 0x3F);          /* 10xxxxxx */
        return 4;
    }
    return 0;
}


// return length of char at byte, or SIZE_MAX if invalid. buf should have 4 valid characters
static NODISCARD size_t utf8_next_char(uint8_t *s, uint32_t *ch) {
    uint8_t byte = *s;
    // UTF8-1      = %x00-7F
    // UTF8-2      = %xC2-DF UTF8-tail
    // UTF8-3      = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
    //               %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
    // UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
    //               %xF4 %x80-8F 2( UTF8-tail )
    if (byte < 0x80) {
        *ch = byte;
        return 1;
    } else if (byte < 0xc2) {
        return SIZE_MAX;
    } else if (byte < 0xe0) {
        if (s[1] >= 0x80 && s[1] < 0xc0) {
            *ch = ((byte&0x1f)<<6) + (s[1] & 0x3f);
            return 2;
        }
        return SIZE_MAX;
    } if (byte < 0xf0) {
        if (!(s[1] >= 0x80 && s[1] < 0xc0) || !(s[2] >= 0x80 && s[2] < 0xc0)) {
            return SIZE_MAX; // basic validation
        }
        if (byte == 0xe0 && s[1] < 0xa0) {
            return SIZE_MAX; // overshort
        }
        if (byte == 0xed && s[1] >= 0xa0) {
            return SIZE_MAX; // surrogate
        }
        *ch = ((byte&0x0f)<<12) + ((s[1] & 0x3f)<<6) + (s[2] & 0x3f);
        return 3;
    } else if (byte < 0xf5) {
        if (!(s[1] >= 0x80 && s[1] < 0xc0) || !(s[2] >= 0x80 && s[2] < 0xc0) || !(s[3] >= 0x80 && s[3] < 0xc0)) {
            return SIZE_MAX; // basic validation
        }
        if (byte == 0xf0 && s[1] < 0x90) {
            return SIZE_MAX; // overshort
        }
        if (byte == 0xf4 && s[1] >= 0x90) {
            return SIZE_MAX; // over max
        }
        *ch = ((byte&0x07)<<18) + ((s[1] & 0x3f)<<12) + ((s[2] & 0x3f)<<6) + (s[3]&0x3f);
        return 4;
    } else {
        return SIZE_MAX;
    }
}

static NODISCARD bool validate_char(uint32_t n) {
    return ((n ^ 0xd800) - 0x800) < 0x110000 - 0x800;
}

#define SMALL_PUNYCODE_LEN 128

static NODISCARD punycode_status punycode_decode(const char *start, size_t ascii_len, const char *punycode_start, size_t punycode_len, uint32_t (*out_)[SMALL_PUNYCODE_LEN], size_t *out_len) {
    uint32_t *out = *out_;

    if (punycode_len == 0) {
        return PunycodeError;
    }

    if (ascii_len > SMALL_PUNYCODE_LEN) {
        return PunycodeError;
    }
    for (size_t i = 0; i < ascii_len; i++) {
        out[i] = start[i];
    }
    size_t len = ascii_len;

    size_t base = 36, t_min = 1, t_max = 26, skew = 38, damp = 700, bias = 72, i = 0, n = 0x80;
    for (;;) {
        size_t delta = 0, w = 1, k = 0;
        for (;;) {
            k += base;
            size_t biased = k < bias ? 0 : k - bias;
            size_t t = MIN(MAX(biased, t_min), t_max);
            size_t d;
            if (punycode_len == 0) {
                return PunycodeError;
            }
            char nx = *punycode_start++;
            punycode_len--;
            if ('a' <= nx && nx <= 'z') {
                d = nx - 'a';
            } else if ('0' <= nx && nx <= '9') {
                d = 26 + (nx - '0');
            } else {
                return PunycodeError;
            }
            if (w == 0 || d > SIZE_MAX / w || d*w > SIZE_MAX - delta) {
                return PunycodeError;
            }
            delta += d * w;
            if (d < t) {
                break;
            }
            if (base < t || w == 0 || (base - t) > SIZE_MAX / w) {
                return PunycodeError;
            }
            w *= (base - t);
        }

        len += 1;
        if (i > SIZE_MAX - delta) {
            return PunycodeError;
        }
        i += delta;
        if (n > SIZE_MAX - i / len) {
            return PunycodeError;
        }
        n += i / len;
        i %= len;

        // char validation
        if (n > UINT32_MAX || !validate_char((uint32_t)n)) {
            return PunycodeError;
        }

        // insert new character
        if (len > SMALL_PUNYCODE_LEN) {
            return PunycodeError;
        }
        memmove(out + i + 1, out + i, (len - i - 1) * sizeof(uint32_t));
        out[i] = (uint32_t)n;

        // start i index at incremented position
        i++;

        // If there are no more deltas, decoding is complete.
        if (punycode_len == 0) {
            *out_len = len;
            return PunycodeOk;
        }

        // Perform bias adaptation.
        delta /= damp;
        damp = 2;

        delta += delta / len;
        k = 0;
        while (delta > ((base - t_min) * t_max) / 2) {
            delta /= base - t_min;
            k += base;
        }
        bias = k + ((base - t_min + 1) * delta) / (delta + skew);
    }
}

struct ident {
    const char *ascii_start;
    size_t ascii_len;
    const char *punycode_start;
    size_t punycode_len;
};

static NODISCARD overflow_status display_ident(const char *ascii_start, size_t ascii_len, const char *punycode_start, size_t punycode_len, uint8_t *out, size_t *out_len) {
    uint32_t outbuf[SMALL_PUNYCODE_LEN];

    size_t wide_len;
    size_t out_buflen = *out_len;

    if (punycode_len == 0) {
        if (ascii_len > out_buflen) {
            return OverflowOverflow;
        }
        memcpy(out, ascii_start, ascii_len);
        *out_len = ascii_len;
    } else if (punycode_decode(ascii_start, ascii_len, punycode_start, punycode_len, &outbuf, &wide_len) == PunycodeOk) {
        size_t narrow_len = 0;
        for (size_t i = 0; i < wide_len; i++) {
            if (out_buflen - narrow_len < 4) {
                return OverflowOverflow;
            }
            unsigned char *pos = &out[narrow_len];
            narrow_len += code_to_utf8(pos, outbuf[i]);
        }
        *out_len = narrow_len;
    } else {
        size_t narrow_len = 0;
        if (out_buflen < strlen("punycode{")) {
            return OverflowOverflow;
        }
        memcpy(out, "punycode{", strlen("punycode{"));
        narrow_len = strlen("punycode{");
        if (ascii_len > 0) {
            if (out_buflen - narrow_len < ascii_len || out_buflen - narrow_len - ascii_len < 1) {
                return OverflowOverflow;
            }
            memcpy(out + narrow_len, ascii_start, ascii_len);
            narrow_len += ascii_len;
            out[narrow_len] = '-';
            narrow_len++;
        }
        if (out_buflen - narrow_len < punycode_len || out_buflen - narrow_len - punycode_len < 1) {
            return OverflowOverflow;
        }
        memcpy(out + narrow_len, punycode_start, punycode_len);
        narrow_len += punycode_len;
        out[narrow_len] = '}';
        narrow_len++;
        *out_len = narrow_len;
    }

    return OverflowOk;
}

static NODISCARD bool try_parse_uint(const char *buf, size_t len, uint64_t *result) {
    size_t cur = 0;
    for(;cur < len && buf[cur] == '0';cur++);
    uint64_t result_val = 0;
    if (len - cur > 16) return false;
    for(;cur < len;cur++) {
        char c = buf[cur];
        result_val <<= 4;
        if ('0' <= c && c <= '9') {
            result_val += c - '0';
        } else if ('a' <= c && c <= 'f') {
            result_val += 10 + (c - 'a');
        } else {
            return false;
        }
    }
    *result = result_val;
    return true;
}

static NODISCARD bool dinibble2int(const char *buf, uint8_t *result) {
    uint8_t result_val = 0;
    for (int i = 0; i < 2; i++) {
        char c = buf[i];
        result_val <<= 4;
        if ('0' <= c && c <= '9') {
            result_val += c - '0';
        } else if ('a' <= c && c <= 'f') {
            result_val += 10 + (c - 'a');
        } else {
            return false;
        }
    }
    *result = result_val;
    return true;
}


typedef enum {
    NtsOk = 0,
    NtsOverflow = 1,
    NtsInvalid = 2
} nibbles_to_string_status;

// '\u{10ffff}', +margin
#define ESCAPED_SIZE 12

static NODISCARD size_t char_to_string(uint32_t ch, uint8_t quote, bool first, char (*buf)[ESCAPED_SIZE]) {
    // encode the character
    char *escaped_buf = *buf;
    escaped_buf[0] = '\\';
    size_t escaped_len = 2;
    switch (ch) {
        case '\0':
        escaped_buf[1] = '0';
        break;
        case '\t':
        escaped_buf[1] = 't';
        break;
        case '\r':
        escaped_buf[1] = 'r';
        break;
        case '\n':
        escaped_buf[1] = 'n';
        break;
        case '\\':
        escaped_buf[1] = '\\';
        break;
        default:
        if (ch == quote) {
            escaped_buf[1] = ch;
        } else if (!unicode_isprint(ch) || (first && unicode_isgraphemextend(ch))) {
            int hexlen = snprintf(escaped_buf, ESCAPED_SIZE, "\\u{%x}", (unsigned int)ch);
            if (hexlen < 0) {
                return 0; // (snprintf shouldn't fail!)
            }
            escaped_len = hexlen;
        } else {
            // printable character
            escaped_buf[0] = ch;
            escaped_len = 1;
        }
        break;
    }

    return escaped_len;
}

// convert nibbles to a single/double-quoted string
static NODISCARD nibbles_to_string_status nibbles_to_string(const char *buf, size_t len, uint8_t *out, size_t *out_len) {
    uint8_t quote = '"';
    bool first = true;

    if ((len % 2) != 0) {
        return NtsInvalid; // odd number of nibbles
    }

    size_t cur_out_len = 0;

    // write starting quote
    if (out != NULL) {
        cur_out_len = *out_len;
        if (cur_out_len == 0) {
            return NtsOverflow;
        }
        *out++ = quote;
        cur_out_len--;
    }

    uint8_t conv_buf[4] = {0};
    size_t conv_buf_len = 0;
    while (len > 1 || conv_buf_len > 0) {
        while (len > 1 && conv_buf_len < sizeof(conv_buf)) {
            if (!dinibble2int(buf, &conv_buf[conv_buf_len])) {
                return NtsInvalid;
            }
            conv_buf_len++;
            buf += 2;
            len -= 2;
        }

        // conv_buf is full here if possible, process 1 UTF-8 character
        uint32_t ch = 0;
        size_t consumed = utf8_next_char(conv_buf, &ch);
        if (consumed > conv_buf_len) {
            // either SIZE_MAX (invalid UTF-8) or finished input buffer and
            // there are still bytes remaining, in both cases invalid
            return NtsInvalid;
        }

        // "consume" the character
        memmove(conv_buf, conv_buf+consumed, conv_buf_len-consumed);
        conv_buf_len -= consumed;

        char escaped_buf[ESCAPED_SIZE];
        size_t escaped_len = char_to_string(ch, '"', first, &escaped_buf);
        if (out != NULL) {
            if (cur_out_len < escaped_len) {
                return NtsOverflow;
            }
            memcpy(out, escaped_buf, escaped_len);
            out += escaped_len;
            cur_out_len -= escaped_len;
        }
        first = false;
    }

    // write ending quote
    if (out != NULL) {
        if (cur_out_len == 0) {
            return NtsOverflow;
        }
        *out++ = quote;
        cur_out_len--;
        *out_len -= cur_out_len; // subtract remaining space to get used space
    }

    return NtsOk;
}

static const char* basic_type(uint8_t tag) {
    switch(tag) {
        case 'b':
        return "bool";
        case 'c':
        return "char";
        case 'e':
        return "str";
        case 'u':
        return "()";
        case 'a':
        return "i8";
        case 's':
        return "i16";
        case 'l':
        return "i32";
        case 'x':
        return "i64";
        case 'n':
        return "i128";
        case 'i':
        return "isize";
        case 'h':
        return "u8";
        case 't':
        return "u16";
        case 'm':
        return "u32";
        case 'y':
        return "u64";
        case 'o':
        return "u128";
        case 'j':
        return "usize";
        case 'f':
        return "f32";
        case 'd':
        return "f64";
        case 'z':
        return "!";
        case 'p':
        return "_";
        case 'v':
        return "...";
        default:
        return NULL;
    }
}

static NODISCARD demangle_status parser_push_depth(struct parser *parser) {
    parser->depth++;
    if (parser->depth > MAX_DEPTH) {
        return DemangleRecursed;
    } else {
        return DemangleOk;
    }
}

static demangle_status parser_pop_depth(struct parser *parser) {
    parser->depth--;
    return DemangleOk;
}

static uint8_t parser_peek(struct parser const *parser) {
    if (parser->next == parser->sym_len) {
        return 0; // add a "pseudo nul terminator" to avoid peeking past the end of a symbol
    } else {
        return parser->sym[parser->next];
    }
}

static bool parser_eat(struct parser *parser, uint8_t ch) {
    if (parser_peek(parser) == ch) {
        if (ch != 0) { // safety: make sure we don't skip past the NUL terminator
            parser->next++;
        }
        return true;
    } else {
        return false;
    }
}

static uint8_t parser_next(struct parser *parser) {
    // don't advance after end of input, and return an imaginary NUL terminator
    if (parser->next == parser->sym_len) {
        return 0;
    } else {
        return parser->sym[parser->next++];
    }
}

static NODISCARD demangle_status parser_ch(struct parser *parser, uint8_t *next) {
    // don't advance after end of input
    if (parser->next == parser->sym_len) {
        return DemangleInvalid;
    } else {
        *next = parser->sym[parser->next++];
        return DemangleOk;
    }
}

struct buf {
    const char *start;
    size_t len;
};

static NODISCARD demangle_status parser_hex_nibbles(struct parser *parser, struct buf *buf) {
    size_t start = parser->next;
    for (;;) {
        uint8_t ch = parser_next(parser);
        if (ch == '_') {
            break;
        }
        if (!(('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f'))) {
            return DemangleInvalid;
        }
    }
    buf->start = parser->sym + start;
    buf->len = parser->next - start - 1; // skip final _
    return DemangleOk;
}

static NODISCARD demangle_status parser_digit_10(struct parser *parser, uint8_t *out) {
    uint8_t ch = parser_peek(parser);
    if ('0' <= ch && ch <= '9') {
        *out = ch - '0';
        parser->next++;
        return DemangleOk;
    } else {
        return DemangleInvalid;
    }
}

static NODISCARD demangle_status parser_digit_62(struct parser *parser, uint64_t *out) {
    uint8_t ch = parser_peek(parser);
    if ('0' <= ch && ch <= '9') {
        *out = ch - '0';
        parser->next++;
        return DemangleOk;
    } else if ('a' <= ch && ch <= 'z') {
        *out = 10 + (ch - 'a');
        parser->next++;
        return DemangleOk;
    } else if ('A' <= ch && ch <= 'Z') {
        *out = 10 + 26 + (ch - 'A');
        parser->next++;
        return DemangleOk;
    } else {
        return DemangleInvalid;
    }
}

static NODISCARD demangle_status parser_integer_62(struct parser *parser, uint64_t *out) {
    if (parser_eat(parser, '_')) {
        *out = 0;
        return DemangleOk;
    }

    uint64_t x = 0;
    demangle_status status;
    while (!parser_eat(parser, '_')) {
        uint64_t d;
        if ((status = parser_digit_62(parser, &d)) != DemangleOk) {
            return status;
        }
        if (x > UINT64_MAX / 62) {
            return DemangleInvalid;
        }
        x *= 62;
        if (x > UINT64_MAX - d) {
            return DemangleInvalid;
        }
        x += d;
    }
    if (x == UINT64_MAX) {
        return DemangleInvalid;
    }
    *out = x + 1;
    return DemangleOk;
}

static NODISCARD demangle_status parser_opt_integer_62(struct parser *parser, uint8_t tag, uint64_t *out) {
    if (!parser_eat(parser, tag)) {
        *out = 0;
        return DemangleOk;
    }

    demangle_status status;
    if ((status = parser_integer_62(parser, out)) != DemangleOk) {
        return status;
    }
    if (*out == UINT64_MAX) {
        return DemangleInvalid;
    }
    *out = *out + 1;
    return DemangleOk;
}

static NODISCARD demangle_status parser_disambiguator(struct parser *parser, uint64_t *out) {
    return parser_opt_integer_62(parser, 's', out);
}

typedef uint8_t parser_namespace_type;

static NODISCARD demangle_status parser_namespace(struct parser *parser, parser_namespace_type *out) {
    uint8_t next = parser_next(parser);
    if ('A' <= next && next <= 'Z') {
        *out = next;
        return DemangleOk;
    } else if ('a' <= next && next <= 'z') {
        *out = 0;
        return DemangleOk;
    } else {
        return DemangleInvalid;
    }
}

static NODISCARD demangle_status parser_backref(struct parser *parser, struct parser *out) {
    size_t start = parser->next;
    if (start == 0) {
        return DemangleBug;
    }
    size_t s_start = start - 1;
    uint64_t i;
    demangle_status status = parser_integer_62(parser, &i);
    if (status != DemangleOk) {
        return status;
    }
    if (i >= s_start) {
        return DemangleInvalid;
    }
    struct parser res = {
        .sym = parser->sym,
        .sym_len = parser->sym_len,
        .next = (size_t)i,
        .depth = parser->depth
    };
    status = parser_push_depth(&res);
    if (status != DemangleOk) {
        return status;
    }
    *out = res;
    return DemangleOk;
}

static NODISCARD demangle_status parser_ident(struct parser *parser, struct ident *out) {
    bool is_punycode = parser_eat(parser, 'u');
    size_t len;
    uint8_t d;
    demangle_status status = parser_digit_10(parser, &d);
    len = d;
    if (status != DemangleOk) {
        return status;
    }
    if (len) {
        for (;;) {
            status = parser_digit_10(parser, &d);
            if (status != DemangleOk) {
                break;
            }
            if (len > SIZE_MAX / 10) {
                return DemangleInvalid;
            }
            len *= 10;
            if (len > SIZE_MAX - d) {
                return DemangleInvalid;
            }
            len += d;
        }
    }

    // Skip past the optional `_` separator.
    parser_eat(parser, '_');

    size_t start = parser->next;
    if (parser->sym_len - parser->next < len) {
        return DemangleInvalid;
    }
    parser->next += len;

    const char *ident = &parser->sym[start];

    if (is_punycode) {
        const char *underscore = (const char *)demangle_memrchr(ident, '_', (size_t)len);
        if (underscore == NULL) {
            *out = (struct ident){
                .ascii_start="",
                .ascii_len=0,
                .punycode_start=ident,
                .punycode_len=len
            };
        } else {
            size_t ascii_len = underscore - ident;
            // ascii_len <= len - 1 since `_` is in the first len bytes
            size_t punycode_len = len - 1 - ascii_len;
            *out = (struct ident){
                .ascii_start=ident,
                .ascii_len=ascii_len,
                .punycode_start=underscore + 1,
                .punycode_len=punycode_len
            };
        }
        if (out->punycode_len == 0) {
            return DemangleInvalid;
        }
        return DemangleOk;
    } else {
        *out = (struct ident) {
            .ascii_start=ident,
            .ascii_len=(size_t)len,
            .punycode_start="",
            .punycode_len=0,
        };
        return DemangleOk;
    }
}

#define INVALID_SYNTAX "{invalid syntax}"

static const char *demangle_error_message(demangle_status status) {
    switch (status) {
        case DemangleInvalid:
        return INVALID_SYNTAX;
        case DemangleBug:
        return "{bug}";
        case DemangleRecursed:
        return "{recursion limit reached}";
        default:
        return "{unknown error}";
    }
}

#define PRINT(print_fn) \
 do { \
   if ((print_fn) == OverflowOverflow) { \
    return OverflowOverflow; \
   } \
 } while(0)

#define PRINT_CH(printer, s) PRINT(printer_print_ch((printer), (s)))
#define PRINT_STR(printer, s) PRINT(printer_print_str((printer), (s)))
#define PRINT_U64(printer, s) PRINT(printer_print_u64((printer), (s)))
#define PRINT_IDENT(printer, s) PRINT(printer_print_ident((printer), (s)))

#define INVALID(printer) \
  do { \
    PRINT_STR((printer), INVALID_SYNTAX); \
    (printer)->status = DemangleInvalid; \
    return OverflowOk; \
  } while(0)

#define PARSE(printer, method, ...) \
  do { \
    if ((printer)->status != DemangleOk) { \
      PRINT_STR((printer), "?"); \
      return OverflowOk; \
    } else { \
      demangle_status _parse_status = method(&(printer)->parser, ## __VA_ARGS__); \
      if (_parse_status != DemangleOk) { \
        PRINT_STR((printer), demangle_error_message(_parse_status)); \
        (printer)->status = _parse_status; \
        return OverflowOk; \
      } \
    } \
  } while(0)

#define PRINT_SEP_LIST(printer, body, sep) \
  do { \
    size_t _sep_list_i; \
    PRINT_SEP_LIST_COUNT(printer, _sep_list_i, body, sep); \
  } while(0)

#define PRINT_SEP_LIST_COUNT(printer, count, body, sep) \
  do { \
    count = 0; \
    while ((printer)->status == DemangleOk && !printer_eat((printer), 'E')) { \
      if (count > 0) { PRINT_STR(printer, sep); } \
      body; \
      count++; \
    } \
  } while(0)

static bool printer_eat(struct printer *printer, uint8_t b) {
    if (printer->status != DemangleOk) {
        return false;
    }

    return parser_eat(&printer->parser, b);
}

static void printer_pop_depth(struct printer *printer) {
    if (printer->status == DemangleOk) {
        parser_pop_depth(&printer->parser);
    }
}

static NODISCARD overflow_status printer_print_buf(struct printer *printer, const char *start, size_t len) {
    if (printer->out == NULL) {
        return OverflowOk;
    }
    if (printer->out_len < len) {
        return OverflowOverflow;
    }

    memcpy(printer->out, start, len);
    printer->out += len;
    printer->out_len -= len;
    return OverflowOk;
}

static NODISCARD overflow_status printer_print_str(struct printer *printer, const char *buf) {
    return printer_print_buf(printer, buf, strlen(buf));
}

static NODISCARD overflow_status printer_print_ch(struct printer *printer, char ch) {
    return printer_print_buf(printer, &ch, 1);
}

static NODISCARD overflow_status printer_print_u64(struct printer *printer, uint64_t n) {
    char buf[32] = {0};
    snprintf(buf, sizeof(buf), "%llu", (unsigned long long)n); // printing uint64 uses 21 < 32 chars
    return printer_print_str(printer, buf);
}

static NODISCARD overflow_status printer_print_ident(struct printer *printer, struct ident *ident) {
    if (printer->out == NULL) {
        return OverflowOk;
    }

    size_t out_len = printer->out_len;
    overflow_status status;
    if ((status = display_ident(ident->ascii_start, ident->ascii_len, ident->punycode_start, ident->punycode_len, (uint8_t*)printer->out, &out_len)) != OverflowOk) {
        return status;
    }
    printer->out += out_len;
    printer->out_len -= out_len;
    return OverflowOk;
}

typedef overflow_status (*printer_fn)(struct printer *printer);
typedef overflow_status (*backref_fn)(struct printer *printer, bool *arg);

static NODISCARD overflow_status printer_print_backref(struct printer *printer, backref_fn func, bool *arg) {
    struct parser backref;
    PARSE(printer, parser_backref, &backref);

    if (printer->out == NULL) {
        return OverflowOk;
    }

    struct parser orig_parser = printer->parser;
    demangle_status orig_status = printer->status; // fixme not sure this is needed match for Ok on the Rust side
    printer->parser = backref;
    printer->status = DemangleOk;
    overflow_status status = func(printer, arg);
    printer->parser = orig_parser;
    printer->status = orig_status;

    return status;
}

static NODISCARD overflow_status printer_print_lifetime_from_index(struct printer *printer, uint64_t lt) {
    // Bound lifetimes aren't tracked when skipping printing.
    if (printer->out == NULL) {
        return OverflowOk;
    }

    PRINT_STR(printer, "'");
    if (lt == 0) {
        PRINT_STR(printer, "_");
        return OverflowOk;
    }

    if (printer->bound_lifetime_depth < lt) {
        INVALID(printer);
    } else {
        uint64_t depth = printer->bound_lifetime_depth - lt;
        if (depth < 26) {
            PRINT_CH(printer, 'a' + depth);
        } else {
            PRINT_STR(printer, "_");
            PRINT_U64(printer, depth);
        }

        return OverflowOk;
    }
}

static NODISCARD overflow_status printer_in_binder(struct printer *printer, printer_fn func) {
    uint64_t bound_lifetimes;
    PARSE(printer, parser_opt_integer_62, 'G', &bound_lifetimes);

    // Don't track bound lifetimes when skipping printing.
    if (printer->out == NULL) {
        return func(printer);
    }

    if (bound_lifetimes > 0) {
        PRINT_STR(printer, "for<");
        for (uint64_t i = 0; i < bound_lifetimes; i++) {
            if (i > 0) {
                PRINT_STR(printer, ", ");
            }
            printer->bound_lifetime_depth++;
            PRINT(printer_print_lifetime_from_index(printer, 1));
        }
        PRINT_STR(printer, "> ");
    }

    overflow_status r = func(printer);
    printer->bound_lifetime_depth -= bound_lifetimes;

    return r;
}

static NODISCARD overflow_status printer_print_generic_arg(struct printer *printer) {
    if (printer_eat(printer, 'L')) {
        uint64_t lt;
        PARSE(printer, parser_integer_62, &lt);
        return printer_print_lifetime_from_index(printer, lt);
    } else if (printer_eat(printer, 'K')) {
        return printer_print_const(printer, false);
    } else {
        return printer_print_type(printer);
    }
}

static NODISCARD overflow_status printer_print_generic_args(struct printer *printer) {
    PRINT_STR(printer, "<");
    PRINT_SEP_LIST(printer, PRINT(printer_print_generic_arg(printer)), ", ");
    PRINT_STR(printer, ">");
    return OverflowOk;
}

static NODISCARD overflow_status printer_print_path_out_of_value(struct printer *printer, bool *_arg) {
    (void)_arg;
    return printer_print_path(printer, false);
}

static NODISCARD overflow_status printer_print_path_in_value(struct printer *printer, bool *_arg) {
    (void)_arg;
    return printer_print_path(printer, true);
}

static NODISCARD overflow_status printer_print_path(struct printer *printer, bool in_value) {
    PARSE(printer, parser_push_depth);
    uint8_t tag;
    PARSE(printer, parser_ch, &tag);

    overflow_status st;
    uint64_t dis;
    struct ident name;
    parser_namespace_type ns;
    char *orig_out;

    switch(tag) {
    case 'C':
        PARSE(printer, parser_disambiguator, &dis);
        PARSE(printer, parser_ident, &name);

        PRINT_IDENT(printer, &name);

        if (printer->out != NULL && !printer->alternate && dis != 0) {
            PRINT_STR(printer, "[");
            char buf[24] = {0};
            snprintf(buf, sizeof(buf), "%llx", (unsigned long long)dis);
            PRINT_STR(printer, buf);
            PRINT_STR(printer, "]");
        }
        break;
    case 'N':
        PARSE(printer, parser_namespace, &ns);
        if ((st = printer_print_path(printer, in_value)) != OverflowOk) {
            return st;
        }

        // HACK(eddyb) if the parser is already marked as having errored,
        // `parse!` below will print a `?` without its preceding `::`
        // (because printing the `::` is skipped in certain conditions,
        // i.e. a lowercase namespace with an empty identifier),
        // so in order to get `::?`, the `::` has to be printed here.
        if (printer->status != DemangleOk) {
            PRINT_STR(printer, "::");
        }

        PARSE(printer, parser_disambiguator, &dis);
        PARSE(printer, parser_ident, &name);
        // Special namespace, like closures and shims
        if (ns) {
            PRINT_STR(printer, "::{");
            if (ns == 'C') {
                PRINT_STR(printer, "closure");
            } else if (ns == 'S') {
                PRINT_STR(printer, "shim");
            } else {
                PRINT_CH(printer, ns);
            }
            if (name.ascii_len != 0 || name.punycode_len != 0) {
                PRINT_STR(printer, ":");
                PRINT_IDENT(printer, &name);
            }
            PRINT_STR(printer, "#");
            PRINT_U64(printer, dis);
            PRINT_STR(printer, "}");
        } else {
            // Implementation-specific/unspecified namespaces
            if (name.ascii_len != 0 || name.punycode_len != 0) {
                PRINT_STR(printer, "::");
                PRINT_IDENT(printer, &name);
            }
        }
        break;
    case 'M':
    case 'X':
    // for impls, ignore the impls own path
    PARSE(printer, parser_disambiguator, &dis);
    orig_out = printer->out;
    printer->out = NULL;
    PRINT(printer_print_path(printer, false));
    printer->out = orig_out;

    // fallthru
    case 'Y':
    PRINT_STR(printer, "<");
    PRINT(printer_print_type(printer));
    if (tag != 'M') {
        PRINT_STR(printer, " as ");
        PRINT(printer_print_path(printer, false));
    }
    PRINT_STR(printer, ">");
    break;
    case 'I':
    PRINT(printer_print_path(printer, in_value));
    if (in_value) {
        PRINT_STR(printer, "::");
    }
    PRINT(printer_print_generic_args(printer));
    break;
    case 'B':
    PRINT(printer_print_backref(printer, in_value ? printer_print_path_in_value : printer_print_path_out_of_value, NULL));
    break;
    default:
    INVALID(printer);
    break;
    }

    printer_pop_depth(printer);
    return OverflowOk;
}

static NODISCARD overflow_status printer_print_const_uint(struct printer *printer, uint8_t tag) {
    struct buf hex;
    PARSE(printer, parser_hex_nibbles, &hex);

    uint64_t val;
    if (try_parse_uint(hex.start, hex.len, &val)) {
        PRINT_U64(printer, val);
    } else {
        PRINT_STR(printer, "0x");
        PRINT(printer_print_buf(printer, hex.start, hex.len));
    }

    if (printer->out != NULL && !printer->alternate) {
        const char *ty = basic_type(tag);
        if (/* safety */ ty != NULL) {
            PRINT_STR(printer, ty);
        }
    }

    return OverflowOk;
}

static NODISCARD overflow_status printer_print_const_str_literal(struct printer *printer) {
    struct buf hex;
    PARSE(printer, parser_hex_nibbles, &hex);

    size_t out_len = SIZE_MAX;
    nibbles_to_string_status nts_status = nibbles_to_string(hex.start, hex.len, NULL, &out_len);
    switch (nts_status) {
    case NtsOk:
        if (printer->out != NULL) {
            out_len = printer->out_len;
            nts_status = nibbles_to_string(hex.start, hex.len, (uint8_t*)printer->out, &out_len);
            if (nts_status != NtsOk) {
                return OverflowOverflow;
            }
            printer->out += out_len;
            printer->out_len -= out_len;
        }
        return OverflowOk;
    case NtsOverflow:
        // technically if there is a string of size `SIZE_MAX/6` whose escaped version overflows
        // SIZE_MAX but has an invalid char, this will be a "fake" overflow. In practice,
        // that is not going to happen and a fuzzer will not generate strings of this length.
        return OverflowOverflow;
    case NtsInvalid:
    default:
        INVALID(printer);
    }
}

static NODISCARD overflow_status printer_print_const_struct(struct printer *printer) {
    uint64_t dis;
    struct ident name;
    PARSE(printer, parser_disambiguator, &dis);
    PARSE(printer, parser_ident, &name);
    PRINT_IDENT(printer, &name);
    PRINT_STR(printer, ": ");
    return printer_print_const(printer, true);
}

static NODISCARD overflow_status printer_print_const_out_of_value(struct printer *printer, bool *_arg) {
    (void)_arg;
    return printer_print_const(printer, false);
}

static NODISCARD overflow_status printer_print_const_in_value(struct printer *printer, bool *_arg) {
    (void)_arg;
    return printer_print_const(printer, true);
}

static NODISCARD overflow_status printer_print_const(struct printer *printer, bool in_value) {
    uint8_t tag;

    PARSE(printer, parser_ch, &tag);
    PARSE(printer, parser_push_depth);

    struct buf hex;
    uint64_t val;
    size_t count;

    bool opened_brace = false;
#define OPEN_BRACE_IF_OUTSIDE_EXPR \
        do { if (!in_value) { \
            opened_brace = true; \
            PRINT_STR(printer, "{"); \
        } } while(0)

    switch(tag) {
    case 'p':
        PRINT_STR(printer, "_");
        break;
    // Primitive leaves with hex-encoded values (see `basic_type`).
    case 'a':
    case 's':
    case 'l':
    case 'x':
    case 'n':
    case 'i':
        if (printer_eat(printer, 'n')) {
            PRINT_STR(printer, "-");
        }
        /* fallthrough */
    case 'h':
    case 't':
    case 'm':
    case 'y':
    case 'o':
    case 'j':
        PRINT(printer_print_const_uint(printer, tag));
        break;
    case 'b':
        PARSE(printer, parser_hex_nibbles, &hex);
        if (try_parse_uint(hex.start, hex.len, &val)) {
            if (val == 0) {
                PRINT_STR(printer, "false");
            } else if (val == 1) {
                PRINT_STR(printer, "true");
            } else {
                INVALID(printer);
            }
        } else {
            INVALID(printer);
        }
        break;
    case 'c':
        PARSE(printer, parser_hex_nibbles, &hex);
        if (try_parse_uint(hex.start, hex.len, &val)
            && val < UINT32_MAX
            && validate_char((uint32_t)val))
        {
            char escaped_buf[ESCAPED_SIZE];
            size_t escaped_size = char_to_string((uint32_t)val, '\'', true, &escaped_buf);

            PRINT_STR(printer, "'");
            PRINT(printer_print_buf(printer, escaped_buf, escaped_size));
            PRINT_STR(printer, "'");
        } else {
            INVALID(printer);
        }
        break;
    case 'e':
        OPEN_BRACE_IF_OUTSIDE_EXPR;
        PRINT_STR(printer, "*");
        PRINT(printer_print_const_str_literal(printer));
        break;
    case 'R':
    case 'Q':
        if (tag == 'R' && printer_eat(printer, 'e')) {
            PRINT(printer_print_const_str_literal(printer));
        } else {
            OPEN_BRACE_IF_OUTSIDE_EXPR;
            PRINT_STR(printer, "&");
            if (tag != 'R') {
                PRINT_STR(printer, "mut ");
            }
            PRINT(printer_print_const(printer, true));
        }
        break;
    case 'A':
        OPEN_BRACE_IF_OUTSIDE_EXPR;
        PRINT_STR(printer, "[");
        PRINT_SEP_LIST(printer, PRINT(printer_print_const(printer, true)), ", ");
        PRINT_STR(printer, "]");
        break;
    case 'T':
        OPEN_BRACE_IF_OUTSIDE_EXPR;
        PRINT_STR(printer, "(");
        PRINT_SEP_LIST_COUNT(printer, count, PRINT(printer_print_const(printer, true)), ", ");
        if (count == 1) {
            PRINT_STR(printer, ",");
        }
        PRINT_STR(printer, ")");
        break;
    case 'V':
        OPEN_BRACE_IF_OUTSIDE_EXPR;
        PRINT(printer_print_path(printer, true));
        PARSE(printer, parser_ch, &tag);
        switch(tag) {
        case 'U':
        break;
        case 'T':
        PRINT_STR(printer, "(");
        PRINT_SEP_LIST(printer, PRINT(printer_print_const(printer, true)), ", ");
        PRINT_STR(printer, ")");
        break;
        case 'S':
        PRINT_STR(printer, " { ");
        PRINT_SEP_LIST(printer, PRINT(printer_print_const_struct(printer)), ", ");
        PRINT_STR(printer, " }");
        break;
        default:
        INVALID(printer);
        }
        break;
    case 'B':
        PRINT(printer_print_backref(printer, in_value ? printer_print_const_in_value : printer_print_const_out_of_value, NULL));
        break;
    default:
        INVALID(printer);
    }
#undef OPEN_BRACE_IF_OUTSIDE_EXPR

    if (opened_brace) {
        PRINT_STR(printer, "}");
    }
    printer_pop_depth(printer);

    return OverflowOk;
}

/// A trait in a trait object may have some "existential projections"
/// (i.e. associated type bindings) after it, which should be printed
/// in the `<...>` of the trait, e.g. `dyn Trait<T, U, Assoc=X>`.
/// To this end, this method will keep the `<...>` of an 'I' path
/// open, by omitting the `>`, and return `Ok(true)` in that case.
static NODISCARD overflow_status printer_print_maybe_open_generics(struct printer *printer, bool *open) {
    if (printer_eat(printer, 'B')) {
        // NOTE(eddyb) the closure may not run if printing is being skipped,
        // but in that case the returned boolean doesn't matter.
        *open = false;
        return printer_print_backref(printer, printer_print_maybe_open_generics, open);
    } else if(printer_eat(printer, 'I')) {
        PRINT(printer_print_path(printer, false));
        PRINT_STR(printer, "<");
        PRINT_SEP_LIST(printer, PRINT(printer_print_generic_arg(printer)), ", ");
        *open = true;
        return OverflowOk;
    } else {
        PRINT(printer_print_path(printer, false));
        *open = false;
        return OverflowOk;
    }
}

static NODISCARD overflow_status printer_print_dyn_trait(struct printer *printer) {
    bool open;
    PRINT(printer_print_maybe_open_generics(printer, &open));

    while (printer_eat(printer, 'p')) {
        if (!open) {
            PRINT_STR(printer, "<");
            open = true;
        } else {
            PRINT_STR(printer, ", ");
        }

        struct ident name;
        PARSE(printer, parser_ident, &name);

        PRINT_IDENT(printer, &name);
        PRINT_STR(printer, " = ");
        PRINT(printer_print_type(printer));
    }

    if (open) {
        PRINT_STR(printer, ">");
    }

    return OverflowOk;
}

static NODISCARD overflow_status printer_print_object_bounds(struct printer *printer) {
    PRINT_SEP_LIST(printer, PRINT(printer_print_dyn_trait(printer)), " + ");
    return OverflowOk;
}

static NODISCARD overflow_status printer_print_function_type(struct printer *printer) {
    bool is_unsafe = printer_eat(printer, 'U');
    const char *abi;
    size_t abi_len;
    if (printer_eat(printer, 'K')) {
        if (printer_eat(printer, 'C')) {
            abi = "C";
            abi_len = 1;
        } else {
            struct ident abi_ident;
            PARSE(printer, parser_ident, &abi_ident);
            if (abi_ident.ascii_len == 0 || abi_ident.punycode_len != 0) {
                INVALID(printer);
            }
            abi = abi_ident.ascii_start;
            abi_len = abi_ident.ascii_len;
        }
    } else {
        abi = NULL;
        abi_len = 0;
    }

    if (is_unsafe) {
        PRINT_STR(printer, "unsafe ");
    }

    if (abi != NULL) {
        PRINT_STR(printer, "extern \"");

        // replace _ with -
        while (abi_len > 0) {
            const char *minus = (const char *)memchr(abi, '_', abi_len);
            if (minus == NULL) {
                PRINT(printer_print_buf(printer, (const char*)abi, abi_len));
                break;
            } else {
                size_t space_to_minus = minus - abi;
                PRINT(printer_print_buf(printer, (const char*)abi, space_to_minus));
                PRINT_STR(printer, "-");
                abi = minus + 1;
                abi_len -= (space_to_minus + 1);
            }
        }

        PRINT_STR(printer, "\" ");
    }

    PRINT_STR(printer, "fn(");
    PRINT_SEP_LIST(printer, PRINT(printer_print_type(printer)), ", ");
    PRINT_STR(printer, ")");

    if (printer_eat(printer, 'u')) {
        // Skip printing the return type if it's 'u', i.e. `()`.
    } else {
        PRINT_STR(printer, " -> ");
        PRINT(printer_print_type(printer));
    }

    return OverflowOk;
}

static NODISCARD overflow_status printer_print_type_backref(struct printer *printer, bool *_arg) {
    (void)_arg;
    return printer_print_type(printer);
}

static NODISCARD overflow_status printer_print_type(struct printer *printer) {
    uint8_t tag;
    PARSE(printer, parser_ch, &tag);

    const char *basic_ty = basic_type(tag);
    if (basic_ty) {
        return printer_print_str(printer, basic_ty);
    }

    uint64_t count;
    uint64_t lt;

    PARSE(printer, parser_push_depth);
    switch (tag) {
    case 'R':
    case 'Q':
        PRINT_STR(printer, "&");
        if (printer_eat(printer, 'L')) {
            PARSE(printer, parser_integer_62, &lt);
            if (lt != 0) {
                PRINT(printer_print_lifetime_from_index(printer, lt));
                PRINT_STR(printer, " ");
            }
        }
        if (tag != 'R') {
            PRINT_STR(printer, "mut ");
        }
        PRINT(printer_print_type(printer));
        break;
    case 'P':
    case 'O':
        PRINT_STR(printer, "*");
        if (tag != 'P') {
            PRINT_STR(printer, "mut ");
        } else {
            PRINT_STR(printer, "const ");
        }
        PRINT(printer_print_type(printer));
        break;
    case 'A':
    case 'S':
        PRINT_STR(printer, "[");
        PRINT(printer_print_type(printer));
        if (tag == 'A') {
            PRINT_STR(printer, "; ");
            PRINT(printer_print_const(printer, true));
        }
        PRINT_STR(printer, "]");
        break;
    case 'T':
        PRINT_STR(printer, "(");
        PRINT_SEP_LIST_COUNT(printer, count, PRINT(printer_print_type(printer)), ", ");
        if (count == 1) {
            PRINT_STR(printer, ",");
        }
        PRINT_STR(printer, ")");
        break;
    case 'F':
        PRINT(printer_in_binder(printer, printer_print_function_type));
        break;
    case 'D':
        PRINT_STR(printer, "dyn ");
        PRINT(printer_in_binder(printer, printer_print_object_bounds));

        if (!printer_eat(printer, 'L')) {
            INVALID(printer);
        }
        PARSE(printer, parser_integer_62, &lt);

        if (lt != 0) {
            PRINT_STR(printer, " + ");
            PRINT(printer_print_lifetime_from_index(printer, lt));
        }
        break;
    case 'B':
        PRINT(printer_print_backref(printer, printer_print_type_backref, NULL));
        break;
    default:
        // Go back to the tag, so `print_path` also sees it.
        if (printer->status == DemangleOk && /* safety */ printer->parser.next > 0) {
            printer->parser.next--;
        }
        PRINT(printer_print_path(printer, false));
    }

    printer_pop_depth(printer);
    return OverflowOk;
}

NODISCARD static demangle_status rust_demangle_legacy_demangle(const char *s, size_t s_len, struct demangle_legacy *res, const char **rest)
{
    if (s_len > strlen(s)) {
        // s_len only exists to shorten the string, this is not a buffer API
        return DemangleInvalid;
    }

    const char *inner;
    size_t inner_len;
    if (s_len >= 3 && !strncmp(s, "_ZN", 3)) {
        inner = s + 3;
        inner_len = s_len - 3;
    } else if (s_len >= 2 && !strncmp(s, "ZN", 2)) {
        // On Windows, dbghelp strips leading underscores, so we accept "ZN...E"
        // form too.
        inner = s + 2;
        inner_len = s_len - 2;
    } else if (s_len >= 4 && !strncmp(s, "__ZN", 4)) {
        // On OSX, symbols are prefixed with an extra _
        inner = s + 4;
        inner_len = s_len - 4;
    } else {
        return DemangleInvalid;
    }

    if (!str_isascii(inner, inner_len)) {
        return DemangleInvalid;
    }

    size_t elements = 0;
    const char *chars = inner;
    size_t chars_len = inner_len;
    if (chars_len == 0) {
        return DemangleInvalid;
    }
    char c;
    while ((c = *chars) != 'E') {
        // Decode an identifier element's length
        if (c < '0' || c > '9') {
            return DemangleInvalid;
        }
        size_t len = 0;
        while (c >= '0' && c <= '9') {
            size_t d = c - '0';
            if (len > SIZE_MAX / 10) {
                return DemangleInvalid;
            }
            len *= 10;
            if (len > SIZE_MAX - d) {
                return DemangleInvalid;
            }
            len += d;

            chars++;
            chars_len--;
            if (chars_len == 0) {
                return DemangleInvalid;
            }
            c = *chars;
        }

        // Advance by the length
        if (chars_len <= len) {
            return DemangleInvalid;
        }
        chars += len;
        chars_len -= len;
        elements++;
    }
    *res = (struct demangle_legacy) { inner, inner_len, elements };
    *rest = chars + 1;
    return DemangleOk;
}

static bool is_rust_hash(const char *s, size_t len) {
    if (len == 0 || s[0] != 'h') {
        return false;
    }

    for (size_t i = 1; i < len; i++) {
        if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f') || (s[i] >= 'A' && s[i] <= 'F'))) {
            return false;
        }
    }

    return true;
}

NODISCARD static overflow_status rust_demangle_legacy_display_demangle(struct demangle_legacy res, char *out, size_t len, bool alternate)
{
    struct printer printer = {
        // not actually using the parser part of the printer, just keeping it to share the format functions
        DemangleOk,
        { NULL },
        out,
        len,
        0,
        alternate
    };
    const char *inner = res.mangled;
    for (size_t element = 0; element < res.elements; element++) {
        size_t i = 0;
        const char *rest;
        for (rest = inner; rest < res.mangled + res.mangled_len && *rest >= '0' && *rest <= '9'; rest++) {
            i *= 10;
            i += *rest - '0';
        }
        if ((size_t)(res.mangled + res.mangled_len - rest) < i) {
            // safety: shouldn't reach this place if the input string is validated. bail out.
            // safety: we knwo rest <= res.mangled + res.mangled_len from the for-loop above
            break;
        }

        size_t len = i;
        inner = rest + len;

        // From here on, inner contains a pointer to the next element, rest[:len] to the current one
        if (alternate && element + 1 == res.elements && is_rust_hash(rest, i)) {
            break;
        }
        if (element != 0) {
            PRINT_STR(&printer, "::");
        }

        if (len >= 2 && !strncmp(rest, "_$", 2)) {
            rest++;
            len--;
        }

        while (len > 0) {
            if (rest[0] == '.') {
                if (len >= 2 && rest[1] == '.') {
                    PRINT_STR(&printer, "::");
                    rest += 2;
                    len -= 2;
                } else {
                    PRINT_STR(&printer, ".");
                    rest += 1;
                    len -= 1;
                }
            } else if (rest[0] == '$') {
                const char *escape = (const char *)memchr(rest + 1, '$', len - 1);
                if (escape == NULL) {
                    break;
                }
                const char *escape_start = rest + 1;
                size_t escape_len = escape - (rest + 1);

                size_t next_len = len - (escape + 1 - rest);
                const char *next_rest = escape + 1;

                char ch;
                if ((escape_len == 2 && escape_start[0] == 'S' && escape_start[1] == 'P')) {
                    ch = '@';
                } else if ((escape_len == 2 && escape_start[0] == 'B' && escape_start[1] == 'P')) {
                    ch = '*';
                } else if ((escape_len == 2 && escape_start[0] == 'R' && escape_start[1] == 'F')) {
                    ch = '&';
                } else if ((escape_len == 2 && escape_start[0] == 'L' && escape_start[1] == 'T')) {
                    ch = '<';
                } else if ((escape_len == 2 && escape_start[0] == 'G' && escape_start[1] == 'T')) {
                    ch = '>';
                } else if ((escape_len == 2 && escape_start[0] == 'L' && escape_start[1] == 'P')) {
                    ch = '(';
                } else if ((escape_len == 2 && escape_start[0] == 'R' && escape_start[1] == 'P')) {
                    ch = ')';
                } else if ((escape_len == 1 && escape_start[0] == 'C')) {
                    ch = ',';
                } else {
                    if (escape_len > 1 && escape_start[0] == 'u') {
                        escape_start++;
                        escape_len--;
                        uint64_t val;
                        if (try_parse_uint(escape_start, escape_len, &val)
                            && val < UINT32_MAX
                            && validate_char((uint32_t)val))
                        {
                            if (!unicode_iscontrol(val)) {
                                uint8_t wchr[4];
                                size_t wchr_len = code_to_utf8(wchr, (uint32_t)val);
                                PRINT(printer_print_buf(&printer, (const char*)wchr, wchr_len));
                                len = next_len;
                                rest = next_rest;
                                continue;
                            }
                        }
                    }
                    break; // print the rest of this element raw
                }
                PRINT_CH(&printer, ch);
                len = next_len;
                rest = next_rest;
            } else {
                size_t j = 0;
                for (;j < len && rest[j] != '$' && rest[j] != '.';j++);
                if (j == len) {
                    break;
                }
                PRINT(printer_print_buf(&printer, rest, j));
                rest += j;
                len -= j;
            }
        }
        PRINT(printer_print_buf(&printer, rest, len));
    }

    if (printer.out_len < OVERFLOW_MARGIN) {
        return OverflowOverflow;
    }
    *printer.out = '\0';
    return OverflowOk;
}

static bool is_symbol_like(const char *s, size_t len) {
    // rust-demangle definition of symbol like: control characters and space are not symbol-like, all else is
    for (size_t i = 0; i < len; i++) {
        char ch = s[i];
        if (!(ch >= 0x21 && ch <= 0x7e)) {
            return false;
        }
    }
    return true;
}

void rust_demangle_demangle(const char *s, struct demangle *res)
{
    // During ThinLTO LLVM may import and rename internal symbols, so strip out
    // those endings first as they're one of the last manglings applied to symbol
    // names.
    const char *llvm = ".llvm.";
    const char *found_llvm = strstr(s, llvm);
    size_t s_len = strlen(s);
    if (found_llvm) {
        const char *all_hex_ptr = found_llvm + strlen(".llvm.");
        bool all_hex = true;
        for (;*all_hex_ptr;all_hex_ptr++) {
            if (!(('0' <= *all_hex_ptr && *all_hex_ptr <= '9') ||
                  ('A' <= *all_hex_ptr && *all_hex_ptr <= 'F') ||
                  *all_hex_ptr == '@')) {
                all_hex = false;
                break;
            }
        }

        if (all_hex) {
            s_len = found_llvm - s;
        }
    }

    const char *suffix;
    struct demangle_legacy legacy;
    demangle_status st = rust_demangle_legacy_demangle(s, s_len, &legacy, &suffix);
    if (st == DemangleOk) {
        *res = (struct demangle) {
            .style=DemangleStyleLegacy,
            .mangled=legacy.mangled,
            .mangled_len=legacy.mangled_len,
            .elements=legacy.elements,
            .original=s,
            .original_len=s_len,
            .suffix=suffix,
            .suffix_len=s_len - (suffix - s),
        };
    } else {
        struct demangle_v0 v0;
        st = rust_demangle_v0_demangle(s, s_len, &v0, &suffix);
        if (st == DemangleOk) {
            *res = (struct demangle) {
                .style=DemangleStyleV0,
                .mangled=v0.mangled,
                .mangled_len=v0.mangled_len,
                .elements=0,
                .original=s,
                .original_len=s_len,
                .suffix=suffix,
                .suffix_len=s_len - (suffix - s),
            };
        } else {
            *res = (struct demangle) {
                .style=DemangleStyleUnknown,
                .mangled=NULL,
                .mangled_len=0,
                .elements=0,
                .original=s,
                .original_len=s_len,
                .suffix=s,
                .suffix_len=0,
            };
        }
    }

    // Output like LLVM IR adds extra period-delimited words. See if
    // we are in that case and save the trailing words if so.
    if (res->suffix_len) {
        if (res->suffix[0] == '.' && is_symbol_like(res->suffix, res->suffix_len)) {
            // Keep the suffix
        } else {
            // Reset the suffix and invalidate the demangling
            res->style = DemangleStyleUnknown;
            res->suffix_len = 0;
        }
    }
}

bool rust_demangle_is_known(struct demangle *res) {
    return res->style != DemangleStyleUnknown;
}

overflow_status rust_demangle_display_demangle(struct demangle const *res, char *out, size_t len, bool alternate) {
    size_t original_len = res->original_len;
    size_t out_len;
    switch (res->style) {
    case DemangleStyleUnknown:
    if (len < original_len) {
        return OverflowOverflow;
    } else {
        memcpy(out, res->original, original_len);
        out += original_len;
        len -= original_len;
        break;
    }
    break;
    case DemangleStyleLegacy: {
        struct demangle_legacy legacy = {
            res->mangled,
            res->mangled_len,
            res->elements
        };
        if (rust_demangle_legacy_display_demangle(legacy, out, len, alternate) == OverflowOverflow) {
            return OverflowOverflow;
        }
        out_len = strlen(out);
        out += out_len;
        len -= out_len;
        break;
    }
    case DemangleStyleV0: {
        struct demangle_v0 v0 = {
            res->mangled,
            res->mangled_len
        };
        if (rust_demangle_v0_display_demangle(v0, out, len, alternate) == OverflowOverflow) {
            return OverflowOverflow;
        }
        out_len = strlen(out);
        out += out_len;
        len -= out_len;
        break;
    }
    }
    size_t suffix_len = res->suffix_len;
    if (len < suffix_len || len - suffix_len < OVERFLOW_MARGIN) {
        return OverflowOverflow;
    }
    memcpy(out, res->suffix, suffix_len);
    out[suffix_len] = 0;
    return OverflowOk;
}
