/* gs1.c - Verifies GS1 data */
/*
    libzint - the open source barcode library
    Copyright (C) 2009-2026 Robin Stuart <rstuart114@gmail.com>

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.
    3. Neither the name of the project nor the names of its contributors
       may be used to endorse or promote products derived from this software
       without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
    OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
    OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
    SUCH DAMAGE.
 */
/* SPDX-License-Identifier: BSD-3-Clause */

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#ifdef ZINT_HAVE_GS1SE
#include <gs1encoders.h>
#endif
#include "common.h"
#include "gs1.h"

/* gs1_lint() validators and checkers */

/* Set `err_msg`, returning 0 for convenience */
static int gs1_err_msg_cpy_nochk(char err_msg[50], const char *msg) {
    const int length = (int) strlen(msg);

    assert(length < 50);
    memcpy(err_msg, msg, length + 1); /* Include terminating NUL */

    return 0;
}

/* sprintf into `err_msg`, returning 0 for convenience */
ZINT_FORMAT_PRINTF(2, 3) static int gs1_err_msg_printf_nochk(char err_msg[50], const char *fmt, ...) {
    va_list ap;
    int size;
    va_start(ap, fmt);

#ifdef Z_NO_VSNPRINTF
    size = vsprintf(err_msg, fmt, ap);
#else
    size = vsnprintf(err_msg, 50, fmt, ap);
#endif

    (void)size;
    assert(size >= 0);
    assert(size < 50);

    va_end(ap);

    return 0;
}

/* Validate numeric */
static int gs1_numeric(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50]) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            if (!z_isdigit(*d)) {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Non-numeric character '%c'", *d);
            }
        }
    }

    return 1;
}

/* GS1 General Specifications Release 26.0 Table 7-18 GS1 AI encodable character reference values.
   Also used to determine if character in set 82 - a value of 82 means not in */
static const char gs1_c82[] = {
    /*   !   "   #   $   %   &   '   (   )   *   */
         0,  1, 82, 82,  2,  3,  4,  5,  6,  7,
    /*   +   ,   -   .   /   0   1   2   3   4   */
         8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
    /*   5   6   7   8   9   :   ;   <   =   >   */
        18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
    /*   ?   @   A   B   C   D   E   F   G   H   */
        28, 82, 29, 30, 31, 32, 33, 34, 35, 36,
    /*   I   J   K   L   M   N   O   P   Q   R   */
        37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
    /*   S   T   U   V   W   X   Y   Z   [   \   */
        47, 48, 49, 50, 51, 52, 53, 54, 82, 82,
    /*   ]   ^   _   `   a   b   c   d   e   f   */
        82, 82, 55, 82, 56, 57, 58, 59, 60, 61,
    /*   g   h   i   j   k   l   m   n   o   p   */
        62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
    /*   q   r   s   t   u   v   w   x   y   z   */
        72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
};

/* Validate of character set 82
   (GS1 General Specifications Release 26.0 Table 7-20 GS1 AI encodable character set 82) */
static int gs1_cset82(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50]) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            if (*d < '!' || *d > 'z' || gs1_c82[*d - '!'] == 82) {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Invalid CSET 82 character '%c'", *d);
            }
        }
    }

    return 1;
}

/* Validate of character set 39
   (GS1 General Specifications Release 26.0 Table 7-21 GS1 AI encodable character set 39) */
static int gs1_cset39(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50]) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            /* 0-9, A-Z and "#", "-", "/" */
            if ((*d < '0' && *d != '#' && *d != '-' && *d != '/') || (*d > '9' && *d < 'A') || *d > 'Z') {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Invalid CSET 39 character '%c'", *d);
            }
        }
    }

    return 1;
}

/* Validate of character set 64 (GSCN 21-307 Figure 7.11-3) */
static int gs1_cset64(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50]) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            /* 0-9, A-Z, a-z and "-", "_" */
            if ((*d < '0' && *d != '-') || (*d > '9' && *d < 'A') || (*d > 'Z' && *d < 'a' && *d != '_')
                    || *d > 'z') {
                /* One or two "="s can be used as padding to mod 3 length */
                if (*d == '=' && (d + 1 == de || (d + 2 == de && *(d + 1) == '=')) && data_len % 3 == 0) {
                    break;
                }
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Invalid CSET 64 character '%c'", *d);
            }
        }
    }

    return 1;
}

/* Check a check digit
   (GS1 General Specifications Release 26.0 7.9.1 Standard check digit calculations for GS1 data structures) */
static int gs1_csum(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len) - 1; /* Note less last character */
        int checksum = 0;
        int factor = (min & 1) ? 1 : 3;

        for (; d < de; d++) {
            checksum += (*d - '0') * factor;
            factor ^= 0x02; /* Toggles 1 and 3 */
        }
        checksum = 10 - checksum % 10;
        if (checksum == 10) {
            checksum = 0;
        }
        if (checksum != *d - '0') {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Bad checksum '%c', expected '%c'", *d, checksum + '0');
        }
    }

    return 1;
}

/* Check alphanumeric check characters
   (GS1 General Specifications Release 26.0 7.9.5 Check character calculation (for alphanumeric keys)) */
static int gs1_csumalpha(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    /* Do this check separately for backward compatibility */
    if (data_len && data_len < 2) {
        *p_err_no = 4;
        return 0;
    }

    if (!length_only && data_len) {
        static const char c32[] = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
        static const char weights[] = {
            2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83
        };
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len) - 2; /* Note less last 2 characters */
        int checksum = 0, c1, c2;

        for (; d < de; d++) {
            checksum += gs1_c82[*d - '!'] * weights[de - 1 - d];
        }
        checksum %= 1021;
        c1 = c32[checksum >> 5];
        c2 = c32[checksum & 0x1F];

        if (de[0] != c1 || de[1] != c2) {
            *p_err_no = 3;
            *p_err_posn = (int) (de - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Bad checksum '%.2s', expected '%c%c'", de, c1, c2);
        }
    }

    return 1;
}

#define GS1_GCP_MIN_LENGTH  4 /* Minimum length of GS1 Company Prefix */

/* Check for a GS1 Prefix (GS1 General Specifications Release 26.0 1.2.3.3 GS1 Company Prefix) */
static int gs1_gcppos1(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    /* Do this check separately for backward compatibility */
    if (data_len && data_len < 2) {
        *p_err_no = 4;
        return 0;
    }

    if (!length_only && data_len) {
        int i;

        if (data_len < GS1_GCP_MIN_LENGTH) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "GS1 Company Prefix length %d too short (minimum 4)", data_len);
        }

        data += offset;

        for (i = 0; i < GS1_GCP_MIN_LENGTH; i++) {
            if (!z_isdigit(data[i])) {
                *p_err_no = 3;
                *p_err_posn = offset + i + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Non-numeric company prefix '%c'", data[i]);
            }
        }
    }

    return 1;
}

/* Check for a GS1 Prefix at offset 1 (2nd position) */
static int gs1_gcppos2(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    return gs1_gcppos1(data, data_len, offset + 1, min - 1, max - 1, p_err_no, p_err_posn, err_msg, length_only);
}

/* Note following date/time checkers (!length_only) assume data all digits, i.e. `numeric()` has succeeded */

/* Check for a date YYYYMMDD with zero day allowed */
static int gs1_yyyymmd0(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    static const char days_in_month[13] = { 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 8)) {
        return 0;
    }

    if (!length_only && data_len) {
        int month, day;

        month = z_to_int(data + offset + 4, 2);
        if (month == 0 || month > 12) {
            *p_err_no = 3;
            *p_err_posn = offset + 4 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid month '%.2s'", data + offset + 4);
        }

        day = z_to_int(data + offset + 6, 2);
        if (day && day > days_in_month[month]) {
            *p_err_no = 3;
            *p_err_posn = offset + 6 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid day '%.2s'", data + offset + 6);
        }
        /* Leap year check */
        if (month == 2 && day == 29) {
            const int year = z_to_int(data + offset, 4);
            if ((year & 3) || (year % 100 == 0 && year % 400 != 0)) {
                *p_err_no = 3;
                *p_err_posn = offset + 6 + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Invalid day '%.2s'", data + offset + 6);
            }
        }
    }

    return 1;
}

/* Check for a date YYYYMMDD. Zero day NOT allowed */
static int gs1_yyyymmdd(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    if (!gs1_yyyymmd0(data, data_len, offset, min, max, p_err_no, p_err_posn, err_msg, length_only)) {
        return 0;
    }

    data_len = data_len < offset ? 0 : data_len - offset;

    if (!length_only && data_len) {
        const int day = z_to_int(data + offset + 6, 2);
        if (day == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 6 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid day '%.2s'", data + offset + 6);
        }
    }

    return 1;
}

/* Check for a date YYMMDD with zero day allowed */
static int gs1_yymmd0(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 6)) {
        return 0;
    }

    if (!length_only && data_len) {
        /* For leap year detection, only matters if 00 represents century divisible by 400 or not */
        /* Following good until 2050 when 00 will mean 2100
           (GS1 General Specifications Release 26.0 7.12 Determination of century in dates) */
        unsigned char buf[8] = { '2', '0' };

        memcpy(buf + 2, data + offset, 6);
        if (!gs1_yyyymmd0(buf, 8, 0, min, max, p_err_no, p_err_posn, err_msg, length_only)) {
            *p_err_posn += offset - 2;
            return 0;
        }
    }

    return 1;
}

/* Check for a date YYMMDD. Zero day NOT allowed */
static int gs1_yymmdd(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    if (!gs1_yymmd0(data, data_len, offset, min, max, p_err_no, p_err_posn, err_msg, length_only)) {
        return 0;
    }

    data_len = data_len < offset ? 0 : data_len - offset;

    if (!length_only && data_len) {
        const int day = z_to_int(data + offset + 4, 2);
        if (day == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 4 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid day '%.2s'", data + offset + 4);
        }
    }

    return 1;
}

/* Check for a time HHMI */
static int gs1_hhmi(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 4)) {
        return 0;
    }

    if (!length_only && data_len) {
        int hour, mins;

        hour = z_to_int(data + offset, 2);
        if (hour > 23) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid hour of day '%.2s'", data + offset);
        }
        mins = z_to_int(data + offset + 2, 2);
        if (mins > 59) {
            *p_err_no = 3;
            *p_err_posn = offset + 2 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid minutes in the hour '%.2s'", data + offset + 2);
        }
    }

    return 1;
}

/* Check for a time HH (hours) */
static int gs1_hh(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 2)) {
        return 0;
    }

    if (!length_only && data_len) {
        const int hour = z_to_int(data + offset, 2);
        if (hour > 23) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid hour of day '%.2s'", data + offset);
        }
    }

    return 1;
}

/* Check for a time MI (minutes) */
static int gs1_mi(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 2)) {
        return 0;
    }

    if (!length_only && data_len) {
        const int mins = z_to_int(data + offset, 2);
        if (mins > 59) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid minutes in the hour '%.2s'", data + offset);
        }
    }

    return 1;
}

/* Check for a time SS (seconds) */
static int gs1_ss(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 2)) {
        return 0;
    }

    if (!length_only && data_len) {
        const int secs = z_to_int(data + offset, 2);
        if (secs > 59) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid seconds in the minute '%.2s'", data + offset);
        }
    }

    return 1;
}

/* Generated by "php backend/tools/gen_iso3166_h.php > backend/iso3166.h" */
#include "iso3166.h"

/* Check for an ISO 3166-1 numeric country code */
static int gs1_iso3166(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 3)) {
        if (offset) {
            /* For backward compatibility only warn if not first */
            *p_err_no = 4;
        }
        return 0;
    }

    if (!length_only && data_len) {
        if (!iso3166_numeric(z_to_int(data + offset, 3))) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Unknown country code '%.3s'", data + offset);
        }
    }

    return 1;
}

/* Check for an ISO 3166-1 numeric country code allowing "999" */
static int gs1_iso3166999(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 3)) {
        return 0;
    }

    if (!length_only && data_len) {
        const int cc = z_to_int(data + offset, 3);
        if (cc != 999 && !iso3166_numeric(cc)) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Unknown country code '%.3s'", data + offset);
        }
    }

    return 1;
}

/* Check for an ISO 3166-1 alpha2 country code */
static int gs1_iso3166alpha2(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 2)) {
        return 0;
    }

    if (!length_only && data_len) {
        if (!iso3166_alpha2((const char *) (data + offset))) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Unknown country code '%.2s'", data + offset);
        }
    }

    return 1;
}

/* Generated by "php backend/tools/gen_iso4217_h.php > backend/iso4217.h" */
#include "iso4217.h"

/* Check for an ISO 4217 currency code */
static int gs1_iso4217(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 3)) {
        return 0;
    }

    if (!length_only && data_len) {
        if (!iso4217_numeric(z_to_int(data + offset, 3))) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Unknown currency code '%.3s'", data + offset);
        }
    }

    return 1;
}

/* Check for percent encoded */
static int gs1_pcenc(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            if (*d == '%') {
                if (de - d < 3) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - data) + 1;
                    return gs1_err_msg_cpy_nochk(err_msg, "Invalid % escape");
                }
                d += 2;
                if (!z_isxdigit(*(d - 1)) || !z_isxdigit(*d)) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - data) + 1;
                    return gs1_err_msg_cpy_nochk(err_msg, "Invalid character for percent encoding");
                }
            }
        }
    }

    return 1;
}

/* Check for yes/no (1/0) indicator */
static int gs1_yesno(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        if (data[offset] != '0' && data[offset] != '1') {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Neither 0 nor 1 for yes or no");
        }
    }

    return 1;
}

/* Check for importer index
   (GS1 General Specifications Release 26.0 3.8.18 GS1 UIC with Extension 1 and Importer index: AI (7040)) */
static int gs1_importeridx(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;

        /* 0-9, A-Z, a-z and "-", "_" */
        if ((*d < '0' && *d != '-') || (*d > '9' && *d < 'A') || (*d > 'Z' && *d < 'a' && *d != '_') || *d > 'z') {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid importer index '%c'", *d);
        }
    }

    return 1;
}

/* Check non-zero */
static int gs1_nonzero(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const int val = z_to_int(data + offset, data_len > max ? max : data_len);

        if (val == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Zero not permitted");
        }
    }

    return 1;
}

/* Check winding direction (0/1/9) (GS1 General Specifications Release 26.0 3.9.1 Roll products) */
static int gs1_winding(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        if (data[offset] != '0' && data[offset] != '1' && data[offset] != '9') {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid winding direction '%c'", data[offset]);
        }
    }

    return 1;
}

/* Check zero */
static int gs1_zero(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        if (data[offset] != '0') {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Zero is required");
        }
    }

    return 1;
}

/* Check piece of a trade item
   (GS1 General Specifications Release 26.0 3.9.6 Identification of an individual trade item piece
    and 3.9.18 Identification of pieces of a trade item (ITIP) contained in a logistic unit) */
static int gs1_pieceoftotal(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 4)) {
        return 0;
    }

    if (!length_only && data_len) {
        int pieces, total;

        pieces = z_to_int(data + offset, 2);
        if (pieces == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Piece number cannot be zero");
        }
        total = z_to_int(data + offset + 2, 2);
        if (total == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Total number cannot be zero");
        }
        if (pieces > total) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Piece number '%.2s' exceeds total '%.2s'",
                        data + offset, data + offset + 2);
        }
    }

    return 1;
}

/* Check IBAN (ISO 13616) */
static int gs1_iban(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    /* Do this check separately for backward compatibility */
    if (data_len && data_len < 5) {
        *p_err_no = 4;
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);
        int checksum = 0;
        int given_checksum;

        /* 1st 2 chars alphabetic country code */
        if (!z_isupper(d[0]) || !z_isupper(d[1])) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Non-alphabetic IBAN country code '%.2s'", d);
        }
        if (!iso3166_alpha2((const char *) d)) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid IBAN country code '%.2s'", d);
        }
        d += 2;
        /* 2nd 2 chars numeric checksum */
        if (!z_isdigit(d[0]) || !z_isdigit(d[1])) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Non-numeric IBAN checksum '%.2s'", d);
        }
        given_checksum = z_to_int(d, 2);
        d += 2;
        for (; d < de; d++) {
            /* 0-9, A-Z */
            if (*d < '0' || (*d > '9' && *d < 'A') || *d > 'Z') {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_printf_nochk(err_msg, "Invalid IBAN character '%c'", *d);
            }
            if (*d >= 'A') {
                checksum = checksum * 100 + *d - 'A' + 10;
            } else {
                checksum = checksum * 10 + *d - '0';
            }
            checksum %= 97;
        }

        /* Add in country code */
        checksum = (((checksum * 100) % 97) + (data[offset] - 'A' + 10)) * 100 + data[offset + 1] - 'A' + 10;
        checksum %= 97;

        checksum *= 100; /* Allow for checksum "00" */
        checksum %= 97;

        checksum = 98 - checksum;

        if (checksum != given_checksum) {
            *p_err_no = 3;
            *p_err_posn = offset + 2 + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Bad IBAN checksum '%.2s', expected '%02d'",
                        data + offset + 2, checksum);
        }
    }

    return 1;
}

/* Check CPID does not begin with zero */
static int gs1_nozeroprefix(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        /* GS1 General Specifications Release 26.0 3.9.11 "The C/P serial number SHALL NOT begin with a "0" digit,
           unless the entire serial number consists of the single digit '0'." */
        if (data[0] == '0' && data_len != 1) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Zero prefix is not permitted");
        }
    }

    return 1;
}

/* Helper to parse coupon Variable Length Indicator (VLI) and associated field. If `vli_nine` set
 * then a VLI of '9' means no field present */
static const unsigned char *gs1_coupon_vli(const unsigned char *data, const int data_len, const unsigned char *d,
            const char *name, const int vli_offset, const int vli_min, const int vli_max, const int vli_nine,
            int *p_err_no, int *p_err_posn, char err_msg[50]) {
    const unsigned char *de;
    int vli;

    if (d - data + 1 > data_len) {
        *p_err_no = 3;
        *p_err_posn = (int) (d - data) + 1;
        (void) gs1_err_msg_printf_nochk(err_msg, "%s VLI missing", name);
        return NULL;
    }
    vli = z_to_int(d, 1);
    if ((vli < vli_min || vli > vli_max) && (vli != 9 || !vli_nine)) {
        *p_err_no = 3;
        *p_err_posn = (int) (d - data) + 1;
        if (vli < 0) {
            (void) gs1_err_msg_printf_nochk(err_msg, "Non-numeric %s VLI '%c'", name, *d);
        } else {
            (void) gs1_err_msg_printf_nochk(err_msg, "Invalid %s VLI '%c'", name, *d);
        }
        return NULL;
    }
    d++;
    if (vli != 9 || !vli_nine) {
        if (d - data + vli + vli_offset > data_len) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            (void) gs1_err_msg_printf_nochk(err_msg, "%s incomplete", name);
            return NULL;
        }
        de = d + vli + vli_offset;
        for (; d < de; d++) {
            if (!z_isdigit(*d)) {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                (void) gs1_err_msg_printf_nochk(err_msg, "Non-numeric %s '%c'", name, *d);
                return NULL;
            }
        }
    }

    return d;
}

/* Helper to parse coupon value field (numeric) */
static const unsigned char *gs1_coupon_val(const unsigned char *data, const int data_len, const unsigned char *d,
            const char *name, const int val_len, int *p_val, int *p_err_no, int *p_err_posn, char err_msg[50]) {
    int val;

    if (d - data + val_len > data_len) {
        *p_err_no = 3;
        *p_err_posn = (int) (d - data) + 1;
        (void) gs1_err_msg_printf_nochk(err_msg, "%s incomplete", name);
        return NULL;
    }
    val = z_to_int(d, val_len);
    if (val < 0) {
        *p_err_no = 3;
        *p_err_posn = (int) (d - data) + 1;
        (void) gs1_err_msg_printf_nochk(err_msg, "Non-numeric %s", name);
        return NULL;
    }
    d += val_len;

    if (p_val) {
        *p_val = val;
    }
    return d;
}

/* Check North American Coupon Code */
/* Note all fields including optional must be numeric so type could be N..70 */
static int gs1_couponcode(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    /* Minimum possible required fields length = 21
     * (from "North American Coupon Application Guideline Using GS1 DataBar Expanded Symbols R2.0 (Feb 13 2015)")
     * VLI - Variable Length Indicator; GCP - GS1 Company Prefix; OC - Offer Code; SV - Save Value;
     * PPR - Primary Purchase Requirement; PPFC - Primary Purchase Family Code */
    const int min_req_len = 1 /*GCP VLI*/ + 6 /*GCP*/ + 6 /*OC*/ + 1 /*SV VLI*/ + 1 /*SV*/
                            + 1 /*PPR VLI*/ + 1 /*PPR*/ + 1 /*PPR Code*/ + 3 /*PPFC*/;

    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    /* Do separately for backward compatibility */
    if (data_len && data_len < min_req_len) {
        *p_err_no = 4;
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        int val;

        data_len += offset;

        /* Required fields */
        if (!(d = gs1_coupon_vli(data, data_len, d, "Primary GS1 Co. Prefix", 6, 0, 6, 0, p_err_no, p_err_posn,
                                err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_val(data, data_len, d, "Offer Code", 6, NULL, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_vli(data, data_len, d, "Save Value", 0, 1, 5, 0, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_vli(data, data_len, d, "Primary Purch. Req.", 0, 1, 5, 0, p_err_no, p_err_posn,
                                err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_val(data, data_len, d, "Primary Purch. Req. Code", 1, &val, p_err_no, p_err_posn,
                                err_msg))) {
            return 0;
        }
        if (val > 5 && val < 9) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - 1 - data) + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid Primary Purch. Req. Code '%c'", *(d - 1));
        }
        if (!(d = gs1_coupon_val(data, data_len, d, "Primary Purch. Family Code", 3, NULL, p_err_no, p_err_posn,
                                err_msg))) {
            return 0;
        }

        /* Optional fields */
        while (d - data < data_len) {
            const int data_field = z_to_int(d, 1);
            d++;

            if (data_field == 1) {

                if (!(d = gs1_coupon_val(data, data_len, d, "Add. Purch. Rules Code", 1, &val, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (val > 3) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid Add. Purch. Rules Code '%c'", *(d - 1));
                }
                if (!(d = gs1_coupon_vli(data, data_len, d, "2nd Purch. Req.", 0, 1, 5, 0, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "2nd Purch. Req. Code", 1, &val, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (val > 4 && val < 9) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid 2nd Purch. Req. Code '%c'", *(d - 1));
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "2nd Purch. Family Code", 3, NULL, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!(d = gs1_coupon_vli(data, data_len, d, "2nd Purch. GS1 Co. Prefix", 6, 0, 6, 1, p_err_no,
                                        p_err_posn, err_msg))) {
                    return 0;
                }

            } else if (data_field == 2) {

                if (!(d = gs1_coupon_vli(data, data_len, d, "3rd Purch. Req.", 0, 1, 5, 0, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "3rd Purch. Req. Code", 1, &val, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (val > 4 && val < 9) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid 3rd Purch. Req. Code '%c'", *(d - 1));
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "3rd Purch. Family Code", 3, NULL, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!(d = gs1_coupon_vli(data, data_len, d, "3rd Purch. GS1 Co. Prefix", 6, 0, 6, 1, p_err_no,
                                        p_err_posn, err_msg))) {
                    return 0;
                }

            } else if (data_field == 3) {

                if (!(d = gs1_coupon_val(data, data_len, d, "Expiration Date", 6, NULL, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!gs1_yymmd0(data, data_len, (int) (d - 6 - data), 6, 6, p_err_no, p_err_posn, err_msg, 0)) {
                    return 0;
                }

            } else if (data_field == 4) {

                if (!(d = gs1_coupon_val(data, data_len, d, "Start Date", 6, NULL, p_err_no, p_err_posn, err_msg))) {
                    return 0;
                }
                if (!gs1_yymmd0(data, data_len, (int) (d - 6 - data), 6, 6, p_err_no, p_err_posn, err_msg, 0)) {
                    return 0;
                }

            } else if (data_field == 5) {

                if (!(d = gs1_coupon_vli(data, data_len, d, "Serial Number", 6, 0, 9, 0, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }

            } else if (data_field == 6) {

                if (!(d = gs1_coupon_vli(data, data_len, d, "Retailer ID", 6, 1, 7, 0, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }

            } else if (data_field == 9) {

                if (!(d = gs1_coupon_val(data, data_len, d, "Save Value Code", 1, &val, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if ((val > 2 && val < 5) || val > 6) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid Save Value Code '%c'", *(d - 1));
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "Save Value Applies To", 1, &val, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (val > 2) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid Save Value Applies To '%c'", *(d - 1));
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "Store Coupon Flag", 1, NULL, p_err_no, p_err_posn,
                                        err_msg))) {
                    return 0;
                }
                if (!(d = gs1_coupon_val(data, data_len, d, "Don't Multiply Flag", 1, &val, p_err_no, p_err_posn,
                                    err_msg))) {
                    return 0;
                }
                if (val > 1) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - 1 - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid Don't Multiply Flag '%c'", *(d - 1));
                }

            } else {

                *p_err_no = 3;
                *p_err_posn = (int) (d - 1 - data) + 1;
                if (data_field < 0) {
                    (void) gs1_err_msg_printf_nochk(err_msg, "Non-numeric Data Field '%c'", *(d - 1));
                } else {
                    (void) gs1_err_msg_printf_nochk(err_msg, "Invalid Data Field '%c'", *(d - 1));
                }
                return 0;
            }
        }
    }

    return 1;
}

/* Check North American Positive Offer File */
/* Note max is currently set at 36 numeric digits with remaining 34 characters reserved */
static int gs1_couponposoffer(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    /* Minimum possible length = 21
     * (from "GS1 AI (8112) Coupon Data Specifications Release 1.0 (March 2020)")
     * CFMT - Coupon Format; CFID - Coupon Funder ID; VLI - Variable Length Indicator;
     * OC - Offer Code; SN - Serial Number */
    const int min_len = 1 /*CFMT*/ + 1 /*CFID VLI*/ + 6 /*CFID*/ + 6 /*OC*/ + 1 /*SN VLI*/ + 6 /*SN*/;
    const int max_len = 36;

    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    /* Do separately for backward compatibility */
    if (data_len && (data_len < min_len || data_len > max_len)) {
        *p_err_no = 4;
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        int val;

        if (!(d = gs1_coupon_val(data, data_len, d, "Coupon Format", 1, &val, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (val != 0 && val != 1) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - 1 - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Coupon Format must be 0 or 1");
        }
        if (!(d = gs1_coupon_vli(data, data_len, d, "Coupon Funder ID", 6, 0, 6, 0, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_val(data, data_len, d, "Offer Code", 6, NULL, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (!(d = gs1_coupon_vli(data, data_len, d, "Serial Number", 6, 0, 9, 0, p_err_no, p_err_posn, err_msg))) {
            return 0;
        }
        if (d - data != data_len) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Reserved trailing characters");
        }
    }

    return 1;
}

/* Check WSG 84 latitude */
static int gs1_latitude(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 10)) {
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);
        uint64_t lat = 0;

        for (; d < de; d++) {
            lat *= 10;
            lat += *d - '0';
        }
        if (lat > 1800000000) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - 1 - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Invalid latitude");
        }
    }

    return 1;
}

/* Check WSG 84 longitude */
static int gs1_longitude(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 10)) {
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);
        uint64_t lng = 0;

        for (; d < de; d++) {
            lng *= 10;
            lng += *d - '0';
        }
        if (lng > 3600000000) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - 1 - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Invalid longitude");
        }
    }

    return 1;
}

/* Check AIDC media type (GSCN 22-345 Figure 3.8.22-2) */
static int gs1_mediatype(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    if (data_len < min || (data_len && data_len < 2)) {
        return 0;
    }

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);
        unsigned int val = 0;

        for (; d < de; d++) {
            val *= 10;
            val += *d - '0';
        }
        if (val == 0 || (val > 10 && val < 80)) {
            *p_err_no = 3;
            *p_err_posn = (int) (d - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Invalid AIDC media type");
        }
    }

    return 1;
}

/* Check negative temperature indicator (GSCN 22-353) */
static int gs1_hyphen(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de; d++) {
            if (*d != '-') {
                *p_err_no = 3;
                *p_err_posn = (int) (d - data) + 1;
                return gs1_err_msg_cpy_nochk(err_msg, "Invalid temperature indicator (hyphen only)");
            }
        }
    }

    return 1;
}

/* Check for an ISO/IEC 5128 code for the representation of human sexes (GSCN 22-246) */
static int gs1_iso5218(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        /* 0 = Not known, 1 = Male, 2 = Female, 9 = Not applicable */
        if (data[offset] != '0' && data[offset] != '1' && data[offset] != '2' && data[offset] != '9') {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Invalid biological sex code (0, 1, 2 or 9 only)");
        }
    }

    return 1;
}

/* Validate sequence indicator, slash-separated (GSCN 22-246) */
static int gs1_posinseqslash(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);
        const unsigned char *slash = NULL;
        int pos, tot;

        for (; d < de; d++) {
            if (!z_isdigit(*d)) {
                if (*d != '/') {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - data) + 1;
                    return gs1_err_msg_printf_nochk(err_msg, "Invalid character '%c' in sequence", *d);
                }
                if (slash) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - data) + 1;
                    return gs1_err_msg_cpy_nochk(err_msg, "Single sequence separator ('/') only");
                }
                if (d == data + offset || d + 1 == de) {
                    *p_err_no = 3;
                    *p_err_posn = (int) (d - data) + 1;
                    return gs1_err_msg_cpy_nochk(err_msg, "Sequence separator '/' cannot start or end");
                }
                slash = d;
            }
        }
        if (!slash) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "No sequence separator ('/')");
        }
        pos = z_to_int(data + offset, (int) (slash - (data + offset)));
        if (pos == 0) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Sequence position cannot be zero");
        }
        tot = z_to_int(slash + 1, (int) (de - (slash + 1)));
        if (tot == 0) {
            *p_err_no = 3;
            *p_err_posn = (int) (slash + 1 - data) + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Sequence total cannot be zero");
        }
        if (pos > tot) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "Sequence position greater than total");
        }
    }

    return 1;
}

/* Check that input contains non-digit (GSCN 21-283) */
static int gs1_hasnondigit(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {
    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        const unsigned char *d = data + offset;
        const unsigned char *const de = d + (data_len > max ? max : data_len);

        for (; d < de && z_isdigit(*d); d++);

        if (d == de) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_cpy_nochk(err_msg, "A non-digit character is required");
        }
    }

    return 1;
}

/* Check for package type (GSCN 23-272) */
static int gs1_packagetype(const unsigned char *data, int data_len, int offset, int min, int max, int *p_err_no,
            int *p_err_posn, char err_msg[50], const int length_only) {

    /* Package type codes https://navigator.gs1.org/edi/codelist-details?name=PackageTypeCode */
    static const char packagetypes2[381][2] = {
        {'1','A'}, {'1','B'}, {'1','D'}, {'1','F'}, {'1','G'}, {'1','W'}, {'2','C'}, {'3','A'}, {'3','H'}, {'4','3'},
        {'4','4'}, {'4','A'}, {'4','B'}, {'4','C'}, {'4','D'}, {'4','F'}, {'4','G'}, {'4','H'}, {'5','H'}, {'5','L'},
        {'5','M'}, {'6','H'}, {'6','P'}, {'7','A'}, {'7','B'}, {'8','A'}, {'8','B'}, {'8','C'}, {'A','A'}, {'A','B'},
        {'A','C'}, {'A','D'}, {'A','F'}, {'A','G'}, {'A','H'}, {'A','I'}, {'A','J'}, {'A','L'}, {'A','M'}, {'A','P'},
        {'A','T'}, {'A','V'}, {'B','4'}, {'B','B'}, {'B','C'}, {'B','D'}, {'B','E'}, {'B','F'}, {'B','G'}, {'B','H'},
        {'B','I'}, {'B','J'}, {'B','K'}, {'B','L'}, {'B','M'}, {'B','N'}, {'B','O'}, {'B','P'}, {'B','Q'}, {'B','R'},
        {'B','S'}, {'B','T'}, {'B','U'}, {'B','V'}, {'B','W'}, {'B','X'}, {'B','Y'}, {'B','Z'}, {'C','A'}, {'C','B'},
        {'C','C'}, {'C','D'}, {'C','E'}, {'C','F'}, {'C','G'}, {'C','H'}, {'C','I'}, {'C','J'}, {'C','K'}, {'C','L'},
        {'C','M'}, {'C','N'}, {'C','O'}, {'C','P'}, {'C','Q'}, {'C','R'}, {'C','S'}, {'C','T'}, {'C','U'}, {'C','V'},
        {'C','W'}, {'C','X'}, {'C','Y'}, {'C','Z'}, {'D','A'}, {'D','B'}, {'D','C'}, {'D','G'}, {'D','H'}, {'D','I'},
        {'D','J'}, {'D','K'}, {'D','L'}, {'D','M'}, {'D','N'}, {'D','P'}, {'D','R'}, {'D','S'}, {'D','T'}, {'D','U'},
        {'D','V'}, {'D','W'}, {'D','X'}, {'D','Y'}, {'E','1'}, {'E','2'}, {'E','3'}, {'E','C'}, {'E','D'}, {'E','E'},
        {'E','F'}, {'E','G'}, {'E','H'}, {'E','I'}, {'E','N'}, {'F','B'}, {'F','C'}, {'F','D'}, {'F','E'}, {'F','I'},
        {'F','L'}, {'F','O'}, {'F','P'}, {'F','R'}, {'F','T'}, {'F','W'}, {'F','X'}, {'G','B'}, {'G','I'}, {'G','L'},
        {'G','R'}, {'G','U'}, {'G','Y'}, {'G','Z'}, {'H','A'}, {'H','B'}, {'H','C'}, {'H','G'}, {'H','N'}, {'H','R'},
        {'I','A'}, {'I','B'}, {'I','C'}, {'I','D'}, {'I','E'}, {'I','F'}, {'I','G'}, {'I','H'}, {'I','K'}, {'I','L'},
        {'I','N'}, {'I','Z'}, {'J','B'}, {'J','C'}, {'J','G'}, {'J','R'}, {'J','T'}, {'J','Y'}, {'K','G'}, {'K','I'},
        {'L','E'}, {'L','G'}, {'L','T'}, {'L','U'}, {'L','V'}, {'L','Z'}, {'M','A'}, {'M','B'}, {'M','C'}, {'M','E'},
        {'M','R'}, {'M','S'}, {'M','T'}, {'M','W'}, {'M','X'}, {'N','A'}, {'N','E'}, {'N','F'}, {'N','G'}, {'N','S'},
        {'N','T'}, {'N','U'}, {'N','V'}, {'O','A'}, {'O','B'}, {'O','C'}, {'O','D'}, {'O','E'}, {'O','F'}, {'O','K'},
        {'O','T'}, {'O','U'}, {'P','2'}, {'P','A'}, {'P','B'}, {'P','C'}, {'P','D'}, {'P','E'}, {'P','F'}, {'P','G'},
        {'P','H'}, {'P','I'}, {'P','J'}, {'P','K'}, {'P','L'}, {'P','N'}, {'P','O'}, {'P','P'}, {'P','R'}, {'P','T'},
        {'P','U'}, {'P','V'}, {'P','X'}, {'P','Y'}, {'P','Z'}, {'Q','A'}, {'Q','B'}, {'Q','C'}, {'Q','D'}, {'Q','F'},
        {'Q','G'}, {'Q','H'}, {'Q','J'}, {'Q','K'}, {'Q','L'}, {'Q','M'}, {'Q','N'}, {'Q','P'}, {'Q','Q'}, {'Q','R'},
        {'Q','S'}, {'R','D'}, {'R','G'}, {'R','J'}, {'R','K'}, {'R','L'}, {'R','O'}, {'R','T'}, {'R','Z'}, {'S','1'},
        {'S','A'}, {'S','B'}, {'S','C'}, {'S','D'}, {'S','E'}, {'S','H'}, {'S','I'}, {'S','K'}, {'S','L'}, {'S','M'},
        {'S','O'}, {'S','P'}, {'S','S'}, {'S','T'}, {'S','U'}, {'S','V'}, {'S','W'}, {'S','X'}, {'S','Y'}, {'S','Z'},
        {'T','1'}, {'T','B'}, {'T','C'}, {'T','D'}, {'T','E'}, {'T','G'}, {'T','I'}, {'T','K'}, {'T','L'}, {'T','N'},
        {'T','O'}, {'T','R'}, {'T','S'}, {'T','T'}, {'T','U'}, {'T','V'}, {'T','W'}, {'T','Y'}, {'T','Z'}, {'U','C'},
        {'U','N'}, {'V','A'}, {'V','G'}, {'V','I'}, {'V','K'}, {'V','L'}, {'V','N'}, {'V','O'}, {'V','P'}, {'V','Q'},
        {'V','R'}, {'V','S'}, {'V','Y'}, {'W','A'}, {'W','B'}, {'W','C'}, {'W','D'}, {'W','F'}, {'W','G'}, {'W','H'},
        {'W','J'}, {'W','K'}, {'W','L'}, {'W','M'}, {'W','N'}, {'W','P'}, {'W','Q'}, {'W','R'}, {'W','S'}, {'W','T'},
        {'W','U'}, {'W','V'}, {'W','W'}, {'W','X'}, {'W','Y'}, {'W','Z'}, {'X','3'}, {'X','A'}, {'X','B'}, {'X','C'},
        {'X','D'}, {'X','F'}, {'X','G'}, {'X','H'}, {'X','J'}, {'X','K'}, {'Y','A'}, {'Y','B'}, {'Y','C'}, {'Y','D'},
        {'Y','F'}, {'Y','G'}, {'Y','H'}, {'Y','J'}, {'Y','K'}, {'Y','L'}, {'Y','M'}, {'Y','N'}, {'Y','P'}, {'Y','Q'},
        {'Y','R'}, {'Y','S'}, {'Y','T'}, {'Y','V'}, {'Y','W'}, {'Y','X'}, {'Y','Y'}, {'Y','Z'}, {'Z','A'}, {'Z','B'},
        {'Z','C'}, {'Z','D'}, {'Z','F'}, {'Z','G'}, {'Z','H'}, {'Z','J'}, {'Z','K'}, {'Z','L'}, {'Z','M'}, {'Z','N'},
        {'Z','P'}, {'Z','Q'}, {'Z','R'}, {'Z','S'}, {'Z','T'}, {'Z','U'}, {'Z','V'}, {'Z','W'}, {'Z','X'}, {'Z','Y'},
        {'Z','Z'},
    };
    static const char packagetypes3[48][3] = {
        {'2','0','0'}, {'2','0','1'}, {'2','0','2'}, {'2','0','3'}, {'2','0','4'},
        {'2','0','5'}, {'2','0','6'}, {'2','1','0'}, {'2','1','1'}, {'2','1','2'},
        {'A','P','E'}, {'B','G','E'}, {'B','M','E'}, {'B','R','I'}, {'C','B','L'},
        {'C','C','E'}, {'D','P','E'}, {'F','O','B'}, {'F','P','E'}, {'L','A','B'},
        {'M','P','E'}, {'O','P','E'}, {'P','A','E'}, {'P','L','P'}, {'P','O','P'},
        {'P','P','E'}, {'P','U','E'}, {'R','B','1'}, {'R','B','2'}, {'R','B','3'},
        {'R','C','B'}, {'S','E','C'}, {'S','T','L'}, {'T','E','V'}, {'T','H','E'},
        {'T','R','E'}, {'T','T','E'}, {'T','W','E'}, {'U','U','E'}, {'W','R','P'},
        {'X','1','1'}, {'X','1','2'}, {'X','1','5'}, {'X','1','6'}, {'X','1','7'},
        {'X','1','8'}, {'X','1','9'}, {'X','2','0'},
    };

    (void)max;

    data_len = data_len < offset ? 0 : data_len - offset;

    (void)min;
    assert(data_len >= min); /* Min checked before being called */

    if (!length_only && data_len) {
        /* Adapted from GS1 Syntax Dictionary and Linters
           https://github.com/gs1/gs1-syntax-dictionary/blob/main/src/lint_packagetype.c */
        /* SPDX-License-Identifier: Apache-2.0 */
        const char *const d = (const char *const) (data + offset);
        int valid = 0;

        assert(2 /*single 8/9*/ + ARRAY_SIZE(packagetypes2) + ARRAY_SIZE(packagetypes3) == 431);

        if (data_len == 1) {
            valid = *d == '8' || *d == '9';
        } else if (data_len == 2) {
            int s = 0;
            int e = ARRAY_SIZE(packagetypes2);

            while (s < e) {
                const int m = s + (e - s) / 2;
                const int cmp = memcmp(packagetypes2[m], d, 2);
                if (cmp < 0) {
                    s = m + 1;
                } else if (cmp > 0) {
                    e = m;
                } else {
                    valid = 1;
                    break;
                }
            }
        } else if (data_len == 3) {
            int s = 0;
            int e = ARRAY_SIZE(packagetypes3);

            while (s < e) {
                const int m = s + (e - s) / 2;
                const int cmp = memcmp(packagetypes3[m], d, 3);
                if (cmp < 0) {
                    s = m + 1;
                } else if (cmp > 0) {
                    e = m;
                } else {
                    valid = 1;
                    break;
                }
            }
        }

        if (!valid) {
            *p_err_no = 3;
            *p_err_posn = offset + 1;
            return gs1_err_msg_printf_nochk(err_msg, "Invalid package type '%.*s'", data_len, d);
        }
    }

    return 1;
}

/* Helper to say if AI pointed to by `source` has predefined length according to GS1 General Specifications 26.0
   Figure 7-6 "Element strings with predefined length using GS1 Application Identifiers" */
static int gs1_predefined_len(const unsigned char source[]) {
    const int ai2 = z_to_int(source, 2); /* First 2 digits */

    /* NOTE: previously allowed legacy 23, removed 2026-02-26 */
    return (ai2 >= 0 && ai2 <= 4) || (ai2 >= 11 && ai2 <= 20) || (ai2 >= 31 && ai2 <= 36) || ai2 == 41;
}

/* Generated by "php backend/tools/gen_gs1_linter.php > backend/gs1_lint.h" */
#include "gs1_lint.h"

#ifdef ZINT_TEST /* Wrapper for direct testing */
INTERNAL int zint_test_gs1_lint_parse_raw_caret(const unsigned char source[], const int length,
            const int ai_max, int *ai_vals, int *ai_locs, int *data_locs, int *data_lens, int *p_ai_count,
            int *p_err_no, int *p_err_posn) {
    return gs1_lint_parse_raw_caret(source, length,
                ai_max, ai_vals, ai_locs, data_locs, data_lens, p_ai_count, p_err_no, p_err_posn);
}
#endif

/* Whether starts with Digital Link URI */
static int gs1_is_digital_link(const unsigned char *source, const int length) {
    return length >= 7 && (memcmp(source, "http://", 7) == 0 || memcmp(source, "HTTP://", 7) == 0
                            || (length >= 8 && (memcmp(source, "https://", 8) == 0
                                                || memcmp(source, "HTTPS://", 8) == 0)));
}

/* If built with GS1 Syntax Engine library */
#ifdef ZINT_HAVE_GS1SE

/* Helper to set `errtxt` on internal error and exit, freeing `ctx` */
static int gs1se_internal_errtxt(struct zint_symbol *symbol, const int err_id, gs1_encoder *ctx) {
    const char *errmsg = gs1_encoder_getErrMsg(ctx);
    z_errtxtf(0, symbol, err_id, "Internal error using GS1 Syntax Engine: %.80s", *errmsg ? errmsg : "unknown");
    gs1_encoder_free(ctx);
    return ZINT_ERROR_ENCODING_PROBLEM;
}

/* Helper to replace GSs with carets */
static void gs1se_gs_caret_sub(const unsigned char *src, const int length, unsigned char *dst) {
    int i;
    for (i = 0; i < length; i++) {
        dst[i] = src[i] == '\x1D' ? '^' : src[i];
    }
    dst[length] = '\0';
}

/* Use GS1 Syntax Engine to verify */
static int gs1se_verify(struct zint_symbol *symbol, const unsigned char source[], const int length,
                unsigned char reduced[], int *p_reduced_length) {
    static const char linear_dummies[3][19] = {
        { '[','0','1',']','1','2','3','4','5','6','7','8','9','0','1','2','3','1','|' }, /* Normal */
        { '(','0','1',')','1','2','3','4','5','6','7','8','9','0','1','2','3','1','|' }, /* Parens */
        { '^','0','1','1','2','3','4','5','6','7','8','9','0','1','2','3','1','|','^' }, /* Raw/caret */
    };
    int i, j;
    const int primary_len = z_is_composite(symbol->symbology) ? (int) strlen(symbol->primary) : 0;
    const int gs1parens_mode = !!(symbol->input_mode & GS1PARENS_MODE);
    const int gs1raw_mode = !!(symbol->input_mode & GS1RAW_MODE);
    const int gs1_caret = source[0] == '^';
    const int is_digital_link = !primary_len && !gs1_caret && gs1_is_digital_link(source, length);
    const int parens_cnt = !gs1parens_mode && !gs1raw_mode && !gs1_caret ? z_chr_cnt(source, length, '(') : 0;

    int local_length = length;
    const unsigned char *local_source = source;
    const unsigned char *local_source2 = source;
    unsigned char *local_source_buf = (unsigned char *) z_alloca(ARRAY_SIZE(symbol->primary) + 4 + length + 1);
    unsigned char *local_source2_buf = (unsigned char *) z_alloca(ARRAY_SIZE(symbol->primary) * 2 + length
                                                                    + parens_cnt + 1);
    int linear_len = 0;
    char msgBuf[120];
    gs1_encoder_init_status_t status = GS1_ENCODERS_INIT_SUCCESS;
    gs1_encoder_init_opts_t opts = {
        sizeof(gs1_encoder_init_opts_t), gs1_encoder_iDEFAULT, &status, msgBuf, sizeof(msgBuf),
        NULL /*syntaxDictionary*/
    };
    gs1_encoder *ctx;
    int gs1se_ret;

    if (length < 2 + gs1_caret) {
        return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 802, "Data does not start with an AI");
    }

    /* Only matrix symbols can encode Digital Link URIs */
    if (is_digital_link && !z_is_fixed_ratio(symbol->symbology)) {
        return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 264, "Data does not start with an AI");
    }

    /* Need "linear|composite" format to check required AIs */
    if (primary_len) {
        if (symbol->symbology == BARCODE_GS1_128_CC || symbol->symbology == BARCODE_DBAR_EXP_CC
                || symbol->symbology == BARCODE_DBAR_EXPSTK_CC) {
            /* These take GS1 data in the linear part */
            if (gs1raw_mode) {
                /* Need to add initial carets */
                local_source_buf[0] = '^';
                local_length = 1;
                /* Linear GSs */
                if (symbol->primary[0] == '\x1D') { /* Allow initial GS */
                    gs1se_gs_caret_sub(ZCUCP(symbol->primary + 1), primary_len - 1, local_source_buf + local_length);
                    local_length += primary_len - 1;
                } else {
                    gs1se_gs_caret_sub(ZCUCP(symbol->primary), primary_len, local_source_buf + local_length);
                    local_length += primary_len;
                }
                local_source_buf[local_length++] = '|';
                local_source_buf[local_length++] = '^';
                /* CC GSs */
                if (source[0] == '\x1D') { /* Allow initial GS */
                    gs1se_gs_caret_sub(source + 1, length - 1, local_source_buf + local_length);
                    local_length += length - 1;
                } else {
                    gs1se_gs_caret_sub(source, length, local_source_buf + local_length);
                    local_length += length;
                }
                linear_len = primary_len - (symbol->primary[0] == '\x1D'); /* Exclude initial GS if any */
            } else {
                memcpy(local_source_buf, symbol->primary, primary_len);
                local_source_buf[primary_len] = '|';
                memcpy(local_source_buf + primary_len + 1, source, length + 1); /* Include terminating NUL */
                local_length += primary_len + 1;
                linear_len = primary_len - 1; /* Only actual linear data counts - exclude initial caret */
            }
        } else {
            /* Just use dummy "01" linear */
            if (gs1_caret || gs1raw_mode) {
                memcpy(local_source_buf, linear_dummies[2], 18 + gs1raw_mode);
                if (gs1_caret) {
                    memcpy(local_source_buf + 18, source, length + 1); /* Include terminating NUL */
                } else {
                    gs1se_gs_caret_sub(source, length, local_source_buf + 19);
                }
                local_length += 18 + gs1raw_mode;
            } else {
                memcpy(local_source_buf, linear_dummies[gs1parens_mode], 19);
                memcpy(local_source_buf + 19, source, length + 1); /* Include terminating NUL */
                local_length += 19;
            }
            linear_len = 16; /* Only actual linear data "0112345678901231" counts */
        }
        local_source = local_source_buf;
        local_source2 = local_source_buf;
    } else if (gs1raw_mode && !is_digital_link) {
        if (symbol->symbology == BARCODE_GS1_128 || symbol->symbology == BARCODE_DBAR_EXP
                || symbol->symbology == BARCODE_DBAR_EXPSTK || z_is_fixed_ratio(symbol->symbology)
                || symbol->symbology == BARCODE_CODABLOCKF) { /* Codablock F will be GS1-enabled in the future */
            /* Prefix caret */
            local_source_buf[0] = '^';
            if (source[0] == '\x1D') { /* Allow initial GS */
                gs1se_gs_caret_sub(source + 1, length - 1, local_source_buf + 1);
            } else {
                gs1se_gs_caret_sub(source, length, local_source_buf + 1);
                local_length++;
            }
        } else {
            /* Just copy over */
            memcpy(local_source_buf, source, length + 1); /* Include terminating NULL */
        }
        local_source = local_source_buf;
        local_source2 = local_source_buf;
    }

    if (!is_digital_link && !gs1raw_mode && !gs1_caret) {
        assert(local_length > 0);
        /* Convert to GS1 Syntax Engine parenthesis mode */
        if (!gs1parens_mode) {
            /* Replace '[' and ']' with '(' and ')' & escape any data opening parentheses (not closing parentheses) */
            int bracket_level = 0; /* Non-compliant closing square brackets may be in text */
            for (i = 0, j = 0; i < local_length; i++) {
                if (local_source[i] == '[') {
                    local_source2_buf[j++] = '(';
                    bracket_level++;
                } else if (local_source[i] == ']' && bracket_level) {
                    local_source2_buf[j++] = ')';
                    bracket_level--;
                } else {
                    if (local_source[i] == '(') {
                        local_source2_buf[j++] = '\\';
                    }
                    local_source2_buf[j++] = local_source[i];
                }
            }
            local_source2_buf[j] = '\0';
            local_source2 = local_source2_buf;
        /* Unescape any backslashed closing parentheses */
        } else if (strchr(ZCCP(local_source), '\\') != NULL) {
            for (i = 0, j = 0; i < local_length; i++) {
                if (local_source[i] == '\\' && i + 1 < local_length && local_source[i + 1] == ')') {
                    i++;
                }
                local_source2_buf[j++] = local_source[i];
            }
            local_source2_buf[j] = '\0';
            local_source2 = local_source2_buf;
        }
    }

    if (!(ctx = gs1_encoder_init_ex(NULL /*mem*/, &opts))) {
        const int error_number = status == GS1_ENCODERS_INIT_FAILED_NO_MEM
                                    ? ZINT_ERROR_MEMORY : ZINT_ERROR_ENCODING_PROBLEM;
        return z_errtxtf(error_number, symbol, 266, "Failed to initialize GS1 Syntax Engine: %.80s",
                        *opts.msgBuf ? opts.msgBuf : "unknown error");
    }
    /* Do not check for required checks for GS1-128 as may be spread across multiple barcodes - ticket #348
       and https://github.com/gs1/gs1-syntax-dictionary/issues/24 */
    if (symbol->symbology == BARCODE_GS1_128) {
        if (!gs1_encoder_setValidationEnabled(ctx, gs1_encoder_vREQUISITE_AIS, false)) {
            return gs1se_internal_errtxt(symbol, 0 /*err_id*/, ctx);
        }
    }

    if (is_digital_link || gs1raw_mode || gs1_caret) {
        /* Required for `gs1_encoder_getScanData()` */
        gs1_encoder_setSym(ctx, primary_len ? gs1_encoder_sGS1_128_CCA : gs1_encoder_sDM);
        gs1se_ret = gs1_encoder_setDataStr(ctx, ZCCP(local_source2));
    } else {
        gs1se_ret = gs1_encoder_setAIdataStr(ctx, ZCCP(local_source2));
    }
    if (!gs1se_ret) {
        const char *errmsg = gs1_encoder_getErrMsg(ctx);
        const int errmsg_len = (int) strlen(errmsg);
        const char *errmarkup = gs1_encoder_getErrMarkup(ctx);
        int errmarkup_len = (int) strlen(errmarkup);
        int errtxt_set = 0;
        if (errmsg_len && errmarkup_len && errmsg_len + 1 + errmarkup_len < ARRAY_SIZE(symbol->errtxt)) {
            char *local_errmarkup = (char *) z_alloca(errmarkup_len * 4 + 1);
            z_debug_print_escape(ZCUCP(errmarkup), errmarkup_len, local_errmarkup);
            errmarkup_len = (int) strlen(local_errmarkup);
            if (errmsg_len + 1 + errmarkup_len < ARRAY_SIZE(symbol->errtxt)) {
                ZEXT z_errtxtf(0, symbol, 267, "%1$s %2$s", errmsg, local_errmarkup);
                errtxt_set = 1;
            }
        }
        if (!errtxt_set) {
            z_errtxt(0, symbol, 268, *errmsg ? errmsg : "Unknown error using GS1 Syntax Engine");
        }
        gs1_encoder_free(ctx);
        return ZINT_ERROR_INVALID_DATA;
    }

    /* If Digital Link or raw/caret mode, set `reduced` */
    if (is_digital_link || gs1raw_mode || gs1_caret) {
        const char *scan_data;
        int reduced_length;

        if (!(scan_data = gs1_encoder_getScanData(ctx))) { /* Shouldn't happen */
            return gs1se_internal_errtxt(symbol, 269 /*err_id*/, ctx);
        }
        reduced_length = (int) strlen(scan_data);

        /* Skip over Symbology Identifier */
        if (reduced_length >= 3 && scan_data[0] == ']') {
            scan_data += 3;
            reduced_length -= 3;
        }
        /* Skip over any linear stuff */
        if (linear_len) {
            scan_data += linear_len;
            reduced_length -= linear_len;
            /* Need to check if the last linear AI was non-predefined length, when a GS is added */
            if (*scan_data == '\x1D') { /* Skip */
                scan_data++;
                reduced_length--;
            }
        }
        assert(reduced_length <= length);

        memcpy(reduced, scan_data, reduced_length + 1); /* Include terminating NUL */
        reduced[reduced_length] = '\0';
        *p_reduced_length = reduced_length;
    } else {
        *p_reduced_length = 0; /* `reduced` not set */
    }

    gs1_encoder_free(ctx);

    return 0;
}

#endif /* ZINT_HAVE_GS1SE */

/* Check for valid AI values and data lengths according to GS1 General Specifications Release 26, January 2026 */
static int gs1_run_lint(struct zint_symbol *symbol, const unsigned char source[], const int ai_count,
            int *ai_vals, int *ai_locs, int *data_locs, int *data_lens) {
    int i;
    int error_number = 0;

    for (i = 0; i < ai_count; i++) {
        int err_no, err_posn;
        char err_msg[50];
        if (!gs1_lint(ai_vals[i], source + data_locs[i], data_lens[i], &err_no, &err_posn,
                        err_msg)) {
            if (err_no == 1) {
                ZEXT z_errtxtf(0, symbol, 260, "Invalid AI (%1$02d) at position %2$d",
                                ai_vals[i], ai_locs[i] + 1);
            } else if (err_no == 2 || err_no == 4) { /* 4 is backward-incompatible bad length */
                ZEXT z_errtxtf(0, symbol, 259, "Invalid data length for AI (%1$02d) at position %2$d",
                                ai_vals[i], ai_locs[i] + 1);
            } else {
                ZEXT z_errtxtf(0, symbol, 261, "AI (%1$02d) data position %2$d: %3$s", ai_vals[i], err_posn, err_msg);
            }
            /* For backward compatibility only error on unknown AI or bad length */
            if (err_no == 1 || err_no == 2) {
                return ZINT_ERROR_INVALID_DATA;
            }
            error_number = ZINT_WARN_NONCOMPLIANT;
        }
    }
    return error_number;
}

/* Helper to convert parsed AIs to HRT */
static int gs1_hrt_conv_parsed(struct zint_symbol *symbol, const unsigned char source[], const int length,
            const int ai_count, const int *ai_vals, const int *ai_locs, const int *data_locs, const int *data_lens,
            const int no_errtxt) {
    int i, j;
    int warn_number = 0;
    const int text_size = ARRAY_SIZE(symbol->text);
    const int gs1_newline = symbol->show_hrt & ZINT_HRT_GS1_NEWLINE;
    int first_ai = 1;

#ifdef NDEBUG
    (void)length;
#endif

    for (i = 0, j = 0; i < ai_count && j < text_size; i++) {
        const int ai_len = 2 + (ai_vals[i] >= 100) + (ai_vals[i] >= 1000);
        if (j + 1 + ai_len + 1 + data_lens[i] + (!first_ai && gs1_newline) >= text_size) {
            warn_number = ZINT_WARN_HRT_TRUNCATED;
            break;
        }
        if (!first_ai && gs1_newline) {
            symbol->text[j++] = '\n';
        }
        symbol->text[j++] = '(';
        memcpy(symbol->text + j, source + ai_locs[i], ai_len);
        j += ai_len;
        symbol->text[j++] = ')';
        assert(data_locs[i] + data_lens[i] <= length);
        memcpy(symbol->text + j, source + data_locs[i], data_lens[i]);
        j += data_lens[i];
        first_ai = 0;
    }

    if (j >= text_size) {
        warn_number = ZINT_WARN_HRT_TRUNCATED;
        j = text_size - 1;
    }
    symbol->text_length = j;
    symbol->text[j] = '\0';

    if (warn_number && !no_errtxt) {
        z_errtxt(0, symbol, 799, "Human Readable Text truncated");
    }
    return warn_number;

}

#ifdef ZINT_TEST /* Wrapper for direct testing */
INTERNAL int zint_test_gs1_hrt_conv_parsed(struct zint_symbol *symbol, const unsigned char source[], const int length,
            const int ai_count, const int *ai_vals, const int *ai_locs, const int *data_locs, const int *data_lens,
            const int no_errtxt) {
    return gs1_hrt_conv_parsed(symbol, source, length, ai_count, ai_vals, ai_locs, data_locs, data_lens,
                no_errtxt);
}
#endif

/* Helper to set HRT when have GS1PARENS_MODE - truncates if too long for HRT buffer */
static int gs1_hrt_cpy(struct zint_symbol *symbol, const unsigned char source[], const int length,
            const int no_errtxt) {
    int warn_number = 0;
    const int text_size = ARRAY_SIZE(symbol->text);
    const int gs1_newline = symbol->show_hrt & ZINT_HRT_GS1_NEWLINE;

    assert(symbol->input_mode & GS1PARENS_MODE);
    assert(source[0] == '(');

    if (gs1_newline) {
        int i, j = 1, last_opening_bracket = 0;
        assert(source[0] == '(');
        symbol->text[0] = '(';
        for (i = 1; i < length && j < text_size; i++) {
            if (source[i] == '\\' && i + 1 < length && (source[i + 1] == '\\' || source[i + 1] == '(')) {
                if (j + 1 >= text_size) {
                    break;
                }
                symbol->text[j++] = source[i++];
            } else if (source[i] == '(') {
                if (j + 1 >= text_size) {
                    break;
                }
                last_opening_bracket = j;
                symbol->text[j++] = '\n';
            }
            symbol->text[j++] = source[i];
        }
        if (i < length || j >= text_size) {
            if (j < text_size && source[i] == '(') {
                /* Finished at end of AI data */
                symbol->text_length = j;
                symbol->text[j] = '\0';
            } else {
                /* Use last full AI + data */
                symbol->text_length = last_opening_bracket;
                symbol->text[last_opening_bracket] = '\0';
            }
            warn_number = ZINT_WARN_HRT_TRUNCATED;
        } else {
            symbol->text_length = j;
            symbol->text[j] = '\0';
        }
    } else if (length < text_size) {
        memcpy(symbol->text, source, length + 1); /* Include terminating NUL */
        symbol->text_length = length;
    } else {
        /* Find last full AI - need to scan forward to track escaped opening parens */
        int i, last_opening_bracket = 0;
        for (i = 0; i < text_size; i++) {
            if (source[i] == '\\' && i + 1 < length && (source[i + 1] == '\\' || source[i + 1] == '(')) {
                i++;
            } else if (source[i] == '(') {
                last_opening_bracket = i;
            }
        }
        if (text_size < length && source[text_size] == '(') {
            /* Finished at end of AI data */
            memcpy(symbol->text, source, text_size - 1);
            symbol->text_length = text_size - 1;
            symbol->text[text_size - 1] = '\0';
        } else {
            /* Use last full AI + data */
            memcpy(symbol->text, source, last_opening_bracket);
            symbol->text_length = last_opening_bracket;
            symbol->text[last_opening_bracket] = '\0';
        }
        warn_number = ZINT_WARN_HRT_TRUNCATED;
    }
    if (warn_number && !no_errtxt) {
        z_errtxt(0, symbol, 801, "Human Readable Text truncated");
    }
    return warn_number;
}

#ifdef ZINT_TEST /* Wrapper for direct testing */
INTERNAL int zint_test_gs1_hrt_cpy(struct zint_symbol *symbol, const unsigned char source[], const int length,
            const int no_errtxt) {
    return gs1_hrt_cpy(symbol, source, length, no_errtxt);
}
#endif

/* Helper to set HRT when don't have GS1PARENS_MODE - substitutes square brackets with parentheses
   & truncates if too long for HRT buffer */
static int gs1_hrt_conv_brackets(struct zint_symbol *symbol, const unsigned char source[], const int length,
            const int no_errtxt) {
    int i, j;
    int warn_number = 0;
    int bracket_level = 0; /* Non-compliant closing square brackets may be in text */
    int last_opening_bracket = 0;
    const int text_size = ARRAY_SIZE(symbol->text);
    const int max_len = length > text_size ? text_size : length;
    const int gs1_newline = symbol->show_hrt & ZINT_HRT_GS1_NEWLINE;
    int first_ai = 1;

    assert((symbol->input_mode & GS1PARENS_MODE) == 0);
    assert(source[0] == '[');

    for (i = 0, j = 0; i < max_len && j < text_size; i++) {
        if (source[i] == '[') {
            const int do_gs1_newline = !first_ai && gs1_newline;
            if (j + 1 + do_gs1_newline >= text_size) {
                warn_number = ZINT_WARN_HRT_TRUNCATED;
                break;
            }
            if (do_gs1_newline) {
                symbol->text[j++] = '\n';
            }
            symbol->text[j++] = '(';
            bracket_level++;
            last_opening_bracket = j - (1 + do_gs1_newline);
        } else if (source[i] == ']' && bracket_level) {
            symbol->text[j++] = ')';
            bracket_level--;
        } else {
            symbol->text[j++] = source[i];
        }
        first_ai = 0;
    }
    if (j == text_size) {
        j--;
        if (source[i] != '[') {
            j = last_opening_bracket;
        }
        warn_number = ZINT_WARN_HRT_TRUNCATED;
    }
    symbol->text_length = j;
    symbol->text[j] = '\0';

    if (warn_number && !no_errtxt) {
        z_errtxt(0, symbol, 798, "Human Readable Text truncated");
    }
    return warn_number;
}

#ifdef ZINT_TEST /* Wrapper for direct testing */
INTERNAL int zint_test_gs1_hrt_conv_brackets(struct zint_symbol *symbol, const unsigned char source[],
                const int length, const int no_errtxt) {
    return gs1_hrt_conv_brackets(symbol, source, length, no_errtxt);
}
#endif

#define GS1_PARENS_PLACEHOLDER_MASK 0x20  /* Mask `(` or `)` by this if escaped (get BS (\x08) or HT (\x09)) */

/* Helper to re-instate escaped parentheses */
static void gs1_reinstate_escaped_parens(unsigned char source[], const int length) {
    int i;
    for (i = 0; i < length; i++) {
        if (source[i] < '\x1D') {
            source[i] |= GS1_PARENS_PLACEHOLDER_MASK;
        }
    }
}

/* Verify a GS1 input string */
INTERNAL int zint_gs1_verify(struct zint_symbol *symbol, const unsigned char source[], const int length,
                unsigned char reduced[], int *p_reduced_length, const int set_hrt) {
    int i, j;
    int error_number = 0, warn_number = 0;
    int bracket_level = 0;
    int ai_latch;
    int local_length = length;
    const unsigned char *local_source = source;
    unsigned char *local_source_buf = (unsigned char *) z_alloca(length + 1);
    int *ai_vals, *ai_locs, *data_locs, *data_lens;

    const int gs1parens_mode = symbol->input_mode & GS1PARENS_MODE;
    const char obracket = gs1parens_mode ? '(' : '[';
    const char cbracket = gs1parens_mode ? ')' : ']';
    int done_gs1se = 0;
    int is_digital_link = 0; /* Is Digital Link? */
    const int gs1raw_mode = symbol->input_mode & GS1RAW_MODE;
    const int gs1nocheck_mode = symbol->input_mode & GS1NOCHECK_MODE;

    /* Note: Digital Link URIs only validated if have GS1 Syntax Engine and GS1SYNTAXENGINE_MODE set */

    *p_reduced_length = 0;

#ifdef ZINT_HAVE_GS1SE
    if ((symbol->input_mode & GS1SYNTAXENGINE_MODE) && !gs1nocheck_mode) {
        /* Strict verification */
        if ((error_number = gs1se_verify(symbol, source, length, reduced, p_reduced_length))) {
            return error_number;
        }
        done_gs1se = 1;
        /* `p_reduced_length` will be set if Digital Link or raw/caret */
    }
#endif

    if (!done_gs1se) {
        /* Detect control and extended ASCII characters */
        for (i = 0; i < length; i++) {
            if (!z_isascii(source[i])) {
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 250,
                                "Extended ASCII characters are not supported by GS1");
            }
            if (source[i] == '\0') {
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 262, "NUL characters not permitted in GS1 mode");
            }
            if (source[i] < 32 && (!gs1raw_mode || source[i] != '\x1D')) {
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 251, "Control characters are not supported by GS1");
            }
            if (source[i] == 127) {
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 263, "DEL characters are not supported by GS1");
            }
        }

        is_digital_link = gs1_is_digital_link(source, length);

        if (source[0] != obracket && !is_digital_link && (gs1raw_mode || source[0] == '^')) {
            int ai_count;
            int err_no, err_posn;
            const int ai_max = (length + 2) / 3 + 1; /* Min AI 2, min data 1 */

            ai_vals = (int *) z_alloca(sizeof(int) * ai_max);
            ai_locs = (int *) z_alloca(sizeof(int) * ai_max);
            data_locs = (int *) z_alloca(sizeof(int) * ai_max);
            data_lens = (int *) z_alloca(sizeof(int) * ai_max);
            if (!gs1_lint_parse_raw_caret(source, length, ai_max, ai_vals, ai_locs, data_locs, data_lens, &ai_count,
                    &err_no, &err_posn)) {
                /* Both raw & caret modes require valid AIs and underlong data lengths to work, so check regardless of
                   GS1NOCHECK_MODE */
                if (err_no == 1) {
                    if (ai_vals[ai_count] != -1) {
                        ZEXT z_errtxtf(0, symbol, 856, "Invalid AI (%1$02d) at position %2$d", ai_vals[ai_count],
                                        err_posn);
                    } else {
                        z_errtxtf(0, symbol, 857, "Invalid AI at position %d", err_posn);
                    }
                } else {
                    assert(err_no == 2);
                    if (data_lens[ai_count] == 0) {
                        ZEXT z_errtxtf(0, symbol, 858, "Empty data field for AI (%1$02d) at position %2$d",
                                        ai_vals[ai_count], err_posn);
                    } else {
                        ZEXT z_errtxtf(0, symbol, 859, "Invalid data length for AI (%1$02d) at position %2$d",
                                        ai_vals[ai_count], err_posn);
                    }
                }
                return ZINT_ERROR_INVALID_DATA;
            }
            assert(ai_count < ai_max); /* Suppress clang-tidy-22 clang-analyzer-security.ArrayBound */
            if (!gs1nocheck_mode) {
                /* Do lint to check that data values are ok */
                error_number = gs1_run_lint(symbol, source, ai_count, ai_vals, ai_locs, data_locs, data_lens);
                if (error_number >= ZINT_ERROR) {
                    return error_number;
                }
            }

            if (set_hrt) {
                warn_number = gs1_hrt_conv_parsed(symbol, source, length, ai_count, ai_vals, ai_locs, data_locs,
                                data_lens, error_number != 0 /*no_errtxt*/);
            }

            /* Set reduced - need to set it via `ai_vals` etc. so as to lose any superfluous carets/GSs */
            for (i = 0, j = 0; i < ai_count; i++) {
                const int ai_len = 2 + (ai_vals[i] >= 100) + (ai_vals[i] >= 1000);
                memcpy(reduced + j, source + ai_locs[i], ai_len);
                j += ai_len;
                memcpy(reduced + j, source + data_locs[i], data_lens[i]);
                j += data_lens[i];
                if (i + 1 != ai_count && !gs1_predefined_len(source + ai_locs[i])) {
                    reduced[j++] = '\x1D';
                }
            }
            reduced[j] = '\0';
            *p_reduced_length = j;
            return error_number ? error_number : warn_number;
        }
        if (source[0] != obracket) {
            if (!z_is_fixed_ratio(symbol->symbology)) { /* Only matrix symbols can encode Digital Link URIs */
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 252, "Data does not start with an AI");
            }
            if (!is_digital_link) {
                return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 855,
                                "Data does not start with an AI or Digital Link URI");
            }
        }
    }

    if (is_digital_link || (done_gs1se && *p_reduced_length)) {
        if (!done_gs1se) {
            /* Just copy over Digital Link URI - no verification */
            memcpy(reduced, source, length + 1); /* Include terminating NUL */
            *p_reduced_length = length;
            if (set_hrt) {
                error_number = z_hrt_cpy_iso8859_1(symbol, source, length);
            }
        }
        return error_number;
    }

    if (gs1parens_mode) {
        /* Check for escaped parentheses */
        for (i = 0; i < length; i++) {
            if (source[i] == '\\' && i + 1 < length && (source[i + 1] == '(' || source[i + 1] == ')')) {
                break;
            }
        }
        if (i != length) {
            assert(length > 0); /* Suppress clang-tidy-21 clang-analyzer-security.ArrayBound */
            local_source = local_source_buf;
            /* Replace with control-char placeholders */
            for (i = 0, j = 0; i < length; i++) {
                if (source[i] == '\\' && i + 1 < length && (source[i + 1] == '(' || source[i + 1] == ')')) {
                    local_source_buf[j++] = source[++i] & ~GS1_PARENS_PLACEHOLDER_MASK; /* BS (\x08) or HT (\x09) */
                } else {
                    local_source_buf[j++] = source[i];
                }
            }
            local_source_buf[j] = '\0';
            local_length = j;
        }
    }

    if (!done_gs1se) {
        /* Verify AIs */
        int max_ai_length = 0, min_ai_length = 5;
        int max_bracket_level = 0;
        int ai_length = 0;
        int max_ai_pos = 0, min_ai_pos = 0; /* Suppress gcc 14 "-Wmaybe-uninitialized" false positives */
        int ai_zero_len_no_data = 0, ai_single_digit = 0, ai_nonnumeric = 0;
        int ai_nonnumeric_pos = 0; /* Suppress gcc 14 "-Wmaybe-uninitialized" false positive */
        const int ai_max = z_chr_cnt(local_source, local_length, obracket) + 1; /* Plus 1 so non-zero */

        ai_vals = (int *) z_alloca(sizeof(int) * ai_max);
        ai_locs = (int *) z_alloca(sizeof(int) * ai_max);
        data_locs = (int *) z_alloca(sizeof(int) * ai_max);
        data_lens = (int *) z_alloca(sizeof(int) * ai_max);

        /* Check the balance of the brackets & AI lengths */
        ai_latch = 0;
        for (i = 0; i < local_length; i++) {
            if (local_source[i] == obracket) {
                bracket_level++;
                if (bracket_level > max_bracket_level) {
                    max_bracket_level = bracket_level;
                }
                ai_latch = 1;
            } else if (local_source[i] == cbracket && bracket_level) {
                bracket_level--;
                if (ai_length > max_ai_length) {
                    max_ai_length = ai_length;
                    max_ai_pos = i - ai_length;
                }
                if (ai_length < min_ai_length) {
                    min_ai_length = ai_length;
                    min_ai_pos = i - ai_length;
                }
                /* Check zero-length AI has data */
                if (ai_length == 0 && (i + 1 == local_length || local_source[i + 1] == obracket)) {
                    ai_zero_len_no_data = 1;
                } else if (ai_length == 1) {
                    ai_single_digit = 1;
                }
                ai_length = 0;
                ai_latch = 0;
            } else if (ai_latch) {
                ai_length++;
                if (!z_isdigit(local_source[i])) {
                    ai_nonnumeric = 1;
                    ai_nonnumeric_pos = i - ai_length + 1;
                }
            }
        }

        if (bracket_level != 0) {
            /* Not all brackets are closed */
            return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 253, "Malformed AI in input (brackets don\'t match)");
        }

        if (max_bracket_level > 1) {
            /* Nested brackets */
            return z_errtxt(ZINT_ERROR_INVALID_DATA, symbol, 254, "Found nested brackets in input");
        }

        if (max_ai_length > 4) {
            /* AI is too long */
            return z_errtxtf(ZINT_ERROR_INVALID_DATA, symbol, 255,
                            "Invalid AI at position %d in input (AI too long)", max_ai_pos);
        }

        if (min_ai_length <= 1) {
            /* Allow too short AI if GS1NOCHECK_MODE and no single-digit AIs and all zero-length AIs have some data
               - permits dummy "[]" workaround for ticket #204 data with no valid AI */
            if (!gs1nocheck_mode || ai_single_digit || ai_zero_len_no_data) {
                /* AI is too short */
                return z_errtxtf(ZINT_ERROR_INVALID_DATA, symbol, 256,
                                "Invalid AI at position %d in input (AI too short)", min_ai_pos);
            }
        }

        if (ai_nonnumeric) {
            /* Non-numeric data in AI */
            return z_errtxtf(ZINT_ERROR_INVALID_DATA, symbol, 257,
                            "Invalid AI at position %d in input (non-numeric characters in AI)", ai_nonnumeric_pos);
        }

        if (!gs1nocheck_mode) {
            const unsigned char *local_source2 = local_source;
            unsigned char *local_source2_buf = (unsigned char *) z_alloca(local_length + 1);
            int ai_count = 0;
            for (i = 1; i < local_length; i++) {
                if (local_source[i - 1] == obracket) {
                    ai_locs[ai_count] = i - 1;
                    for (j = 1; local_source[i + j] != cbracket; j++);
                    ai_vals[ai_count] = z_to_int(local_source + i, j);
                    ai_count++;
                    i += j;
                }
            }

            for (i = 0; i < ai_count; i++) {
                data_locs[i] = ai_locs[i] + 2 /*brackets*/ + 2 + (ai_vals[i] >= 100) + (ai_vals[i] >= 1000);
                data_lens[i] = 0;
                while (data_locs[i] + data_lens[i] < local_length
                        && local_source[data_locs[i] + data_lens[i]] != obracket) {
                    data_lens[i]++;
                }
                if (data_lens[i] == 0) {
                    /* No data for given AI */
                    return ZEXT z_errtxtf(ZINT_ERROR_INVALID_DATA, symbol, 258,
                                            "Empty data field for AI (%1$02d) at position %2$d",
                                            ai_vals[i], ai_locs[i] + 1);
                }
            }

            if (local_length != length) {
                /* Temporarily re-instate escaped parentheses before linting */
                local_source2 = local_source2_buf;
                memcpy(local_source2_buf, local_source, local_length + 1); /* Include terminating NUL */
                gs1_reinstate_escaped_parens(local_source2_buf, local_length);
            }

            /* Check AI/data */
            error_number = gs1_run_lint(symbol, local_source2, ai_count, ai_vals, ai_locs, data_locs, data_lens);
            if (error_number >= ZINT_ERROR) {
                return error_number;
            }
        }
    }

    if (!*p_reduced_length) {
        /* Resolve AI data - put resulting string in `reduced` */
        j = 0;
        ai_latch = 1;
        for (i = 0; i < local_length; i++) {
            if (local_source[i] == obracket) {
                bracket_level++;
                /* Start of an AI string */
                if (ai_latch == 0) {
                    reduced[j++] = '\x1D';
                }
                if (i + 1 != local_length) {
                    ai_latch = gs1_predefined_len(local_source + i + 1);
                }
            } else if (local_source[i] == cbracket && bracket_level) {
                bracket_level--;
            } else {
                reduced[j++] = local_source[i];
            }
        }
        reduced[j] = '\0';
        *p_reduced_length = j;

        if (local_length != length) {
            gs1_reinstate_escaped_parens(reduced, *p_reduced_length);
        }
        if (set_hrt) {
            if (gs1parens_mode) {
                warn_number = gs1_hrt_cpy(symbol, source, length, error_number != 0 /*no_errtxt*/);
            } else {
                warn_number = gs1_hrt_conv_brackets(symbol, source, length, error_number != 0 /*no_errtxt*/);
            }
        }
    }
    /* The character '\x1D' (GS) in the reduced string refers to the FNC1 character */

    return error_number ? error_number : warn_number;
}

/* Helper to return standard GS1 check digit (GS1 General Specifications 7.9.1) */
INTERNAL char zint_gs1_check_digit(const unsigned char source[], const int length) {
    int i;
    int count = 0;
    int factor = length & 1 ? 3 : 1;

    for (i = 0; i < length; i++) {
        count += factor * z_ctoi(source[i]);
        factor ^= 0x02; /* Toggles 1 and 3 */
    }

    return z_itoc((10 - (count % 10)) % 10);
}

/* Helper to expose `iso3166_alpha2()` */
INTERNAL int zint_gs1_iso3166_alpha2(const unsigned char *cc) {
    return iso3166_alpha2((const char *) cc);
}

/* vim: set ts=4 sw=4 et : */
