STKouyuEngine C core line coverage

Total 98.39% (1652 of 1679 lines)

filecoveredlinespercent
Sources/STKouyuEngine/core/ygst_audio.c16716899.40%
Sources/STKouyuEngine/core/ygst_buf.c919793.81%
Sources/STKouyuEngine/core/ygst_crypto.c140140100.00%
Sources/STKouyuEngine/core/ygst_envelope.c212212100.00%
Sources/STKouyuEngine/core/ygst_errors_table.c4040100.00%
Sources/STKouyuEngine/core/ygst_fields.c14114994.63%
Sources/STKouyuEngine/core/ygst_json.c34034897.70%
Sources/STKouyuEngine/core/ygst_multipart.c6969100.00%
Sources/STKouyuEngine/core/ygst_params.c13313797.08%
Sources/STKouyuEngine/core/ygst_retry.c319319100.00%

Sources/STKouyuEngine/core/ygst_audio.c 99.40%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * WAV header writer and reader, local audio checks, a frame energy VAD with an adaptive noise
6  * floor, the 0..100 sound intensity scale and the countdown values of the tick callback.
7  */
8 #include "ygst_core.h"
9 
10 #include <math.h>
11 #include <string.h>
12 
13 55static void put_le32(unsigned char *p, uint32_t v) {
14 55    p[0] = (unsigned char)v;
15 55    p[1] = (unsigned char)(v >> 8);
16 55    p[2] = (unsigned char)(v >> 16);
17 55    p[3] = (unsigned char)(v >> 24);
18 55}
19 
20 44static void put_le16(unsigned char *p, uint16_t v) {
21 44    p[0] = (unsigned char)v;
22 44    p[1] = (unsigned char)(v >> 8);
23 44}
24 
25 210static uint32_t le32(const unsigned char *p) {
26 210    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
27 }
28 
29 162static uint16_t le16(const unsigned char *p) {
30 162    return (uint16_t)(p[0] | (p[1] << 8));
31 }
32 
33 11void ygst_wav_header(unsigned char out[44], uint32_t data_len, int sample_rate, int channels, int bits) {
34 11    uint32_t block = (uint32_t)(channels * (bits / 8));
35 11    memcpy(out, "RIFF", 4);
36 11    put_le32(out + 4, data_len > 0xFFFFFFFFu - 36u ? 0xFFFFFFFFu : data_len + 36u);
37 11    memcpy(out + 8, "WAVE", 4);
38 11    memcpy(out + 12, "fmt ", 4);
39 11    put_le32(out + 16, 16);
40 11    put_le16(out + 20, 1);
41 11    put_le16(out + 22, (uint16_t)channels);
42 11    put_le32(out + 24, (uint32_t)sample_rate);
43 11    put_le32(out + 28, (uint32_t)sample_rate * block);
44 11    put_le16(out + 32, (uint16_t)block);
45 11    put_le16(out + 34, (uint16_t)bits);
46 11    memcpy(out + 36, "data", 4);
47 11    put_le32(out + 40, data_len);
48 11}
49 
50 63int ygst_is_riff_wave(const unsigned char *p, size_t n) {
51 63    return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0;
52 }
53 
54 61int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) {
55 61    size_t pos = 12;
56 61    int have_fmt = 0;
57 61    memset(info, 0, sizeof *info);
58 61    if (!ygst_is_riff_wave(p, n)) return -1;
59 156    while (pos + 8 <= n) {
60 156        uint32_t size = le32(p + pos + 4);
61 156        const unsigned char *id = p + pos;
62 156        size_t body = pos + 8;
63 156        if (memcmp(id, "fmt ", 4) == 0) {
64 55            if (size < 16 || body + 16 > n) return -1;
65 54            info->format = le16(p + body);
66 54            info->channels = le16(p + body + 2);
67 54            info->sample_rate = (int)le32(p + body + 4);
68 54            info->bits = le16(p + body + 14);
69 54            have_fmt = 1;
70 101        } else if (memcmp(id, "data", 4) == 0) {
71 55            if (!have_fmt) return -1;
72 54            info->data_offset = body;
73             /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */
74 54            info->data_len = (size == 0 || size > n - body) ? n - body : size;
75 54            return 0;
76         }
77 100        if (size > n - body) return -1;
78 99        pos = body + size + (size & 1u);
79     }
80 0    return -1;
81 }
82 
83 57long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) {
84     double per_second;
85 57    if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1;
86 55    per_second = (double)sample_rate * channels * (bits / 8.0);
87 55    if (per_second <= 0) return -1;
88 55    return (long)((double)bytes * 1000.0 / per_second);
89 }
90 
91 /* Exact duration test without rounding: bytes * 1000 compared with ms * bytes per second. */
92 46static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) {
93 46    double per_second = (double)sample_rate * channels * (bits / 8.0);
94 46    if (per_second <= 0) return 0;
95 45    return (double)bytes * 1000.0 > (double)ms * per_second;
96 }
97 
98 9int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) {
99     long ms;
100 9    if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY;
101 8    if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE;
102 7    ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits);
103 7    if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
104 6    if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE;
105 4    return 0;
106 }
107 
108 49int ygst_audio_check_upload(const unsigned char *data, size_t len) {
109     ygst_wav_info info;
110 49    if (!data || len == 0) return YGST_ERRID_AUDIO_EMPTY;
111 47    if (len > YGST_MAX_AUDIO_BYTES) return YGST_ERRID_AUDIO_TOO_LARGE;
112 46    if (ygst_wav_parse(data, len, &info) == 0) {
113         long ms;
114 44        if (info.data_len == 0) return YGST_ERRID_AUDIO_EMPTY;
115 42        if (info.format == 1 || info.format == 3 || info.format == 0xFFFE) {
116 42            ms = ygst_pcm_duration_ms(info.data_len, info.sample_rate, info.channels, info.bits);
117 42            if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
118 40            if (pcm_longer_than(info.data_len, info.sample_rate, info.channels, info.bits, YGST_MAX_AUDIO_MS))
119 1                return YGST_ERRID_AUDIO_TOO_LARGE;
120         }
121     }
122 41    return 0;
123 }
124 
125 /* ------------------------------------------------------------------ VAD */
126 
127 #define VAD_ABS_DB (-42.0)      /* a frame quieter than this is never speech */
128 #define VAD_OVER_NOISE_DB 10.0 /* speech must also exceed the noise floor by this much */
129 #define VAD_START_FRAMES 3     /* 30 ms of speech starts an utterance */
130 #define VAD_NOISE_MIN_DB (-90.0)
131 #define VAD_NOISE_MAX_DB (-35.0)
132 
133 10void ygst_vad_init(ygst_vad *v, int sample_rate, double seek_10ms, double ref_length_10ms) {
134 10    memset(v, 0, sizeof *v);
135 10    v->sample_rate = sample_rate > 0 ? sample_rate : 16000;
136 10    v->frame_samples = v->sample_rate / 100;
137 10    if (v->frame_samples <= 0) v->frame_samples = 1;
138 10    v->seek_ms = (int)((seek_10ms > 0 ? seek_10ms : 60) * 10.0);
139 10    v->ref_length_ms = ref_length_10ms > 0 ? (int)(ref_length_10ms * 10.0) : 0;
140 10    v->noise_db = -60.0;
141 10}
142 
143 1341int ygst_sound_intensity_db(double db) {
144 1341    double x = (db + 60.0) * 100.0 / 60.0;
145 1341    if (x < 0) x = 0;
146 1341    if (x > 100) x = 100;
147 1341    return (int)(x + 0.5);
148 }
149 
150 1338static double rms_db(double sumsq, int n) {
151     double rms;
152 1338    if (n <= 0) return -120.0;
153 1337    rms = sqrt(sumsq / n);
154 1337    if (rms < 1.0) return -120.0;
155 651    return 20.0 * log10(rms / 32768.0);
156 }
157 
158 3int ygst_sound_intensity(const int16_t *pcm, size_t samples) {
159 3    double sumsq = 0;
160     size_t i;
161 3203    for (i = 0; i < samples; i++) sumsq += (double)pcm[i] * pcm[i];
162 3    return ygst_sound_intensity_db(rms_db(sumsq, (int)samples));
163 }
164 
165 1335static void vad_frame(ygst_vad *v, double db) {
166 1335    double threshold = v->noise_db + VAD_OVER_NOISE_DB;
167     int speech;
168 1335    if (threshold < VAD_ABS_DB) threshold = VAD_ABS_DB;
169 1335    speech = db > threshold;
170 1335    v->intensity = ygst_sound_intensity_db(db);
171 1335    if (!speech) {
172 927        v->noise_db = v->noise_db * 0.95 + db * 0.05;
173 927        if (v->noise_db < VAD_NOISE_MIN_DB) v->noise_db = VAD_NOISE_MIN_DB;
174 927        if (v->noise_db > VAD_NOISE_MAX_DB) v->noise_db = VAD_NOISE_MAX_DB;
175     }
176 1335    v->speech_run = speech ? v->speech_run + 1 : 0;
177 1335    if (v->status != 1) {
178 644        if (v->speech_run >= VAD_START_FRAMES) {
179 7            v->status = 1;
180 7            v->speaking_ms = 0;
181 7            v->silence_ms = 0;
182         }
183 644        return;
184     }
185 691    v->speaking_ms += 10;
186 691    v->silence_ms = speech ? 0 : v->silence_ms + 10;
187 691    if (v->silence_ms >= v->seek_ms && v->speaking_ms >= v->ref_length_ms) {
188 4        v->status = 2;
189 4        v->ended_edge = 1;
190     }
191 }
192 
193 757int ygst_vad_process(ygst_vad *v, const int16_t *pcm, size_t samples) {
194     size_t i;
195 757    v->ended_edge = 0;
196 214357    for (i = 0; i < samples; i++) {
197 213600        v->acc_sumsq += (double)pcm[i] * pcm[i];
198 213600        v->acc_n++;
199 213600        if (v->acc_n >= v->frame_samples) {
200 1335            vad_frame(v, rms_db(v->acc_sumsq, v->acc_n));
201 1335            v->acc_sumsq = 0;
202 1335            v->acc_n = 0;
203         }
204     }
205 757    return v->status;
206 }
207 
208 131int ygst_vad_process_bytes(ygst_vad *v, const unsigned char *bytes, size_t n) {
209     int16_t chunk[256];
210 131    size_t used = 0;
211 131    int ended = 0;
212 131    size_t i = 0;
213 131    if (v->has_carry && n > 0) {
214 20        chunk[used++] = (int16_t)(uint16_t)(v->carry | (bytes[0] << 8));
215 20        v->has_carry = 0;
216 20        i = 1;
217     }
218 150831    for (; i + 1 < n; i += 2) {
219 150700        chunk[used++] = (int16_t)(uint16_t)(bytes[i] | (bytes[i + 1] << 8));
220 150700        if (used == sizeof chunk / sizeof chunk[0]) {
221 542            ygst_vad_process(v, chunk, used);
222 542            ended |= v->ended_edge;
223 542            used = 0;
224         }
225     }
226 131    if (i < n) {
227 20        v->carry = bytes[i];
228 20        v->has_carry = 1;
229     }
230 131    if (used) {
231 130        ygst_vad_process(v, chunk, used);
232 130        ended |= v->ended_edge;
233     }
234 131    v->ended_edge = ended;
235 131    return v->status;
236 }
237 
238 4void ygst_tick_compute(double duration_ms, double elapsed_ms, double *millis_left, double *percent_left) {
239     double left;
240 4    if (duration_ms <= 0) {
241 1        *millis_left = 0;
242 1        *percent_left = 0;
243 1        return;
244     }
245 3    left = duration_ms - (elapsed_ms > 0 ? elapsed_ms : 0);
246 3    if (left < 0) left = 0;
247 3    *millis_left = left;
248 3    *percent_left = left * 100.0 / duration_ms;
249 }

Sources/STKouyuEngine/core/ygst_buf.c 93.81%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Growable byte buffer and small string helpers.
6  */
7 #include "ygst_core.h"
8 
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 
13 2544void ygst_buf_init(ygst_buf *b) {
14 2544    b->data = NULL;
15 2544    b->len = 0;
16 2544    b->cap = 0;
17 2544    b->oom = 0;
18 2544}
19 
20 1272void ygst_buf_free(ygst_buf *b) {
21 1272    free(b->data);
22 1272    ygst_buf_init(b);
23 1272}
24 
25 1049void ygst_buf_reset(ygst_buf *b) {
26 1049    b->len = 0;
27 1049    b->oom = 0;
28 1049    if (b->data) b->data[0] = 0;
29 1049}
30 
31 6549int ygst_buf_reserve(ygst_buf *b, size_t extra) {
32     size_t need;
33     size_t cap;
34     unsigned char *p;
35 6549    if (b->oom) return -1;
36 6549    if (extra > (size_t)-1 - b->len - 1) {
37 0        b->oom = 1;
38 0        return -1;
39     }
40 6549    need = b->len + extra + 1; /* room for the terminating NUL */
41 6549    if (need <= b->cap) return 0;
42 1297    cap = b->cap ? b->cap : 64;
43 2383    while (cap < need) {
44 1086        if (cap > ((size_t)-1) / 2) {
45 0            cap = need;
46 0            break;
47         }
48 1086        cap *= 2;
49     }
50 1297    p = (unsigned char *)realloc(b->data, cap);
51 1297    if (!p) {
52 0        b->oom = 1;
53 0        return -1;
54     }
55 1297    b->data = p;
56 1297    b->cap = cap;
57 1297    return 0;
58 }
59 
60 6483int ygst_buf_append(ygst_buf *b, const void *p, size_t n) {
61 6483    if (n == 0) return b->oom ? -1 : 0;
62 6480    if (ygst_buf_reserve(b, n) != 0) return -1;
63 6480    memcpy(b->data + b->len, p, n);
64 6480    b->len += n;
65 6480    b->data[b->len] = 0;
66 6480    return 0;
67 }
68 
69 1750int ygst_buf_append_str(ygst_buf *b, const char *s) {
70 1750    return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
71 }
72 
73 2354int ygst_buf_append_char(ygst_buf *b, char c) {
74 2354    return ygst_buf_append(b, &c, 1);
75 }
76 
77 39int ygst_buf_append_int(ygst_buf *b, long long v) {
78     char tmp[32];
79 39    int n = snprintf(tmp, sizeof tmp, "%lld", v);
80 39    if (n < 0) return -1;
81 39    return ygst_buf_append(b, tmp, (size_t)n);
82 }
83 
84 2400const char *ygst_buf_cstr(ygst_buf *b) {
85 2400    if (!b->data) {
86 69        if (ygst_buf_reserve(b, 0) != 0) return "";
87 69        b->data[0] = 0;
88     }
89 2400    return (const char *)b->data;
90 }
91 
92 511char *ygst_strdup(const char *s) {
93     size_t n;
94     char *p;
95 511    if (!s) return NULL;
96 510    n = strlen(s);
97 510    p = (char *)malloc(n + 1);
98 510    if (!p) return NULL;
99 510    memcpy(p, s, n + 1);
100 510    return p;
101 }
102 
103 816static int lower_ascii(int c) {
104 816    return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
105 }
106 
107 213int ygst_strcaseeq(const char *a, const char *b) {
108 213    if (!a || !b) return 0;
109 497    while (*a && *b) {
110 408        if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0;
111 285        a++;
112 285        b++;
113     }
114 89    return *a == 0 && *b == 0;
115 }
116 
117 11int ygst_has_suffix_ci(const char *s, const char *suffix) {
118     size_t n;
119     size_t m;
120 11    if (!s || !suffix) return 0;
121 10    n = strlen(s);
122 10    m = strlen(suffix);
123 10    if (m > n) return 0;
124 9    return ygst_strcaseeq(s + (n - m), suffix);
125 }
126 
127 384static int is_space(unsigned char c) {
128 384    return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
129 }
130 
131 121size_t ygst_trim_copy(const char *s, char *out, size_t cap) {
132     const char *e;
133     size_t n;
134 121    if (!out || cap == 0) return 0;
135 121    out[0] = 0;
136 121    if (!s) return 0;
137 126    while (*s && is_space((unsigned char)*s)) s++;
138 119    e = s + strlen(s);
139 124    while (e > s && is_space((unsigned char)e[-1])) e--;
140 119    n = (size_t)(e - s);
141 119    if (n >= cap) n = cap - 1;
142 119    memcpy(out, s, n);
143 119    out[n] = 0;
144 119    return n;
145 }
146 
147 148int ygst_is_blank(const char *s) {
148 148    if (!s) return 1;
149 149    while (*s) {
150 138        if (!is_space((unsigned char)*s)) return 0;
151 10        s++;
152     }
153 11    return 1;
154 }

Sources/STKouyuEngine/core/ygst_crypto.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 2104), Base64 (RFC 4648). Written from the
6  * specifications so that the core has no dependency on CommonCrypto or OpenSSL.
7  */
8 #include "ygst_core.h"
9 
10 #include <string.h>
11 
12 static const uint32_t K[64] = {
13     0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
14     0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
15     0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
16     0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
17     0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
18     0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
19     0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
20     0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u};
21 
22 #define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
23 
24 15918static void sha256_block(ygst_sha256_ctx *c, const unsigned char *p) {
25     uint32_t w[64];
26     uint32_t a, b, cc, d, e, f, g, h, t1, t2;
27     int i;
28 270606    for (i = 0; i < 16; i++) {
29 254688        w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) |
30 254688               (uint32_t)p[i * 4 + 3];
31     }
32 779982    for (i = 16; i < 64; i++) {
33 764064        uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3);
34 764064        uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10);
35 764064        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
36     }
37 15918    a = c->h[0];
38 15918    b = c->h[1];
39 15918    cc = c->h[2];
40 15918    d = c->h[3];
41 15918    e = c->h[4];
42 15918    f = c->h[5];
43 15918    g = c->h[6];
44 15918    h = c->h[7];
45 1034670    for (i = 0; i < 64; i++) {
46 1018752        uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
47 1018752        uint32_t ch = (e & f) ^ ((~e) & g);
48 1018752        uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
49 1018752        uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
50 1018752        t1 = h + S1 + ch + K[i] + w[i];
51 1018752        t2 = S0 + maj;
52 1018752        h = g;
53 1018752        g = f;
54 1018752        f = e;
55 1018752        e = d + t1;
56 1018752        d = cc;
57 1018752        cc = b;
58 1018752        b = a;
59 1018752        a = t1 + t2;
60     }
61 15918    c->h[0] += a;
62 15918    c->h[1] += b;
63 15918    c->h[2] += cc;
64 15918    c->h[3] += d;
65 15918    c->h[4] += e;
66 15918    c->h[5] += f;
67 15918    c->h[6] += g;
68 15918    c->h[7] += h;
69 15918}
70 
71 129void ygst_sha256_init(ygst_sha256_ctx *c) {
72 129    c->h[0] = 0x6a09e667u;
73 129    c->h[1] = 0xbb67ae85u;
74 129    c->h[2] = 0x3c6ef372u;
75 129    c->h[3] = 0xa54ff53au;
76 129    c->h[4] = 0x510e527fu;
77 129    c->h[5] = 0x9b05688cu;
78 129    c->h[6] = 0x1f83d9abu;
79 129    c->h[7] = 0x5be0cd19u;
80 129    c->total = 0;
81 129    c->used = 0;
82 129}
83 
84 1243void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) {
85 1243    const unsigned char *p = (const unsigned char *)data;
86 1243    c->total += (uint64_t)n;
87 1243    if (c->used) {
88 1008        size_t take = 64 - c->used;
89 1008        if (take > n) take = n;
90 1008        memcpy(c->block + c->used, p, take);
91 1008        c->used += take;
92 1008        p += take;
93 1008        n -= take;
94 1008        if (c->used < 64) return;
95 996        sha256_block(c, c->block);
96 996        c->used = 0;
97     }
98 16002    while (n >= 64) {
99 14771        sha256_block(c, p);
100 14771        p += 64;
101 14771        n -= 64;
102     }
103 1231    if (n) {
104 1119        memcpy(c->block, p, n);
105 1119        c->used = n;
106     }
107 }
108 
109 129void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) {
110 129    uint64_t bits = c->total * 8u;
111     int i;
112 129    c->block[c->used++] = 0x80;
113 129    if (c->used > 56) {
114 22        memset(c->block + c->used, 0, 64 - c->used);
115 22        sha256_block(c, c->block);
116 22        c->used = 0;
117     }
118 129    memset(c->block + c->used, 0, 56 - c->used);
119 1161    for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i));
120 129    sha256_block(c, c->block);
121 1161    for (i = 0; i < 8; i++) {
122 1032        out[i * 4] = (unsigned char)(c->h[i] >> 24);
123 1032        out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16);
124 1032        out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8);
125 1032        out[i * 4 + 3] = (unsigned char)c->h[i];
126     }
127 129}
128 
129 5void ygst_sha256(const void *data, size_t n, unsigned char out[32]) {
130     ygst_sha256_ctx c;
131 5    ygst_sha256_init(&c);
132 5    ygst_sha256_update(&c, data, n);
133 5    ygst_sha256_final(&c, out);
134 5}
135 
136 45void ygst_hmac_sha256(const void *key, size_t key_len, const void *msg, size_t msg_len, unsigned char out[32]) {
137     unsigned char k[64];
138     unsigned char pad[64];
139     unsigned char inner[32];
140     ygst_sha256_ctx c;
141     int i;
142 45    memset(k, 0, sizeof k);
143 45    if (key_len > 64) {
144 1        ygst_sha256(key, key_len, k);
145 44    } else if (key_len) {
146 44        memcpy(k, key, key_len);
147     }
148 2925    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36);
149 45    ygst_sha256_init(&c);
150 45    ygst_sha256_update(&c, pad, 64);
151 45    ygst_sha256_update(&c, msg, msg_len);
152 45    ygst_sha256_final(&c, inner);
153 2925    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c);
154 45    ygst_sha256_init(&c);
155 45    ygst_sha256_update(&c, pad, 64);
156 45    ygst_sha256_update(&c, inner, 32);
157 45    ygst_sha256_final(&c, out);
158 45}
159 
160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
161 
162 51size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) {
163 51    size_t need = ((n + 2) / 3) * 4;
164     size_t i;
165 51    size_t o = 0;
166 51    if (!out || cap < need + 1) return 0;
167 474    for (i = 0; i + 2 < n; i += 3) {
168 425        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2];
169 425        out[o++] = B64[(v >> 18) & 63];
170 425        out[o++] = B64[(v >> 12) & 63];
171 425        out[o++] = B64[(v >> 6) & 63];
172 425        out[o++] = B64[v & 63];
173     }
174 49    if (n - i == 1) {
175 2        uint32_t v = (uint32_t)in[i] << 16;
176 2        out[o++] = B64[(v >> 18) & 63];
177 2        out[o++] = B64[(v >> 12) & 63];
178 2        out[o++] = '=';
179 2        out[o++] = '=';
180 47    } else if (n - i == 2) {
181 44        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
182 44        out[o++] = B64[(v >> 18) & 63];
183 44        out[o++] = B64[(v >> 12) & 63];
184 44        out[o++] = B64[(v >> 6) & 63];
185 44        out[o++] = '=';
186     }
187 49    out[o] = 0;
188 49    return o;
189 }
190 
191 134void ygst_hex_lower(const unsigned char *in, size_t n, char *out) {
192     static const char H[] = "0123456789abcdef";
193     size_t i;
194 1974    for (i = 0; i < n; i++) {
195 1840        out[i * 2] = H[in[i] >> 4];
196 1840        out[i * 2 + 1] = H[in[i] & 15];
197     }
198 134    out[n * 2] = 0;
199 134}

Sources/STKouyuEngine/core/ygst_envelope.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Shengtong style result and error JSON (DESIGN 6.2), the dtLastResponse clock text, the
6  * request params echo, base URL resolution and small identifiers.
7  */
8 #include "ygst_core.h"
9 
10 #include <stdio.h>
11 #include <string.h>
12 
13 /* civil_from_days (H. Hinnant) */
14 31static void civil_from_days(long long z, long long *y, unsigned *m, unsigned *d) {
15     long long era;
16     unsigned doe, yoe, doy, mp;
17 31    z += 719468;
18 31    era = (z >= 0 ? z : z - 146096) / 146097;
19 31    doe = (unsigned)(z - era * 146097);
20 31    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
21 31    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
22 31    mp = (5 * doy + 2) / 153;
23 31    *d = doy - (153 * mp + 2) / 5 + 1;
24 31    *m = mp < 10 ? mp + 3 : mp - 9;
25 31    *y = (long long)yoe + era * 400 + (*m <= 2);
26 31}
27 
28 31int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) {
29 31    long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL;
30 31    long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL);
31 31    long long rem = ms - days * 86400000LL;
32     long long y;
33     unsigned m, d, hh, mi, ss, mss;
34 31    civil_from_days(days, &y, &m, &d);
35 31    if (y < 0) y = 0;
36 31    if (y > 9999) y = 9999;
37 31    hh = (unsigned)(rem / 3600000LL) % 24u;
38 31    mi = (unsigned)(rem / 60000LL) % 60u;
39 31    ss = (unsigned)(rem / 1000LL) % 60u;
40 31    mss = (unsigned)rem % 1000u;
41 31    snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss,
42              mss);
43 31    return 0;
44 }
45 
46 42int ygst_align_result(ygst_span result, ygst_buf *out) {
47     ygst_span sentences, sent, details, d, scores, v, so, sp;
48 42    size_t spos = 0;
49 42    size_t copied = 0; /* bytes of result already copied to out */
50 42    int added = 0;
51 42    if (!result.p) return -1;
52 78    if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 ||
53 37        ygst_json_type(sentences) != 'a') {
54 5        return ygst_buf_append(out, result.p, result.n) == 0 ? 0 : -1;
55     }
56 77    while (ygst_json_array_next(sentences, &spos, &sent) == 1) {
57 41        size_t dpos = 0;
58 77        if (ygst_json_type(sent) != 'o' || ygst_json_member(sent, "details", &details) != 1 ||
59 36            ygst_json_type(details) != 'a') {
60 6            continue;
61         }
62 233        while (ygst_json_array_next(details, &dpos, &d) == 1) {
63             int add_overall, add_pron;
64             size_t close;
65 198            if (ygst_json_type(d) != 'o' || ygst_json_member(d, "overall", &v) == 1) continue;
66 196            if (ygst_json_member(d, "scores", &scores) != 1 || ygst_json_type(scores) != 'o') continue;
67 194            add_overall = ygst_json_member(scores, "overall", &so) == 1;
68 194            add_pron = ygst_json_member(d, "pronunciation", &v) != 1 && ygst_json_member(scores, "pronunciation", &sp) == 1;
69 194            if (!add_overall && !add_pron) continue;
70 193            close = (size_t)(d.p + d.n - 1 - result.p); /* the closing brace of the item */
71 193            ygst_buf_append(out, result.p + copied, close - copied);
72 193            if (add_overall) {
73 193                ygst_buf_append_str(out, ",\"overall\":");
74 193                ygst_buf_append(out, so.p, so.n);
75 193                added++;
76             }
77 193            if (add_pron) {
78 190                ygst_buf_append_str(out, ",\"pronunciation\":");
79 190                ygst_buf_append(out, sp.p, sp.n);
80 190                added++;
81             }
82 193            copied = close;
83         }
84     }
85 36    ygst_buf_append(out, result.p + copied, result.n - copied);
86 36    return out->oom ? -1 : added;
87 }
88 
89 370static void key(ygst_buf *out, const char *k, int first) {
90 370    if (!first) ygst_buf_append_char(out, ',');
91 370    ygst_json_escape(out, k);
92 370    ygst_buf_append_char(out, ':');
93 370}
94 
95 32int ygst_envelope_result(ygst_buf *out, const char *token_id, const char *record_id_json, const char *app_key,
96                          const char *user_id, const char *ref_text, const char *dt_last_response,
97                          ygst_span result_raw, const char *params_json, const char *audio_url) {
98 32    if (!result_raw.p || result_raw.n == 0) return -1;
99 31    ygst_buf_append_char(out, '{');
100 31    key(out, "tokenId", 1);
101 31    ygst_json_escape(out, token_id);
102 31    if (record_id_json && record_id_json[0]) {
103 30        key(out, "recordId", 0);
104 30        ygst_buf_append_str(out, record_id_json);
105     }
106 31    key(out, "applicationId", 0);
107 31    ygst_json_escape(out, app_key);
108 31    key(out, "userId", 0);
109 31    ygst_json_escape(out, user_id);
110 31    key(out, "refText", 0);
111 31    ygst_json_escape(out, ref_text);
112 31    key(out, "eof", 0);
113 31    ygst_buf_append_char(out, '1');
114 31    key(out, "dtLastResponse", 0);
115 31    ygst_json_escape(out, dt_last_response);
116 31    key(out, "result", 0);
117 31    if (ygst_align_result(result_raw, out) < 0) return -1;
118 31    if (params_json && params_json[0]) {
119 2        key(out, "params", 0);
120 2        ygst_buf_append_str(out, params_json);
121     }
122 31    if (audio_url && audio_url[0]) {
123 3        key(out, "audioUrl", 0);
124 3        ygst_json_escape(out, audio_url);
125     }
126 31    ygst_buf_append_char(out, '}');
127 31    ygst_buf_cstr(out);
128 31    return out->oom ? -1 : 0;
129 }
130 
131 17int ygst_envelope_error(ygst_buf *out, const char *token_id, int err_id, const char *message,
132                         const char *app_key) {
133 17    ygst_buf_append_char(out, '{');
134 17    key(out, "tokenId", 1);
135 17    ygst_json_escape(out, token_id);
136 17    key(out, "errId", 0);
137 17    ygst_buf_append_int(out, err_id);
138 17    key(out, "error", 0);
139 17    ygst_json_escape(out, message);
140 17    key(out, "eof", 0);
141 17    ygst_buf_append_char(out, '1');
142 17    key(out, "applicationId", 0);
143 17    ygst_json_escape(out, app_key);
144 17    ygst_buf_append_char(out, '}');
145 17    ygst_buf_cstr(out);
146 17    return out->oom ? -1 : 0;
147 }
148 
149 3int ygst_params_json(ygst_buf *out, const char *app_key, const char *user_id, int64_t timestamp_sec,
150                      const char *audio_type, int sample_rate, int channel, int sample_bytes,
151                      const char *core_type, const char *token_id, const ygst_fields *request_fields) {
152     size_t i;
153     char ts[32];
154 3    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
155 3    ygst_buf_append_str(out, "{\"app\":{");
156 3    key(out, "applicationId", 1);
157 3    ygst_json_escape(out, app_key);
158 3    key(out, "userId", 0);
159 3    ygst_json_escape(out, user_id);
160 3    key(out, "timestamp", 0);
161 3    ygst_json_escape(out, ts);
162 3    ygst_buf_append_str(out, "},\"audio\":{");
163 3    key(out, "audioType", 1);
164 3    ygst_json_escape(out, audio_type);
165 3    key(out, "sampleRate", 0);
166 3    ygst_buf_append_int(out, sample_rate);
167 3    key(out, "channel", 0);
168 3    ygst_buf_append_int(out, channel);
169 3    key(out, "sampleBytes", 0);
170 3    ygst_buf_append_int(out, sample_bytes);
171 3    ygst_buf_append_str(out, "},\"request\":{");
172 3    key(out, "coreType", 1);
173 3    ygst_json_escape(out, core_type);
174 3    key(out, "tokenId", 0);
175 3    ygst_json_escape(out, token_id);
176 3    if (request_fields) {
177 9        for (i = 0; i < request_fields->count; i++) {
178 7            const char *n = request_fields->items[i].name;
179 7            if (strcmp(n, "coreType") == 0 || strcmp(n, "tokenId") == 0) continue;
180 6            key(out, n, 0);
181 6            ygst_json_escape(out, request_fields->items[i].value);
182         }
183     }
184 3    ygst_buf_append_str(out, "}}");
185 3    ygst_buf_cstr(out);
186 3    return out->oom ? -1 : 0;
187 }
188 
189 /* ------------------------------------------------------------------ base URL */
190 
191 46static int prefix_ci(const char *s, const char *p) {
192 157    while (*p) {
193 144        char a = *s, b = *p;
194 144        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
195 144        if (a != b) return 0;
196 111        s++;
197 111        p++;
198     }
199 13    return 1;
200 }
201 
202 13static int host_is_shengtong(const char *host, size_t n) {
203     static const char D[] = "stkouyu.com";
204 13    size_t m = sizeof D - 1;
205     char h[256];
206     size_t i;
207 13    if (n == 0 || n >= sizeof h) return 0;
208 195    for (i = 0; i < n; i++) {
209 182        char c = host[i];
210 182        h[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
211     }
212 13    h[n] = 0;
213 13    if (n == m) return strcmp(h, D) == 0;
214 9    return n > m && strcmp(h + n - m, D) == 0 && h[n - m - 1] == '.';
215 }
216 
217 18ygst_base_kind ygst_resolve_base_url(const char *server, char *out, size_t cap) {
218     char s[1024];
219     const char *scheme;
220     const char *rest;
221     const char *host_start;
222     const char *host_end;
223     const char *end;
224     size_t n;
225     int written;
226 18    if (!out || cap == 0) return YGST_BASE_DEFAULT_EMPTY;
227 18    snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL);
228 18    n = ygst_trim_copy(server, s, sizeof s);
229 18    if (n == 0) return YGST_BASE_DEFAULT_EMPTY;
230 16    if (prefix_ci(s, "https://")) {
231 2        scheme = "https";
232 2        rest = s + 8;
233 14    } else if (prefix_ci(s, "http://")) {
234 5        scheme = "http";
235 5        rest = s + 7;
236 9    } else if (prefix_ci(s, "wss://")) {
237 2        scheme = "https";
238 2        rest = s + 6;
239 7    } else if (prefix_ci(s, "ws://")) {
240 4        scheme = "http";
241 4        rest = s + 5;
242     } else {
243 3        return host_is_shengtong(s, strcspn(s, "/:?#")) ? YGST_BASE_DEFAULT_SHENGTONG : YGST_BASE_ERR_SCHEME;
244     }
245 13    host_start = rest;
246 13    if (*host_start == '[') {
247 2        host_end = strchr(host_start, ']');
248 2        if (!host_end) return YGST_BASE_ERR_HOST;
249 1        host_end++;
250     } else {
251 11        host_end = host_start;
252 159        while (*host_end && *host_end != '/' && *host_end != ':' && *host_end != '?' && *host_end != '#') host_end++;
253     }
254 12    if (host_end == host_start) return YGST_BASE_ERR_HOST;
255 10    if (host_is_shengtong(host_start, (size_t)(host_end - host_start))) return YGST_BASE_DEFAULT_SHENGTONG;
256 7    end = rest;
257 145    while (*end && *end != '?' && *end != '#') end++;
258 9    while (end > host_end && end[-1] == '/') end--;
259 7    written = snprintf(out, cap, "%s://%.*s", scheme, (int)(end - rest), rest);
260 7    if (written < 0 || (size_t)written >= cap) {
261 1        snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL);
262 1        return YGST_BASE_ERR_HOST;
263     }
264 6    return YGST_BASE_CUSTOM;
265 }
266 
267 36int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) {
268     size_t n;
269     int w;
270 36    if (!base || !core_type || !out) return -1;
271 35    n = strlen(base);
272 36    while (n > 0 && base[n - 1] == '/') n--;
273 35    w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type);
274 35    return (w < 0 || (size_t)w >= cap) ? -1 : 0;
275 }
276 
277 41int ygst_token_id(const unsigned char rnd[16], char out[33]) {
278 41    ygst_hex_lower(rnd, 16, out);
279 41    return 0;
280 }
281 
282 8int ygst_idempotency_key_ok(const char *k) {
283 8    size_t n = 0;
284 8    if (!k) return 0;
285 445    for (; k[n]; n++) {
286 442        unsigned char c = (unsigned char)k[n];
287 442        if (c < 0x21 || c > 0x7E) return 0;
288 439        if (n >= 200) return 0;
289     }
290 3    return n >= 1;
291 }
292 
293 3void ygst_mask_app_key(const char *app_key, char out[16]) {
294 3    size_t n = app_key ? strlen(app_key) : 0;
295 3    if (n > 4) n = 4;
296 3    memcpy(out, app_key ? app_key : "", n);
297 3    memcpy(out + n, "***", 4);
298 3}

Sources/STKouyuEngine/core/ygst_errors_table.c 100.00%

1 /* Generated by tools/gen_c_errors.py from spec/errors.json version 2026-10-08. Do not edit. */
2 #include "ygst_core.h"
3 
4 const char *const ygst_table_version = "2026-10-08";
5 
6 const ygst_error_entry ygst_table_errors[] = {
7     {40001, "VALIDATION_ERROR", "INVALID_PARAM", 0, 400, "请求参数校验失败"},
8     {40100, "UNAUTHORIZED", "AUTH", 0, 401, "未认证,缺少或无效的凭据"},
9     {40300, "FORBIDDEN", "PERMISSION", 0, 403, "无权限,例如 API Key 未授权该 coreType"},
10     {40400, "NOT_FOUND", "NOT_FOUND", 0, 404, "资源不存在"},
11     {40900, "CONFLICT", "CONFLICT", 0, 409, "资源冲突"},
12     {40901, "IDEMPOTENCY_IN_PROGRESS", "CONFLICT", 1, 409, "同一幂等键的请求仍在处理中,稍后用同一个键重试"},
13     {40902, "QUOTA_INSUFFICIENT", "QUOTA", 0, 409, "套餐额度和账户余额均不足"},
14     {40903, "IDEMPOTENCY_KEY_REUSED", "CONFLICT", 0, 409, "幂等键已用于另一个不同的请求"},
15     {42900, "TOO_MANY_REQUESTS", "RATE_LIMIT", 1, 429, "请求过多或评测排队超时,按 Retry-After 稍后重试"},
16     {42901, "CONCURRENCY_LIMIT", "RATE_LIMIT", 1, 429, "并发评测数超出套餐层级限制"},
17     {42902, "TRIAL_REPORT_DAILY_LIMIT", "QUOTA", 0, 429, "试用层 AI 报告达到每日上限"},
18     {42903, "SANDBOX_DAILY_LIMIT", "QUOTA", 0, 429, "沙箱密钥当日调用次数达到上限"},
19     {50000, "INTERNAL_ERROR", "SERVER", 1, 500, "服务器内部错误"},
20     {50010, "FEATURE_NOT_IMPLEMENTED", "SERVER", 0, 500, "功能未实现"},
21     {50200, "UPSTREAM_SERVICE_ERROR", "UPSTREAM", 1, 502, "上游评测服务暂不可用"},
22     {2001, "TOKEN_EXPIRED", "AUTH", 0, 401, "token 已过期,兼容层早期引擎接口也用此码表示缺少鉴权三元组或签名不匹配"},
23     {2002, "TOKEN_INVALID", "AUTH", 0, 401, "token 无效,兼容层早期引擎接口也用此码表示时间戳超窗"},
24     {2003, "SIGNATURE_VERIFICATION_FAILED", "AUTH", 0, 401, "签名验证失败,兼容层早期引擎接口也用此码表示 appKey 不存在"},
25     {2004, "SESSION_EXPIRED", "AUTH", 0, 401, "会话已过期"},
26     {2005, "TOKEN_BLACKLISTED", "AUTH", 0, 401, "token 已被列入黑名单"},
27     {2006, "API_KEY_INVALID", "AUTH", 0, 401, "API Key 无效"},
28     {2007, "API_KEY_EXPIRED", "AUTH", 0, 401, "API Key 已过期"},
29     {2008, "TOKEN_METADATA_NOT_FOUND", "AUTH", 0, 401, "token 元数据丢失"},
30     {2009, "TOKEN_REFRESH_FAILED", "AUTH", 0, 401, "token 刷新失败"},
31     {2010, "API_KEY_NOT_FOUND", "AUTH", 0, 401, "API Key 不存在"},
32     {2011, "API_KEY_DISABLED", "AUTH", 0, 401, "API Key 已禁用"},
33     {2012, "API_KEY_QUOTA_EXCEEDED", "QUOTA", 0, 400, "API Key 配额已超限"},
34     {3001, "RATE_LIMIT_EXCEEDED_IP", "RATE_LIMIT", 1, 429, "请求过于频繁,按来源地址限流"},
35     {3002, "RATE_LIMIT_EXCEEDED_USER", "RATE_LIMIT", 1, 429, "请求过于频繁,按用户限流"},
36     {3003, "RATE_LIMIT_EXCEEDED_API_KEY", "RATE_LIMIT", 1, 429, "请求过于频繁,按 API Key 限流"},
37     {1004, "USER_DISABLED", "PERMISSION", 0, 400, "用户已禁用"},
38     {1005, "USER_LOCKED", "PERMISSION", 0, 400, "用户已锁定"},
39     {1012, "ACCOUNT_DISABLED", "PERMISSION", 0, 400, "账户已禁用"},
40     {1013, "ACCOUNT_FROZEN", "PERMISSION", 0, 400, "账户已冻结"},
41     {1310, "ACCOUNT_BALANCE_INSUFFICIENT", "QUOTA", 0, 400, "账户余额不足"},
42     {1311, "ACCOUNT_LEDGER_CONFLICT", "CONFLICT", 1, 400, "账目并发冲突,重试即可"},
43 };
44 const size_t ygst_table_errors_count = sizeof ygst_table_errors / sizeof ygst_table_errors[0];
45 
46 const ygst_error_entry ygst_table_warnings[] = {
47     {1001, "NO_VALID_AUDIO", "AUDIO", 0, 0, "No valid audio detected!"},
48     {1002, "VOLUME_TOO_LOW", "AUDIO", 0, 0, "Audio volume too low!"},
49     {1003, "VOLUME_TOO_HIGH", "AUDIO", 0, 0, "Audio volume too high!"},
50     {1004, "AUDIO_NOISY", "AUDIO", 0, 0, "Audio noisy!"},
51     {1005, "AUDIO_INCOMPLETE", "AUDIO", 0, 0, "Audio not complete!"},
52     {1009, "SCORER_DEGRADED", "AUDIO", 1, 0, "scorer degraded"},
53 };
54 const size_t ygst_table_warnings_count = sizeof ygst_table_warnings / sizeof ygst_table_warnings[0];
55 
56 const ygst_error_entry ygst_table_local[] = {
57     {90001, "NETWORK_ERROR", "NETWORK", 1, 0, "网络错误,连接失败或被重置"},
58     {90002, "TIMEOUT", "TIMEOUT", 1, 0, "连接或读取超时"},
59     {90003, "CANCELLED", "CANCELLED", 0, 0, "调用方取消了请求"},
60     {90004, "CLIENT_CLOSED", "STATE", 0, 0, "客户端已关闭"},
61     {90005, "PROTOCOL_ERROR", "PROTOCOL", 0, 0, "响应或帧无法解析"},
62     {90006, "RECONNECT_EXHAUSTED", "NETWORK", 0, 0, "实时连接重连次数用尽"},
63     {90007, "RESULT_TIMEOUT", "TIMEOUT", 1, 0, "结束后等待终评结果超时"},
64     {90008, "REPLAY_BUFFER_OVERFLOW", "STATE", 0, 0, "重连重放缓冲超过上限"},
65     {90009, "INVALID_STATE", "STATE", 0, 0, "当前会话状态不允许该操作"},
66     {90010, "INVALID_ARGUMENT", "INVALID_PARAM", 0, 0, "调用参数不合法"},
67     {90011, "TLS_ERROR", "NETWORK", 0, 0, "证书校验失败"},
68     {90101, "AUDIO_TOO_SHORT", "AUDIO", 0, 0, "音频短于 1 秒"},
69     {90102, "AUDIO_TOO_LONG", "AUDIO", 0, 0, "音频长于 300 秒,整段上传大于 50 MB,或实时评测一轮大于 10 MB"},
70     {90103, "AUDIO_SILENT", "AUDIO", 0, 0, "音频全程静音"},
71     {90104, "AUDIO_LOW_VOLUME", "AUDIO", 0, 0, "音量过低"},
72     {90105, "AUDIO_FORMAT_UNSUPPORTED", "AUDIO", 0, 0, "不是 16 位 PCM 或采样率低于 16000"},
73     {90201, "RECORDER_PERMISSION_DENIED", "PERMISSION", 0, 0, "没有麦克风权限"},
74     {90202, "RECORDER_UNAVAILABLE", "STATE", 0, 0, "麦克风被占用或不可用"},
75     {90203, "RECORDER_ERROR", "STATE", 0, 0, "录音过程出错"},
76 };
77 const size_t ygst_table_local_count = sizeof ygst_table_local / sizeof ygst_table_local[0];
78 
79 const ygst_compat_entry ygst_table_compat[] = {
80     {20009, "网络或服务端临时故障,可重评,autoRetry 默认重评此码"},
81     {60001, "音频文件不存在或不可读"},
82     {60002, "音频为空"},
83     {60003, "coreType 不支持"},
84     {60004, "麦克风不可用或没有录音权限"},
85     {60005, "音频短于 1 秒"},
86     {60006, "refText 为空"},
87     {60007, "引擎未初始化"},
88     {60008, "引擎正忙,上一次评测未结束"},
89     {60009, "音频大于 50 MB 或长于 300 秒"},
90 };
91 const size_t ygst_table_compat_count = sizeof ygst_table_compat / sizeof ygst_table_compat[0];
92 
93 const int ygst_table_retryable_http[] = {408, 425, 429, 500, 502, 503, 504};
94 const size_t ygst_table_retryable_http_count = sizeof ygst_table_retryable_http / sizeof ygst_table_retryable_http[0];
95 
96 45const char *ygst_http_fallback_category(int status) {
97 45    switch (status) {
98 3    case 400:
99 3        return "INVALID_PARAM";
100 3    case 401:
101 3        return "AUTH";
102 2    case 403:
103 2        return "PERMISSION";
104 3    case 404:
105 3        return "NOT_FOUND";
106 1    case 405:
107 1        return "INVALID_PARAM";
108 2    case 408:
109 2        return "TIMEOUT";
110 2    case 409:
111 2        return "CONFLICT";
112 2    case 413:
113 2        return "INVALID_PARAM";
114 1    case 415:
115 1        return "INVALID_PARAM";
116 3    case 422:
117 3        return "INVALID_PARAM";
118 1    case 425:
119 1        return "RATE_LIMIT";
120 2    case 429:
121 2        return "RATE_LIMIT";
122 2    case 500:
123 2        return "SERVER";
124 2    case 501:
125 2        return "SERVER";
126 2    case 502:
127 2        return "UPSTREAM";
128 2    case 503:
129 2        return "UPSTREAM";
130 1    case 504:
131 1        return "UPSTREAM";
132 11    default:
133         /* DESIGN 2.4: statuses missing from httpFallback map by class */
134 11        if (status >= 400 && status <= 499) return "INVALID_PARAM";
135 8        if (status >= 500 && status <= 599) return "SERVER";
136 6        return "UNKNOWN";
137     }
138 }

Sources/STKouyuEngine/core/ygst_fields.c 94.63%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Ordered form field list and the platform request signature (CONTRACT.md 0.2):
6  * drop empty values, sort by key, join key=value with &, Base64(HMAC_SHA256(payload, secret)).
7  */
8 #include "ygst_core.h"
9 
10 #include <stdlib.h>
11 #include <string.h>
12 
13 254void ygst_fields_init(ygst_fields *f) {
14 254    f->items = NULL;
15 254    f->count = 0;
16 254    f->cap = 0;
17 254    f->oom = 0;
18 254}
19 
20 123void ygst_fields_free(ygst_fields *f) {
21     size_t i;
22 372    for (i = 0; i < f->count; i++) {
23 249        free(f->items[i].name);
24 249        free(f->items[i].value);
25     }
26 123    free(f->items);
27 123    ygst_fields_init(f);
28 123}
29 
30 268static long find_index(const ygst_fields *f, const char *name) {
31     size_t i;
32 2068    for (i = 0; i < f->count; i++) {
33 1813        if (strcmp(f->items[i].name, name) == 0) return (long)i;
34     }
35 255    return -1;
36 }
37 
38 2int ygst_fields_remove(ygst_fields *f, const char *name) {
39 2    long i = name ? find_index(f, name) : -1;
40 2    if (i < 0) return 0;
41 1    free(f->items[i].name);
42 1    free(f->items[i].value);
43 1    memmove(&f->items[i], &f->items[i + 1], (f->count - (size_t)i - 1) * sizeof(ygst_field));
44 1    f->count--;
45 1    return 1;
46 }
47 
48 261int ygst_fields_set(ygst_fields *f, const char *name, const char *value) {
49     long i;
50     char *v;
51 261    if (!name || f->oom) return -1;
52 260    if (!value) {
53 1        ygst_fields_remove(f, name);
54 1        return 0;
55     }
56 259    i = find_index(f, name);
57 259    v = ygst_strdup(value);
58 259    if (!v) {
59 0        f->oom = 1;
60 0        return -1;
61     }
62 259    if (i >= 0) {
63 9        free(f->items[i].value);
64 9        f->items[i].value = v;
65 9        return 0;
66     }
67 250    if (f->count == f->cap) {
68 93        size_t cap = f->cap ? f->cap * 2 : 16;
69 93        ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field));
70 93        if (!p) {
71 0            free(v);
72 0            f->oom = 1;
73 0            return -1;
74         }
75 93        f->items = p;
76 93        f->cap = cap;
77     }
78 250    f->items[f->count].name = ygst_strdup(name);
79 250    if (!f->items[f->count].name) {
80 0        free(v);
81 0        f->oom = 1;
82 0        return -1;
83     }
84 250    f->items[f->count].value = v;
85 250    f->count++;
86 250    return 0;
87 }
88 
89 7const char *ygst_fields_get(const ygst_fields *f, const char *name) {
90 7    long i = name ? find_index(f, name) : -1;
91 7    return i < 0 ? NULL : f->items[i].value;
92 }
93 
94 1int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) {
95     size_t i;
96 42    for (i = 0; i < src->count; i++) {
97 41        if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1;
98     }
99 1    return 0;
100 }
101 
102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */
103 458static uint32_t next_cp(const unsigned char **s) {
104 458    const unsigned char *p = *s;
105 458    uint32_t c = p[0];
106 458    int n = 0;
107     int i;
108 458    if (c < 0x80) {
109 448        *s = p + 1;
110 448        return c;
111     }
112 10    if ((c & 0xE0) == 0xC0) {
113 3        n = 1;
114 3        c &= 0x1F;
115 7    } else if ((c & 0xF0) == 0xE0) {
116 2        n = 2;
117 2        c &= 0x0F;
118 5    } else if ((c & 0xF8) == 0xF0) {
119 4        n = 3;
120 4        c &= 0x07;
121     } else {
122 1        *s = p + 1;
123 1        return 0xFFFD;
124     }
125 27    for (i = 1; i <= n; i++) {
126 19        if ((p[i] & 0xC0) != 0x80) {
127 1            *s = p + 1;
128 1            return 0xFFFD;
129         }
130 18        c = (c << 6) | (p[i] & 0x3F);
131     }
132 8    *s = p + n + 1;
133 8    return c;
134 }
135 
136 /* Emits the UTF-16 code units of cp into u, returns how many (1 or 2). */
137 458static int utf16_units(uint32_t cp, uint32_t u[2]) {
138 458    if (cp >= 0x10000 && cp <= 0x10FFFF) {
139 4        cp -= 0x10000;
140 4        u[0] = 0xD800 + (cp >> 10);
141 4        u[1] = 0xDC00 + (cp & 0x3FF);
142 4        return 2;
143     }
144 454    u[0] = cp;
145 454    return 1;
146 }
147 
148 154int ygst_utf16_compare(const char *a, const char *b) {
149 154    const unsigned char *pa = (const unsigned char *)a;
150 154    const unsigned char *pb = (const unsigned char *)b;
151     uint32_t ua[2], ub[2];
152 154    int na = 0, nb = 0, ia = 0, ib = 0;
153 77    for (;;) {
154         uint32_t ca, cb;
155 231        if (ia == na) {
156 231            if (!*pa) {
157 2                na = 0;
158             } else {
159 229                na = utf16_units(next_cp(&pa), ua);
160             }
161 231            ia = 0;
162         }
163 231        if (ib == nb) {
164 231            if (!*pb) {
165 2                nb = 0;
166             } else {
167 229                nb = utf16_units(next_cp(&pb), ub);
168             }
169 231            ib = 0;
170         }
171 231        if (na == 0 || nb == 0) {
172 3            if (na == 0 && nb == 0) return 0;
173 2            return na == 0 ? -1 : 1;
174         }
175 228        ca = ua[ia++];
176 228        cb = ub[ib++];
177 228        if (ca != cb) return ca < cb ? -1 : 1;
178     }
179 }
180 
181 146static int cmp_field_ptr(const void *x, const void *y) {
182 146    const ygst_field *a = *(const ygst_field *const *)x;
183 146    const ygst_field *b = *(const ygst_field *const *)y;
184 146    return ygst_utf16_compare(a->name, b->name);
185 }
186 
187 51int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) {
188     const ygst_field **v;
189 51    size_t n = 0;
190     size_t i;
191 51    int first = 1;
192 51    if (f->oom) return -1;
193 51    v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v));
194 51    if (!v) return -1;
195 216    for (i = 0; i < f->count; i++) {
196 165        if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i];
197     }
198 51    qsort(v, n, sizeof(*v), cmp_field_ptr);
199 194    for (i = 0; i < n; i++) {
200 143        if (!first) ygst_buf_append_char(out, '&');
201 143        first = 0;
202 143        ygst_buf_append_str(out, v[i]->name);
203 143        ygst_buf_append_char(out, '=');
204 143        ygst_buf_append_str(out, v[i]->value);
205     }
206 51    free(v);
207 51    ygst_buf_cstr(out);
208 51    return out->oom ? -1 : 0;
209 }
210 
211 43int ygst_sign(const ygst_fields *f, const char *secret, char out[45]) {
212     ygst_buf payload;
213     unsigned char mac[32];
214     int rc;
215 43    out[0] = 0;
216 43    if (!secret) return -1;
217 42    ygst_buf_init(&payload);
218 42    rc = ygst_sign_payload(f, &payload);
219 42    if (rc == 0) {
220 42        ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"",
221                          payload.len, mac);
222 42        if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1;
223     }
224 42    ygst_buf_free(&payload);
225 42    return rc;
226 }

Sources/STKouyuEngine/core/ygst_json.c 97.70%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Minimal validating JSON scanner (RFC 8259). It never builds a tree: values are spans into the
6  * original text, so the platform result can be passed to the caller byte for byte.
7  */
8 #include "ygst_core.h"
9 
10 #include <stdlib.h>
11 #include <string.h>
12 
13 #define YGST_JSON_MAX_DEPTH 128
14 
15 1781539static const char *ws(const char *p, const char *e) {
16 1867010    while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
17 1781539    return p;
18 }
19 
20 156static int hexval(char c) {
21 156    if (c >= '0' && c <= '9') return c - '0';
22 27    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
23 0    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
24 0    return -1;
25 }
26 
27 504425static const char *skip_string(const char *p, const char *e) {
28 504425    if (p >= e || *p != '"') return NULL;
29 504422    p++;
30 3995773    while (p < e) {
31 3995771        unsigned char c = (unsigned char)*p;
32 3995771        if (c == '"') return p + 1;
33 3491354        if (c < 0x20) return NULL;
34 3491353        if (c == '\\') {
35 514            p++;
36 514            if (p >= e) return NULL;
37 514            switch (*p) {
38 489            case '"':
39             case '\\':
40             case '/':
41             case 'b':
42             case 'f':
43             case 'n':
44             case 'r':
45             case 't':
46 489                p++;
47 489                break;
48 24            case 'u': {
49                 int i;
50 24                if (e - p < 5) return NULL;
51 115                for (i = 1; i <= 4; i++) {
52 92                    if (hexval(p[i]) < 0) return NULL;
53                 }
54 23                p += 5;
55 23                break;
56             }
57 1            default:
58 1                return NULL;
59             }
60 512            continue;
61         }
62 3490839        p++;
63     }
64 2    return NULL;
65 }
66 
67 649275static int is_digit(char c) {
68 649275    return c >= '0' && c <= '9';
69 }
70 
71 195350static const char *skip_number(const char *p, const char *e) {
72 195350    if (p < e && *p == '-') p++;
73 195350    if (p >= e) return NULL;
74 195349    if (*p == '0') {
75 66935        p++;
76 128414    } else if (*p >= '1' && *p <= '9') {
77 408903        while (p < e && is_digit(*p)) p++;
78     } else {
79 0        return NULL;
80     }
81 195349    if (p < e && *p == '.') {
82 14983        p++;
83 14983        if (p >= e || !is_digit(*p)) return NULL;
84 34365        while (p < e && is_digit(*p)) p++;
85     }
86 195348    if (p < e && (*p == 'e' || *p == 'E')) {
87 8        p++;
88 8        if (p < e && (*p == '+' || *p == '-')) p++;
89 8        if (p >= e || !is_digit(*p)) return NULL;
90 19        while (p < e && is_digit(*p)) p++;
91     }
92 195347    return p;
93 }
94 
95 9843static const char *skip_lit(const char *p, const char *e, const char *lit) {
96 9843    size_t n = strlen(lit);
97 9843    if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL;
98 9840    return p + n;
99 }
100 
101 433747static const char *skip_value(const char *p, const char *e, int depth) {
102 433747    if (depth > YGST_JSON_MAX_DEPTH) return NULL;
103 433746    p = ws(p, e);
104 433746    if (p >= e) return NULL;
105 433743    switch (*p) {
106 71229    case '{':
107 71229        p = ws(p + 1, e);
108 71229        if (p < e && *p == '}') return p + 1;
109         for (;;) {
110 358431            p = skip_string(ws(p, e), e);
111 358431            if (!p) return NULL;
112 358428            p = ws(p, e);
113 358428            if (p >= e || *p != ':') return NULL;
114 358427            p = skip_value(p + 1, e, depth + 1);
115 358427            if (!p) return NULL;
116 358426            p = ws(p, e);
117 358426            if (p >= e) return NULL;
118 358426            if (*p == ',') {
119 287235                p++;
120 287235                continue;
121             }
122 71191            if (*p == '}') return p + 1;
123 1            return NULL;
124         }
125 40740    case '[':
126 40740        p = ws(p + 1, e);
127 40740        if (p < e && *p == ']') return p + 1;
128         for (;;) {
129 46448            p = skip_value(p, e, depth + 1);
130 46448            if (!p) return NULL;
131 46317            p = ws(p, e);
132 46317            if (p >= e) return NULL;
133 46317            if (*p == ',') {
134 18729                p++;
135 18729                continue;
136             }
137 27588            if (*p == ']') return p + 1;
138 1            return NULL;
139         }
140 116839    case '"':
141 116839        return skip_string(p, e);
142 247    case 't':
143 247        return skip_lit(p, e, "true");
144 9576    case 'f':
145 9576        return skip_lit(p, e, "false");
146 20    case 'n':
147 20        return skip_lit(p, e, "null");
148 195092    default:
149 195092        if (*p == '-' || is_digit(*p)) return skip_number(p, e);
150 6        return NULL;
151     }
152 }
153 
154 247int ygst_json_parse(const char *s, size_t n, ygst_span *root) {
155     const char *e;
156     const char *start;
157     const char *end;
158 247    if (!s) return -1;
159 246    e = s + n;
160 246    start = ws(s, e);
161 246    end = skip_value(start, e, 0);
162 246    if (!end) return -1;
163 220    if (ws(end, e) != e) return -1;
164 218    if (root) {
165 218        root->p = start;
166 218        root->n = (size_t)(end - start);
167     }
168 218    return 0;
169 }
170 
171 36466int ygst_json_type(ygst_span v) {
172 36466    if (!v.p || v.n == 0) return 0;
173 36465    switch (v.p[0]) {
174 34398    case '{':
175 34398        return 'o';
176 545    case '[':
177 545        return 'a';
178 1084    case '"':
179 1084        return 's';
180 14    case 't':
181 14        return 't';
182 49    case 'f':
183 49        return 'f';
184 6    case 'n':
185 6        return 'z';
186 369    default:
187 369        return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0;
188     }
189 }
190 
191 31021int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) {
192 31021    const char *e = obj.p + obj.n;
193     const char *p;
194     const char *k;
195     const char *v;
196 31021    if (ygst_json_type(obj) != 'o') return -1;
197 31020    p = obj.p + (*pos ? *pos : 1);
198 31020    p = ws(p, e);
199 31020    if (p >= e) return -1;
200 31020    if (*p == '}') return 0;
201 28249    if (*pos) {
202 25481        if (*p != ',') return -1;
203 25481        p = ws(p + 1, e);
204     }
205 28249    k = p;
206 28249    p = skip_string(p, e);
207 28249    if (!p) return -1;
208 28249    if (key) {
209 28249        key->p = k;
210 28249        key->n = (size_t)(p - k);
211     }
212 28249    p = ws(p, e);
213 28249    if (p >= e || *p != ':') return -1;
214 28248    v = ws(p + 1, e);
215 28248    p = skip_value(v, e, 1);
216 28248    if (!p) return -1;
217 28248    if (value) {
218 28248        value->p = v;
219 28248        value->n = (size_t)(p - v);
220     }
221 28248    *pos = (size_t)(p - obj.p);
222 28248    return 1;
223 }
224 
225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */
226 27309static int key_equals(ygst_span tok, const char *key) {
227 27309    size_t klen = strlen(key);
228 27309    if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) {
229 27304        return tok.n - 2 == klen && memcmp(tok.p + 1, key, klen) == 0;
230     } else {
231         ygst_buf b;
232         int eq;
233 5        ygst_buf_init(&b);
234 5        eq = ygst_json_string(tok, &b) == 0 && b.len == klen && memcmp(ygst_buf_cstr(&b), key, klen) == 0;
235 5        ygst_buf_free(&b);
236 5        return eq;
237     }
238 }
239 
240 2710int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) {
241 2710    size_t pos = 0;
242     ygst_span k, v;
243 2710    int found = 0;
244     int rc;
245 2710    if (ygst_json_type(obj) != 'o' || !key) return -1;
246 30018    while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) {
247 27309        if (key_equals(k, key)) {
248 2020            found = 1;
249 2020            if (out) *out = v;
250         }
251     }
252 2709    if (rc < 0) return -1;
253 2708    return found;
254 }
255 
256 469int ygst_json_array_next(ygst_span arr, size_t *pos, ygst_span *out) {
257 469    const char *e = arr.p + arr.n;
258     const char *p;
259     const char *v;
260 469    if (ygst_json_type(arr) != 'a') return -1;
261 468    p = arr.p + (*pos ? *pos : 1);
262 468    p = ws(p, e);
263 468    if (p >= e) return -1;
264 468    if (*p == ']') return 0;
265 379    if (*pos) {
266 291        if (*p != ',') return -1;
267 290        p = ws(p + 1, e);
268     }
269 378    v = p;
270 378    p = skip_value(v, e, 1);
271 378    if (!p) return -1;
272 378    if (out) {
273 378        out->p = v;
274 378        out->n = (size_t)(p - v);
275     }
276 378    *pos = (size_t)(p - arr.p);
277 378    return 1;
278 }
279 
280 14static void put_utf8(ygst_buf *out, uint32_t cp) {
281     char b[4];
282 14    if (cp < 0x80) {
283 6        b[0] = (char)cp;
284 6        ygst_buf_append(out, b, 1);
285 8    } else if (cp < 0x800) {
286 1        b[0] = (char)(0xC0 | (cp >> 6));
287 1        b[1] = (char)(0x80 | (cp & 0x3F));
288 1        ygst_buf_append(out, b, 2);
289 7    } else if (cp < 0x10000) {
290 6        b[0] = (char)(0xE0 | (cp >> 12));
291 6        b[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
292 6        b[2] = (char)(0x80 | (cp & 0x3F));
293 6        ygst_buf_append(out, b, 3);
294     } else {
295 1        b[0] = (char)(0xF0 | (cp >> 18));
296 1        b[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
297 1        b[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
298 1        b[3] = (char)(0x80 | (cp & 0x3F));
299 1        ygst_buf_append(out, b, 4);
300     }
301 14}
302 
303 16static uint32_t hex4(const char *p) {
304 16    return (uint32_t)((hexval(p[0]) << 12) | (hexval(p[1]) << 8) | (hexval(p[2]) << 4) | hexval(p[3]));
305 }
306 
307 907int ygst_json_string(ygst_span v, ygst_buf *out) {
308     const char *p;
309     const char *e;
310 907    if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1;
311 905    p = v.p + 1;
312 905    e = v.p + v.n - 1;
313 987    while (p < e) {
314 975        const char *run = p;
315 11972        while (p < e && *p != '\\') p++;
316 975        if (p > run) ygst_buf_append(out, run, (size_t)(p - run));
317 975        if (p >= e) break;
318 82        p++; /* backslash */
319 82        switch (*p) {
320 1        case 'b':
321 1            ygst_buf_append_char(out, '\b');
322 1            p++;
323 1            break;
324 1        case 'f':
325 1            ygst_buf_append_char(out, '\f');
326 1            p++;
327 1            break;
328 1        case 'n':
329 1            ygst_buf_append_char(out, '\n');
330 1            p++;
331 1            break;
332 1        case 'r':
333 1            ygst_buf_append_char(out, '\r');
334 1            p++;
335 1            break;
336 1        case 't':
337 1            ygst_buf_append_char(out, '\t');
338 1            p++;
339 1            break;
340 14        case 'u': {
341 14            uint32_t cp = hex4(p + 1);
342 14            p += 5;
343 14            if (cp >= 0xD800 && cp <= 0xDBFF) {
344 5                if (e - p >= 6 && p[0] == '\\' && p[1] == 'u') {
345 2                    uint32_t lo = hex4(p + 2);
346 2                    if (lo >= 0xDC00 && lo <= 0xDFFF) {
347 1                        cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
348 1                        p += 6;
349                     } else {
350 1                        cp = 0xFFFD;
351                     }
352                 } else {
353 1                    cp = 0xFFFD;
354                 }
355 11            } else if (cp >= 0xDC00 && cp <= 0xDFFF) {
356 1                cp = 0xFFFD;
357             }
358 14            put_utf8(out, cp);
359 14            break;
360         }
361 63        default: /* " \ / */
362 63            ygst_buf_append_char(out, *p);
363 63            p++;
364 63            break;
365         }
366     }
367 905    ygst_buf_cstr(out);
368 905    return out->oom ? -1 : 0;
369 }
370 
371 266static int number_text(ygst_span v, char *tmp, size_t cap) {
372 266    if (ygst_json_type(v) != 'n' || v.n >= cap) return -1;
373 264    if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1;
374 264    memcpy(tmp, v.p, v.n);
375 264    tmp[v.n] = 0;
376 264    return 0;
377 }
378 
379 7int ygst_json_number(ygst_span v, double *out) {
380     char tmp[64];
381 7    char *end = NULL;
382 7    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
383 6    *out = strtod(tmp, &end);
384 6    if (end && *end) {
385         /* A locale with a decimal comma: retry with the separator it expects. */
386 0        char *dot = strchr(tmp, '.');
387 0        if (dot) {
388 0            *dot = ',';
389 0            *out = strtod(tmp, &end);
390         }
391 0        if (end && *end) return -1;
392     }
393 6    return 0;
394 }
395 
396 259int ygst_json_int(ygst_span v, long long *out) {
397     char tmp[64];
398     size_t i;
399 259    int neg = 0;
400 259    long long acc = 0;
401 259    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
402 258    if (strpbrk(tmp, ".eE")) {
403         double d;
404 4        if (ygst_json_number(v, &d) != 0) return -1;
405 4        if (d != d || d > 9.2e18 || d < -9.2e18 || d != (double)(long long)d) return -1;
406 2        *out = (long long)d;
407 2        return 0;
408     }
409 254    i = 0;
410 254    if (tmp[0] == '-') {
411 1        neg = 1;
412 1        i = 1;
413     }
414 1312    for (; tmp[i]; i++) {
415 1059        int dgt = tmp[i] - '0';
416 1059        if (acc > (9223372036854775807LL - dgt) / 10) return -1;
417 1058        acc = acc * 10 + dgt;
418     }
419 253    *out = neg ? -acc : acc;
420 253    return 0;
421 }
422 
423 64int ygst_json_bool(ygst_span v, int *out) {
424 64    int t = ygst_json_type(v);
425 64    if (t == 't' && v.n == 4) {
426 14        *out = 1;
427 14        return 0;
428     }
429 50    if (t == 'f' && v.n == 5) {
430 49        *out = 0;
431 49        return 0;
432     }
433 1    return -1;
434 }
435 
436 605int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) {
437     static const char H[] = "0123456789abcdef";
438     size_t i;
439 605    size_t run = 0;
440 605    ygst_buf_append_char(out, '"');
441 7387    for (i = 0; i < n; i++) {
442 6782        unsigned char c = (unsigned char)s[i];
443 6782        const char *rep = NULL;
444         char u[7];
445 6782        if (c == '"') {
446 4            rep = "\\\"";
447 6778        } else if (c == '\\') {
448 2            rep = "\\\\";
449 6776        } else if (c == '\n') {
450 2            rep = "\\n";
451 6774        } else if (c == '\r') {
452 1            rep = "\\r";
453 6773        } else if (c == '\t') {
454 1            rep = "\\t";
455 6772        } else if (c == '\b') {
456 1            rep = "\\b";
457 6771        } else if (c == '\f') {
458 1            rep = "\\f";
459 6770        } else if (c < 0x20) {
460 2            u[0] = '\\';
461 2            u[1] = 'u';
462 2            u[2] = '0';
463 2            u[3] = '0';
464 2            u[4] = H[c >> 4];
465 2            u[5] = H[c & 15];
466 2            u[6] = 0;
467 2            rep = u;
468         }
469 6782        if (rep) {
470 14            if (i > run) ygst_buf_append(out, s + run, i - run);
471 14            ygst_buf_append_str(out, rep);
472 14            run = i + 1;
473         }
474     }
475 605    if (n > run) ygst_buf_append(out, s + run, n - run);
476 605    ygst_buf_append_char(out, '"');
477 605    return out->oom ? -1 : 0;
478 }
479 
480 605int ygst_json_escape(ygst_buf *out, const char *s) {
481 605    if (!s) s = "";
482 605    return ygst_json_escape_n(out, s, strlen(s));
483 }

Sources/STKouyuEngine/core/ygst_multipart.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * multipart/form-data assembly (RFC 7578) for POST /{coreType}. Text fields carry no filename,
6  * so the platform treats them as form parameters and they are exactly the signed set; the audio
7  * is a file part and is not signed.
8  */
9 #include "ygst_core.h"
10 
11 #include <stdio.h>
12 #include <string.h>
13 
14 34int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) {
15     char hex[33];
16 34    ygst_hex_lower(rnd, 16, hex);
17 34    snprintf(out, 64, "----YuguSTCompat%s", hex);
18 34    return 0;
19 }
20 
21 35int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) {
22     int n;
23 35    if (!boundary || !out) return -1;
24 35    n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary);
25 35    return (n < 0 || (size_t)n >= cap) ? -1 : 0;
26 }
27 
28 110static int contains(const unsigned char *hay, size_t n, const char *needle) {
29 110    size_t m = strlen(needle);
30     size_t i;
31 110    if (m == 0 || n < m) return 0;
32 2154050    for (i = 0; i + m <= n; i++) {
33 2154015        if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1;
34     }
35 35    return 0;
36 }
37 
38 41int ygst_multipart_build(const ygst_fields *fields, const char *boundary, const char *file_field,
39                          const char *filename, const char *file_content_type, const unsigned char *file,
40                          size_t file_len, ygst_buf *out) {
41     size_t i;
42 41    if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1;
43 39    if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1;
44 112    for (i = 0; i < fields->count; i++) {
45 76        const char *v = fields->items[i].value ? fields->items[i].value : "";
46 76        if (!ygst_field_name_ok(fields->items[i].name)) return -1;
47 75        if (contains((const unsigned char *)v, strlen(v), boundary)) return -2;
48     }
49 36    if (file && contains(file, file_len, boundary)) return -2;
50 109    for (i = 0; i < fields->count; i++) {
51 74        ygst_buf_append_str(out, "--");
52 74        ygst_buf_append_str(out, boundary);
53 74        ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
54 74        ygst_buf_append_str(out, fields->items[i].name);
55 74        ygst_buf_append_str(out, "\"\r\n\r\n");
56 74        ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : "");
57 74        ygst_buf_append_str(out, "\r\n");
58     }
59 35    ygst_buf_append_str(out, "--");
60 35    ygst_buf_append_str(out, boundary);
61 35    ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
62 35    ygst_buf_append_str(out, file_field);
63 35    ygst_buf_append_str(out, "\"; filename=\"");
64 35    ygst_buf_append_str(out, filename);
65 35    ygst_buf_append_str(out, "\"\r\nContent-Type: ");
66 35    ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream");
67 35    ygst_buf_append_str(out, "\r\n\r\n");
68 35    if (file_len) ygst_buf_append(out, file, file_len);
69 35    ygst_buf_append_str(out, "\r\n--");
70 35    ygst_buf_append_str(out, boundary);
71 35    ygst_buf_append_str(out, "--\r\n");
72 35    return out->oom ? -1 : 0;
73 }
74 
75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */
76 42const char *ygst_audio_ext(const char *audio_type) {
77     static const char *const KNOWN[] = {"wav", "mp3", "ogg", "amr", "flv", "opus", "aac",
78                                         "spx", "speex", "silk", "slk", "m4a", "pcm"};
79     char t[16];
80     size_t i;
81 42    if (ygst_is_blank(audio_type)) return "wav";
82 40    ygst_trim_copy(audio_type, t, sizeof t);
83 40    if (t[0] == '.') memmove(t, t + 1, strlen(t));
84 97    for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) {
85 94        if (ygst_strcaseeq(t, KNOWN[i])) return strcmp(KNOWN[i], "pcm") == 0 ? "wav" : KNOWN[i];
86     }
87 3    if (ygst_strcaseeq(t, "wave")) return "wav";
88 2    if (ygst_strcaseeq(t, "mpeg")) return "mp3";
89 1    return "bin";
90 }
91 
92 44const char *ygst_audio_content_type(const char *ext) {
93 44    if (!ext) return "application/octet-stream";
94 43    if (ygst_strcaseeq(ext, "wav")) return "audio/wav";
95 9    if (ygst_strcaseeq(ext, "mp3")) return "audio/mpeg";
96 8    if (ygst_strcaseeq(ext, "ogg") || ygst_strcaseeq(ext, "spx") || ygst_strcaseeq(ext, "speex")) return "audio/ogg";
97 6    if (ygst_strcaseeq(ext, "opus")) return "audio/opus";
98 5    if (ygst_strcaseeq(ext, "amr")) return "audio/amr";
99 4    if (ygst_strcaseeq(ext, "aac")) return "audio/aac";
100 3    if (ygst_strcaseeq(ext, "m4a")) return "audio/mp4";
101 2    if (ygst_strcaseeq(ext, "flv")) return "video/x-flv";
102 1    return "application/octet-stream";
103 }
104 
105 4int ygst_audio_type_is_pcm(const char *audio_type) {
106     char t[16];
107 4    if (ygst_is_blank(audio_type)) return 1;
108 3    ygst_trim_copy(audio_type, t, sizeof t);
109 6    return ygst_strcaseeq(t, "wav") || ygst_strcaseeq(t, "pcm") || ygst_strcaseeq(t, "wave") ||
110 6           ygst_strcaseeq(t, ".wav") || ygst_strcaseeq(t, ".pcm");
111 }

Sources/STKouyuEngine/core/ygst_params.c 97.08%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * coreType resolution and the mapping from KYTestConfig properties to the form fields of
6  * POST /{coreType}. The table mirrors DESIGN 6.1 so that iOS and Android send the same fields.
7  */
8 #include "ygst_core.h"
9 
10 #include <math.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 
15 /* KYTestType in declaration order (KYTestConfig.h). NULL: Shengtong publishes no string. */
16 static const char *const KY_TEST_TYPES[] = {
17     "word.eval",     /* KYTestType_Word */
18     "sent.eval",     /* KYTestType_Sentence */
19     "para.eval",     /* KYTestType_Paragraph */
20     "open.eval",     /* KYTestType_Open */
21     "choice.rec",    /* KYTestType_Choice */
22     "asr.rec",       /* KYTestType_Asr */
23     "align.eval",    /* KYTestType_Align */
24     "word.eval.pro", /* KYTestType_Word_Pro */
25     "sent.eval.pro", /* KYTestType_Sentence_Pro */
26     "word.eval.cn",  /* KYTestType_Word_Cn */
27     "sent.eval.cn",  /* KYTestType_Sentence_Cn */
28     "para.eval.cn",  /* KYTestType_Paragraph_Cn */
29     "asr.eval",      /* KYTestType_AsrEval */
30     "word.eval.fr",  /* KYTestType_Word_fr */
31     "sent.eval.fr",  /* KYTestType_Sentence_fr */
32     "para.eval.fr",  /* KYTestType_Paragraph_fr */
33     "word.eval.kr",  /* KYTestType_Word_kr */
34     "sent.eval.kr",  /* KYTestType_Sentence_kr */
35     "para.eval.kr",  /* KYTestType_Paragraph_kr */
36     "word.eval.jp",  /* KYTestType_Word_jp */
37     "sent.eval.jp",  /* KYTestType_Sentence_jp */
38     "para.eval.jp",  /* KYTestType_Paragraph_jp */
39     NULL             /* KYTestType_Wordspell */
40 };
41 
42 /* coreTypes the platform evaluates on POST /{coreType} (DESIGN 6.1). */
43 static const char *const SUPPORTED[] = {"word.eval",    "word.eval.pro", "sent.eval",    "sent.eval.pro",
44                                         "para.eval",    "word.eval.cn",  "sent.eval.cn", "para.eval.cn",
45                                         "alpha.eval",   "pinyin"};
46 
47 /* Shengtong coreTypes that are answered with errId 60003 without network I/O. */
48 static const char *const UNSUPPORTED[] = {"choice.rec",   "open.eval",    "asr.rec",      "asr.eval",
49                                           "align.eval",   "word.eval.fr", "sent.eval.fr", "para.eval.fr",
50                                           "word.eval.jp", "sent.eval.jp", "para.eval.jp", "word.eval.kr",
51                                           "sent.eval.kr", "para.eval.kr"};
52 
53 54const char *ygst_coretype_for_enum(unsigned long kytesttype) {
54 54    if (kytesttype >= sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]) return NULL;
55 51    return KY_TEST_TYPES[kytesttype];
56 }
57 
58 25size_t ygst_coretype_enum_count(void) {
59 25    return sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0];
60 }
61 
62 96ygst_ct_status ygst_coretype_check(const char *core_type) {
63     size_t i;
64 96    if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN;
65 744    for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) {
66 709        if (strcmp(core_type, SUPPORTED[i]) == 0) return YGST_CT_SUPPORTED;
67     }
68 279    for (i = 0; i < sizeof UNSUPPORTED / sizeof UNSUPPORTED[0]; i++) {
69 275        if (strcmp(core_type, UNSUPPORTED[i]) == 0) return YGST_CT_UNSUPPORTED;
70     }
71 4    return YGST_CT_UNKNOWN;
72 }
73 
74 50ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) {
75     const char *mapped;
76 50    if (!out || cap == 0) return YGST_CT_UNKNOWN;
77 49    out[0] = 0;
78 49    if (!ygst_is_blank(core_type_ns)) {
79 43        ygst_trim_copy(core_type_ns, out, cap);
80 43        return ygst_coretype_check(out);
81     }
82 6    mapped = ygst_coretype_for_enum(kytesttype);
83 6    if (!mapped) return YGST_CT_UNSUPPORTED;
84 4    if (strlen(mapped) >= cap) return YGST_CT_UNKNOWN;
85 3    strcpy(out, mapped);
86 3    return ygst_coretype_check(out);
87 }
88 
89 9int ygst_coretype_is_chinese(const char *core_type) {
90 9    return core_type && (ygst_has_suffix_ci(core_type, ".cn") || strcmp(core_type, "pinyin") == 0);
91 }
92 
93 148int ygst_coretype_is_paragraph(const char *core_type) {
94 148    return core_type && strncmp(core_type, "para.", 5) == 0;
95 }
96 
97 74int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) {
98 74    if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1");
99 68    return 0;
100 }
101 
102 48int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text,
103                           const char *ref_pinyin) {
104 48    if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED;
105 43    if (!ygst_is_blank(ref_text)) return 0;
106 7    if (!ygst_is_blank(ref_pinyin) && ygst_coretype_is_chinese(core_type)) return 0;
107 4    return YGST_ERRID_REFTEXT_EMPTY;
108 }
109 
110 /* ------------------------------------------------------------------ number text */
111 
112 69static void fix_decimal_point(char *s) {
113 431    for (; *s; s++) {
114 362        if (*s == ',') *s = '.';
115     }
116 69}
117 
118 3static void trim_fraction_zeros(char *s) {
119 3    char *dot = strchr(s, '.');
120     char *end;
121 3    if (!dot) return;
122 3    end = s + strlen(s);
123 3    while (end > dot + 1 && end[-1] == '0') end--;
124 3    if (end == dot + 1) end = dot;
125 3    *end = 0;
126 }
127 
128 28int ygst_fmt_double(double v, char *out, size_t cap) {
129     char tmp[64];
130     int p;
131 28    if (!out || cap == 0) return -1;
132 27    out[0] = 0;
133 27    if (v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308) return -1; /* NaN, inf */
134 25    if (v == 0) {
135 2        tmp[0] = '0';
136 2        tmp[1] = 0;
137 23    } else if (fabs(v) < 9.0e15 && v == floor(v)) {
138 11        snprintf(tmp, sizeof tmp, "%.0f", v);
139 12    } else if (fabs(v) >= 9.0e15) {
140 1        snprintf(tmp, sizeof tmp, "%.0f", v);
141     } else {
142 41        for (p = 1; p <= 17; p++) {
143 41            char *end = NULL;
144             double back;
145 41            snprintf(tmp, sizeof tmp, "%.*g", p, v);
146 41            fix_decimal_point(tmp);
147 41            back = strtod(tmp, &end);
148 41            if (end && *end) {
149                 /* decimal comma locale: compare through the locale's own separator */
150                 char alt[64];
151                 char *dot;
152 0                strcpy(alt, tmp);
153 0                dot = strchr(alt, '.');
154 0                if (dot) *dot = ',';
155 0                back = strtod(alt, NULL);
156             }
157 41            if (back == v) break;
158         }
159 11        if (strchr(tmp, 'e') || strchr(tmp, 'E')) {
160             /* plain decimal: enough fraction digits to keep the significant ones */
161 3            int exp10 = (int)floor(log10(fabs(v)));
162 3            int digits = (p > 17 ? 17 : p) - 1 - exp10;
163 3            if (digits < 0) digits = 0;
164 3            if (digits > 40) digits = 40;
165 3            snprintf(tmp, sizeof tmp, "%.*f", digits, v);
166 3            fix_decimal_point(tmp);
167 3            trim_fraction_zeros(tmp);
168         }
169     }
170 25    fix_decimal_point(tmp);
171 25    if (strcmp(tmp, "-0") == 0) strcpy(tmp, "0");
172 25    if (strlen(tmp) >= cap) return -1;
173 24    strcpy(out, tmp);
174 24    return 0;
175 }
176 
177 166int ygst_field_name_ok(const char *name) {
178 166    if (!name || !name[0]) return 0;
179 1599    for (; *name; name++) {
180 1440        if (*name == '"' || *name == '\r' || *name == '\n') return 0;
181     }
182 159    return 1;
183 }
184 
185 /* ------------------------------------------------------------------ mapping */
186 
187 68void ygst_test_params_init(ygst_test_params *p) {
188 68    memset(p, 0, sizeof *p);
189 68}
190 
191 723static int set_str(ygst_fields *f, const char *name, const char *v) {
192 723    if (!v || !v[0]) return 0;
193 52    return ygst_fields_set(f, name, v);
194 }
195 
196 504static int set_flag(ygst_fields *f, const char *name, int on) {
197 504    return on ? ygst_fields_set(f, name, "1") : 0;
198 }
199 
200 361static int set_num(ygst_fields *f, const char *name, double v) {
201     char tmp[64];
202 361    if (v == 0 || ygst_fmt_double(v, tmp, sizeof tmp) != 0) return 0;
203 12    return ygst_fields_set(f, name, tmp);
204 }
205 
206 3static int set_int(ygst_fields *f, const char *name, long v) {
207     char tmp[32];
208 3    snprintf(tmp, sizeof tmp, "%ld", v);
209 3    return ygst_fields_set(f, name, tmp);
210 }
211 
212 72int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) {
213     static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"};
214 72    int rc = 0;
215 72    rc |= set_str(out, "refText", p->ref_text);
216 72    rc |= set_str(out, "refPinyin", p->ref_pinyin);
217 72    if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group);
218 72    rc |= set_num(out, "scale", p->scale);
219 72    rc |= set_num(out, "precision", p->precision);
220 72    rc |= set_num(out, "slack", p->slack);
221 72    rc |= set_flag(out, "paragraph_need_word_score",
222 72                   p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type));
223 72    rc |= set_flag(out, "phoneme_output", p->phoneme_output);
224 72    rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url);
225 72    if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]);
226 72    rc |= set_str(out, "dict_dialect", p->dict_dialect);
227 72    rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json);
228 72    rc |= set_str(out, "customized_pron", p->customized_pron_json);
229 72    if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis);
230 72    rc |= set_flag(out, "output_rawtext", p->output_rawtext);
231 72    rc |= set_flag(out, "punctuate", p->punctuate);
232 72    rc |= set_num(out, "itn", p->itn);
233 72    rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable);
234     /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */
235 72    rc |= set_flag(out, "vad_detction", p->vad_detection);
236 72    rc |= set_str(out, "keywords", p->keywords);
237 72    rc |= set_str(out, "keypoints", p->keypoints_json);
238 72    rc |= set_num(out, "keypoints_weight", p->keypoints_weight);
239 72    rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json);
240 72    rc |= set_str(out, "negativeReftext", p->negative_reftext);
241 72    if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home");
242 72    if (p->q_type != 0) rc |= set_int(out, "qType", p->q_type);
243     /* customParams in the order of the Android RecordSetting newParams: a name that is already
244      * mapped keeps its position and takes the new value; request comes last */
245 72    if (p->custom_params) {
246         size_t i;
247 45        for (i = 0; i < p->custom_params->count; i++) {
248 9            const ygst_field *cp = &p->custom_params->items[i];
249 9            if (!ygst_field_name_ok(cp->name) || !cp->value || !cp->value[0]) continue;
250 6            rc |= ygst_fields_set(out, cp->name, cp->value);
251         }
252     }
253 72    rc |= set_str(out, "request", p->request_json);
254     /* customParams cannot switch the paragraph word details off */
255 72    rc |= ygst_fields_apply_coretype_rules(out, p->core_type);
256 72    return (rc != 0 || out->oom) ? -1 : 0;
257 }

Sources/STKouyuEngine/core/ygst_retry.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Retry policy (DESIGN 2.3), Shengtong errId mapping (DESIGN 6.2), response classification and
6  * the per-call controller that decides between delivering, retrying and failing. The Objective-C
7  * layer executes the decisions with NSURLSession; the Linux tests execute them with sockets.
8  */
9 #include "ygst_core.h"
10 
11 #include <math.h>
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 
16 /* ------------------------------------------------------------------ table lookups */
17 
18 245static const ygst_error_entry *find_in(const ygst_error_entry *t, size_t n, int code) {
19     size_t i;
20 3605    for (i = 0; i < n; i++) {
21 3591        if (t[i].code == code) return &t[i];
22     }
23 14    return NULL;
24 }
25 
26 183const ygst_error_entry *ygst_error_find(int code) {
27 183    return find_in(ygst_table_errors, ygst_table_errors_count, code);
28 }
29 
30 9const ygst_error_entry *ygst_warning_find(int code) {
31 9    return find_in(ygst_table_warnings, ygst_table_warnings_count, code);
32 }
33 
34 53const ygst_error_entry *ygst_local_find(int code) {
35 53    return find_in(ygst_table_local, ygst_table_local_count, code);
36 }
37 
38 31const char *ygst_compat_message(int err_id) {
39     size_t i;
40 170    for (i = 0; i < ygst_table_compat_count; i++) {
41 164        if (ygst_table_compat[i].err_id == err_id) return ygst_table_compat[i].message;
42     }
43 6    return NULL;
44 }
45 
46 44int ygst_http_retryable(int status) {
47     size_t i;
48 277    for (i = 0; i < ygst_table_retryable_http_count; i++) {
49 254        if (ygst_table_retryable_http[i] == status) return 1;
50     }
51 23    return 0;
52 }
53 
54 /* ------------------------------------------------------------------ RNG (splitmix64) */
55 
56 49void ygst_rng_seed(ygst_rng *r, uint64_t seed) {
57 49    r->s = seed;
58 49}
59 
60 1057uint64_t ygst_rng_next(ygst_rng *r) {
61 1057    uint64_t z = (r->s += 0x9E3779B97F4A7C15ull);
62 1057    z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
63 1057    z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
64 1057    return z ^ (z >> 31);
65 }
66 
67 1055double ygst_rng_uniform(ygst_rng *r) {
68 1055    return (double)(ygst_rng_next(r) >> 11) * (1.0 / 9007199254740992.0);
69 }
70 
71 /* ------------------------------------------------------------------ policy */
72 
73 42void ygst_retry_policy_default(ygst_retry_policy *p) {
74 42    memset(p, 0, sizeof *p);
75 42    p->max_retries = 2;
76 42    p->initial_delay_ms = 200;
77 42    p->multiplier = 2.0;
78 42    p->max_delay_ms = 4000;
79 42    p->jitter = 0.3;
80 42    p->respect_retry_after = 1;
81 42    p->max_retry_after_ms = 30000;
82 42    p->total_timeout_ms = 300000;
83 42    p->auto_retry = 0;
84 42    p->max_auto_retries = 2;
85 42    p->err_ids[0] = YGST_ERRID_RETRYABLE;
86 42    p->n_err_ids = 1;
87 42}
88 
89 28int ygst_retry_policy_add_err_id(ygst_retry_policy *p, long long err_id) {
90 28    if (err_id <= 0 || err_id > 2147483647LL) return -1;
91 25    if (ygst_retry_policy_has_err_id(p, (int)err_id)) return 0;
92 24    if (p->n_err_ids >= YGST_MAX_ERR_IDS) return -1;
93 17    p->err_ids[p->n_err_ids++] = (int)err_id;
94 17    return 0;
95 }
96 
97 38int ygst_retry_policy_has_err_id(const ygst_retry_policy *p, int err_id) {
98     int i;
99 278    for (i = 0; i < p->n_err_ids; i++) {
100 251        if (p->err_ids[i] == err_id) return 1;
101     }
102 27    return 0;
103 }
104 
105 64long ygst_backoff_delay_ms(const ygst_retry_policy *p, int n, ygst_rng *rng) {
106     double base;
107 64    double factor = 1.0;
108     double d;
109 64    if (n < 1) n = 1;
110 64    base = (double)p->initial_delay_ms * pow(p->multiplier > 0 ? p->multiplier : 1.0, (double)(n - 1));
111 64    if (base > p->max_delay_ms) base = p->max_delay_ms;
112 64    if (p->jitter > 0 && rng) factor = 1.0 + (ygst_rng_uniform(rng) * 2.0 - 1.0) * p->jitter;
113 64    d = base * factor;
114 64    if (d < 0) d = 0;
115 64    return (long)(d + 0.5);
116 }
117 
118 /* days since 1970-01-01 for a proleptic Gregorian date (H. Hinnant, days_from_civil) */
119 3static long long days_from_civil(long long y, unsigned m, unsigned d) {
120     long long era;
121     unsigned yoe, doy, doe;
122 3    y -= m <= 2;
123 3    era = (y >= 0 ? y : y - 399) / 400;
124 3    yoe = (unsigned)(y - era * 400);
125 3    doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
126 3    doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
127 3    return era * 146097 + (long long)doe - 719468;
128 }
129 
130 15int ygst_parse_retry_after(const char *value, int64_t now_ms, long *out_ms) {
131     static const char *const MON[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
132                                       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
133     char v[64];
134     size_t n;
135     size_t i;
136 15    if (!value || !out_ms) return -1;
137 14    n = ygst_trim_copy(value, v, sizeof v);
138 14    if (n == 0) return -1;
139 30    for (i = 0; i < n && v[i] >= '0' && v[i] <= '9'; i++) {
140     }
141 13    if (i == n) {
142 6        long secs = 0;
143 21        for (i = 0; i < n; i++) {
144 16            if (secs > 100000000L) return -1;
145 15            secs = secs * 10 + (v[i] - '0');
146         }
147 5        *out_ms = secs * 1000L;
148 5        return 0;
149     }
150     /* IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT" */
151 7    if (n == 29 && v[3] == ',' && v[4] == ' ' && v[7] == ' ' && v[11] == ' ' && v[16] == ' ' && v[19] == ':' &&
152 5        v[22] == ':' && v[25] == ' ' && strcmp(v + 26, "GMT") == 0) {
153 5        unsigned day, mon = 0, hh, mm, ss;
154         long long year;
155         long long t;
156 65        for (i = 0; i < 12; i++) {
157 60            if (strncmp(v + 8, MON[i], 3) == 0) mon = (unsigned)i + 1;
158         }
159 5        if (!mon) return -1;
160 4        day = (unsigned)((v[5] - '0') * 10 + (v[6] - '0'));
161 4        year = (v[12] - '0') * 1000 + (v[13] - '0') * 100 + (v[14] - '0') * 10 + (v[15] - '0');
162 4        hh = (unsigned)((v[17] - '0') * 10 + (v[18] - '0'));
163 4        mm = (unsigned)((v[20] - '0') * 10 + (v[21] - '0'));
164 4        ss = (unsigned)((v[23] - '0') * 10 + (v[24] - '0'));
165 4        if (day < 1 || day > 31 || hh > 23 || mm > 59 || ss > 60) return -1;
166 3        t = days_from_civil(year, mon, day) * 86400LL + hh * 3600LL + mm * 60LL + ss;
167 3        t = t * 1000LL - now_ms;
168 3        *out_ms = t > 0 ? (long)(t > 2000000000LL ? 2000000000LL : t) : 0;
169 3        return 0;
170     }
171 2    return -1;
172 }
173 
174 /* ------------------------------------------------------------------ retry and errId rules */
175 
176 207int ygst_is_retryable(int local_code, int biz_code, int http_status) {
177     const ygst_error_entry *e;
178 207    if (local_code) {
179 36        return local_code == YGST_LOCAL_NETWORK || local_code == YGST_LOCAL_TIMEOUT ||
180                local_code == YGST_LOCAL_RESULT_TIMEOUT;
181     }
182 171    if (biz_code) {
183 139        e = ygst_error_find(biz_code);
184 139        if (e) return e->retryable;
185     }
186 36    return ygst_http_retryable(http_status);
187 }
188 
189 22int ygst_http_fallback_code(int status) {
190 22    const char *cat = ygst_http_fallback_category(status);
191 22    if (strcmp(cat, "AUTH") == 0) return 40100;
192 20    if (strcmp(cat, "PERMISSION") == 0) return 40300;
193 19    if (strcmp(cat, "NOT_FOUND") == 0) return 40400;
194 17    if (strcmp(cat, "CONFLICT") == 0) return 40900;
195 16    if (strcmp(cat, "RATE_LIMIT") == 0) return 42900;
196 15    if (strcmp(cat, "SERVER") == 0) return status == 501 ? 50010 : 50000;
197 12    if (strcmp(cat, "UPSTREAM") == 0) return 50200;
198 10    if (strcmp(cat, "TIMEOUT") == 0) return YGST_ERRID_RETRYABLE;
199 9    if (strcmp(cat, "INVALID_PARAM") == 0) return 40001;
200 3    return YGST_LOCAL_PROTOCOL;
201 }
202 
203 175int ygst_errid_for(int local_code, int biz_code, int http_status, int retryable) {
204 175    if (local_code >= 60001 && local_code <= 60009) return local_code;
205 164    if (retryable) return YGST_ERRID_RETRYABLE;
206 94    if (biz_code) return biz_code;
207 42    if (local_code) return local_code;
208 22    return ygst_http_fallback_code(http_status);
209 }
210 
211 /* ------------------------------------------------------------------ classification */
212 
213 155void ygst_classification_init(ygst_classification *c) {
214 155    memset(c, 0, sizeof *c);
215 155    ygst_buf_init(&c->record_id);
216 155    ygst_buf_init(&c->audio_url);
217 155    ygst_buf_init(&c->message);
218 155}
219 
220 155void ygst_classification_free(ygst_classification *c) {
221 155    ygst_buf_free(&c->record_id);
222 155    ygst_buf_free(&c->audio_url);
223 155    ygst_buf_free(&c->message);
224 155}
225 
226 120static int member_int(ygst_span obj, const char *key, long long *out) {
227     ygst_span v;
228 120    if (ygst_json_member(obj, key, &v) != 1) return 0;
229 97    if (ygst_json_type(v) == 's') {
230         /* some gateways quote numeric codes */
231         ygst_buf b;
232 1        int ok = 0;
233 1        ygst_buf_init(&b);
234 1        if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 12) {
235 1            char *end = NULL;
236 1            long long x = strtoll(ygst_buf_cstr(&b), &end, 10);
237 1            if (end && *end == 0) {
238 1                *out = x;
239 1                ok = 1;
240             }
241         }
242 1        ygst_buf_free(&b);
243 1        return ok;
244     }
245 96    return ygst_json_int(v, out) == 0;
246 }
247 
248 /* First of the keys whose value is a JSON string. */
249 108static int first_string(ygst_span obj, const char *const *keys, ygst_buf *out) {
250 140    for (; *keys; keys++) {
251         ygst_span v;
252 131        if (ygst_json_member(obj, *keys, &v) == 1 && ygst_json_type(v) == 's') {
253 99            ygst_buf_reset(out);
254 99            return ygst_json_string(v, out) == 0;
255         }
256     }
257 9    return 0;
258 }
259 
260 /* "[2001] signature mismatch": the compat engine code in a FastAPI detail, 3 to 6 digits. */
261 7static int bracket_code(const char *msg) {
262 7    long v = 0;
263 7    int digits = 0;
264 7    if (!msg) return 0;
265 8    while (*msg == ' ' || *msg == '\t' || *msg == '\n' || *msg == '\r') msg++;
266 7    if (*msg != '[') return 0;
267 3    msg++;
268 14    while (*msg >= '0' && *msg <= '9' && digits < 7) {
269 11        v = v * 10 + (*msg - '0');
270 11        msg++;
271 11        digits++;
272     }
273 3    if (*msg != ']' || digits < 3 || digits > 6) return 0;
274 2    return (int)v;
275 }
276 
277 42static void set_message(ygst_buf *m, const char *s) {
278 42    ygst_buf_reset(m);
279 42    ygst_buf_append_str(m, s ? s : "");
280 42    ygst_buf_cstr(m);
281 42}
282 
283 187int ygst_classify(const ygst_outcome *o, ygst_classification *c) {
284     static const char *const MESSAGE_KEYS[] = {"message", "msg", "error", NULL};
285     ygst_span root;
286 187    int parsed = 0;
287 187    long long code = 0;
288 187    c->success = 0;
289 187    c->http_status = o->http_status;
290 187    c->local_code = o->local_code;
291 187    c->biz_code = 0;
292 187    c->has_audio_url = 0;
293 187    c->has_record_id = 0;
294 187    c->record_id_raw.p = NULL;
295 187    c->record_id_raw.n = 0;
296 187    c->result.p = NULL;
297 187    c->result.n = 0;
298 187    ygst_buf_reset(&c->record_id);
299 187    ygst_buf_reset(&c->audio_url);
300 187    ygst_buf_reset(&c->message);
301 
302 187    if (o->local_code || o->http_status <= 0) {
303         const ygst_error_entry *e;
304 29        if (!c->local_code) c->local_code = YGST_LOCAL_NETWORK;
305 29        e = ygst_local_find(c->local_code);
306 51        set_message(&c->message, (o->local_message && o->local_message[0]) ? o->local_message
307 22                                                                            : (e ? e->message : "network error"));
308 29        c->retryable = ygst_is_retryable(c->local_code, 0, 0);
309 29        c->err_id = ygst_errid_for(c->local_code, 0, 0, c->retryable);
310 29        return 0;
311     }
312 
313 158    if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o';
314 
315 158    if (o->http_status >= 200 && o->http_status < 300) {
316         ygst_span result, v;
317 47        if (parsed && ygst_json_member(root, "result", &result) == 1) {
318             ygst_span inner;
319 45            c->success = 1;
320 45            c->result = result;
321 45            if (ygst_json_member(root, "recordId", &v) == 1) {
322 42                c->has_record_id = 1;
323 42                c->record_id_raw = v;
324 42                if (ygst_json_type(v) == 's') {
325 41                    ygst_json_string(v, &c->record_id);
326                 } else {
327 1                    ygst_buf_append(&c->record_id, v.p, v.n);
328                 }
329             }
330 45            if (ygst_json_member(root, "audioUrl", &v) == 1) {
331 5                if (ygst_json_type(v) == 's') c->has_audio_url = ygst_json_string(v, &c->audio_url) == 0;
332 42            } else if (ygst_json_type(result) == 'o' && ygst_json_member(result, "audioUrl", &inner) == 1 &&
333 2                       ygst_json_type(inner) == 's') {
334 1                c->has_audio_url = ygst_json_string(inner, &c->audio_url) == 0;
335             }
336 45            if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0;
337 45            ygst_buf_cstr(&c->record_id);
338 45            ygst_buf_cstr(&c->audio_url);
339 45            c->retryable = 0;
340 45            c->err_id = 0;
341 45            return 0;
342         }
343 2        c->local_code = YGST_LOCAL_PROTOCOL;
344 2        set_message(&c->message, parsed ? "invalid platform response: platform response has no result"
345                                         : "invalid platform response: not a JSON object");
346 2        c->retryable = 0;
347 2        c->err_id = YGST_LOCAL_PROTOCOL;
348 2        return 0;
349     }
350 
351 111    if (parsed) {
352         ygst_span detail;
353 108        if (member_int(root, "code", &code) && code != 0) c->biz_code = (int)code;
354 108        if (!c->biz_code && member_int(root, "errId", &code) && code != 0) c->biz_code = (int)code;
355 108        first_string(root, MESSAGE_KEYS, &c->message);
356 108        if (ygst_json_member(root, "detail", &detail) == 1) {
357 9            if (ygst_json_type(detail) == 's') {
358                 ygst_buf d;
359 7                ygst_buf_init(&d);
360 7                ygst_json_string(detail, &d);
361 7                if (!c->biz_code) c->biz_code = bracket_code(ygst_buf_cstr(&d));
362 7                if (!c->message.len) set_message(&c->message, ygst_buf_cstr(&d));
363 7                ygst_buf_free(&d);
364 2            } else if (!c->message.len) {
365 1                ygst_buf_reset(&c->message);
366 1                ygst_buf_append(&c->message, detail.p, detail.n > 300 ? 300 : detail.n);
367             }
368         }
369     }
370 111    if (!c->message.len) {
371         char tmp[32];
372 4        snprintf(tmp, sizeof tmp, "HTTP %d", o->http_status);
373 4        set_message(&c->message, tmp);
374     }
375 111    ygst_buf_cstr(&c->message);
376 111    c->retryable = ygst_is_retryable(0, c->biz_code, o->http_status);
377 111    c->err_id = ygst_errid_for(0, c->biz_code, o->http_status, c->retryable);
378 111    return 0;
379 }
380 
381 17int ygst_error_message(const ygst_classification *c, int err_id, int attempts, ygst_buf *out) {
382 17    const char *m = c->message.data ? (const char *)c->message.data : "";
383 17    if (err_id == YGST_ERRID_RETRYABLE) {
384 6        const char *base = ygst_compat_message(YGST_ERRID_RETRYABLE);
385 6        ygst_buf_append_str(out, base ? base : "");
386 6        ygst_buf_append_str(out, ": ");
387 6        if (c->http_status > 0) {
388 4            ygst_buf_append_str(out, "HTTP ");
389 4            ygst_buf_append_int(out, c->http_status);
390 4            if (c->biz_code) {
391 2                ygst_buf_append_str(out, " code=");
392 2                ygst_buf_append_int(out, c->biz_code);
393             }
394         } else {
395 2            const ygst_error_entry *e = ygst_local_find(c->local_code);
396 2            if (e) {
397 1                ygst_buf_append_str(out, e->name);
398             } else {
399 1                ygst_buf_append_str(out, "code=");
400 1                ygst_buf_append_int(out, c->local_code);
401             }
402         }
403 6        if (m[0]) {
404 6            ygst_buf_append_char(out, ' ');
405 6            ygst_buf_append_str(out, m);
406         }
407 6        if (attempts > 0) {
408 5            ygst_buf_append_str(out, " (attempts ");
409 5            ygst_buf_append_int(out, attempts);
410 5            ygst_buf_append_char(out, ')');
411         }
412 11    } else if (m[0]) {
413 7        ygst_buf_append_str(out, m);
414     } else {
415 4        const char *cm = ygst_compat_message(err_id);
416 4        const ygst_error_entry *e = cm ? NULL : ygst_error_find(err_id);
417 4        if (!cm && !e) e = ygst_local_find(err_id);
418 4        ygst_buf_append_str(out, cm ? cm : (e ? e->message : ""));
419     }
420 17    ygst_buf_cstr(out);
421 17    return out->oom ? -1 : 0;
422 }
423 
424 /* ------------------------------------------------------------------ controller */
425 
426 45void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) {
427 45    memset(c, 0, sizeof *c);
428 45    if (p) {
429 44        c->policy = *p;
430     } else {
431 1        ygst_retry_policy_default(&c->policy);
432     }
433 45    ygst_rng_seed(&c->rng, seed);
434 45    c->start_ms = now_ms;
435 45}
436 
437 43static long apply_retry_after(const ygst_retry_policy *p, long delay, long retry_after_ms) {
438 43    if (p->respect_retry_after && retry_after_ms >= 0) {
439 4        long ra = retry_after_ms > p->max_retry_after_ms ? p->max_retry_after_ms : retry_after_ms;
440 4        if (ra > delay) delay = ra;
441     }
442 43    return delay;
443 }
444 
445 84ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) {
446     ygst_action a;
447 84    int64_t deadline = c->start_ms + c->policy.total_timeout_ms;
448 84    a.kind = YGST_ACT_FAIL;
449 84    a.delay_ms = 0;
450 84    a.err_id = cls->err_id;
451 84    a.auto_retry = 0;
452 84    c->attempt++;
453 84    c->total_attempts++;
454 84    if (cls->success) {
455 28        a.kind = YGST_ACT_SUCCESS;
456 28        a.err_id = 0;
457 28        return a;
458     }
459 56    if (cls->retryable && c->attempt <= c->policy.max_retries) {
460 34        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->attempt, &c->rng), retry_after_ms);
461 34        if (now_ms + d <= deadline) {
462 33            a.kind = YGST_ACT_RETRY;
463 33            a.delay_ms = d;
464 33            return a;
465         }
466 1        return a;
467     }
468 33    if (c->policy.auto_retry && c->submission < c->policy.max_auto_retries &&
469 11        ygst_retry_policy_has_err_id(&c->policy, cls->err_id)) {
470 9        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->submission + 1, &c->rng),
471                                    retry_after_ms);
472 9        if (now_ms + d <= deadline) {
473 8            c->submission++;
474 8            c->attempt = 0;
475 8            a.kind = YGST_ACT_RETRY;
476 8            a.delay_ms = d;
477 8            a.auto_retry = 1;
478 8            return a;
479         }
480     }
481 14    return a;
482 }