STKouyuEngine C core line coverage

Total 98.76% (2154 of 2181 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.c251251100.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_realtime.c458458100.00%
Sources/STKouyuEngine/core/ygst_retry.c324324100.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 75static void put_le32(unsigned char *p, uint32_t v) {
14 75    p[0] = (unsigned char)v;
15 75    p[1] = (unsigned char)(v >> 8);
16 75    p[2] = (unsigned char)(v >> 16);
17 75    p[3] = (unsigned char)(v >> 24);
18 75}
19 
20 60static void put_le16(unsigned char *p, uint16_t v) {
21 60    p[0] = (unsigned char)v;
22 60    p[1] = (unsigned char)(v >> 8);
23 60}
24 
25 257static uint32_t le32(const unsigned char *p) {
26 257    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
27 }
28 
29 198static uint16_t le16(const unsigned char *p) {
30 198    return (uint16_t)(p[0] | (p[1] << 8));
31 }
32 
33 15void ygst_wav_header(unsigned char out[44], uint32_t data_len, int sample_rate, int channels, int bits) {
34 15    uint32_t block = (uint32_t)(channels * (bits / 8));
35 15    memcpy(out, "RIFF", 4);
36 15    put_le32(out + 4, data_len > 0xFFFFFFFFu - 36u ? 0xFFFFFFFFu : data_len + 36u);
37 15    memcpy(out + 8, "WAVE", 4);
38 15    memcpy(out + 12, "fmt ", 4);
39 15    put_le32(out + 16, 16);
40 15    put_le16(out + 20, 1);
41 15    put_le16(out + 22, (uint16_t)channels);
42 15    put_le32(out + 24, (uint32_t)sample_rate);
43 15    put_le32(out + 28, (uint32_t)sample_rate * block);
44 15    put_le16(out + 32, (uint16_t)block);
45 15    put_le16(out + 34, (uint16_t)bits);
46 15    memcpy(out + 36, "data", 4);
47 15    put_le32(out + 40, data_len);
48 15}
49 
50 75int ygst_is_riff_wave(const unsigned char *p, size_t n) {
51 75    return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0;
52 }
53 
54 73int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) {
55 73    size_t pos = 12;
56 73    int have_fmt = 0;
57 73    memset(info, 0, sizeof *info);
58 73    if (!ygst_is_riff_wave(p, n)) return -1;
59 191    while (pos + 8 <= n) {
60 191        uint32_t size = le32(p + pos + 4);
61 191        const unsigned char *id = p + pos;
62 191        size_t body = pos + 8;
63 191        if (memcmp(id, "fmt ", 4) == 0) {
64 67            if (size < 16 || body + 16 > n) return -1;
65 66            info->format = le16(p + body);
66 66            info->channels = le16(p + body + 2);
67 66            info->sample_rate = (int)le32(p + body + 4);
68 66            info->bits = le16(p + body + 14);
69 66            have_fmt = 1;
70 124        } else if (memcmp(id, "data", 4) == 0) {
71 67            if (!have_fmt) return -1;
72 66            info->data_offset = body;
73             /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */
74 66            info->data_len = (size == 0 || size > n - body) ? n - body : size;
75 66            return 0;
76         }
77 123        if (size > n - body) return -1;
78 122        pos = body + size + (size & 1u);
79     }
80 0    return -1;
81 }
82 
83 69long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) {
84     double per_second;
85 69    if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1;
86 67    per_second = (double)sample_rate * channels * (bits / 8.0);
87 67    if (per_second <= 0) return -1;
88 67    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 57static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) {
93 57    double per_second = (double)sample_rate * channels * (bits / 8.0);
94 57    if (per_second <= 0) return 0;
95 56    return (double)bytes * 1000.0 > (double)ms * per_second;
96 }
97 
98 21int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) {
99     long ms;
100 21    if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY;
101 20    if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE;
102 19    ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits);
103 19    if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
104 17    if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE;
105 15    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 4602void ygst_buf_init(ygst_buf *b) {
14 4602    b->data = NULL;
15 4602    b->len = 0;
16 4602    b->cap = 0;
17 4602    b->oom = 0;
18 4602}
19 
20 2301void ygst_buf_free(ygst_buf *b) {
21 2301    free(b->data);
22 2301    ygst_buf_init(b);
23 2301}
24 
25 3852void ygst_buf_reset(ygst_buf *b) {
26 3852    b->len = 0;
27 3852    b->oom = 0;
28 3852    if (b->data) b->data[0] = 0;
29 3852}
30 
31 20220int ygst_buf_reserve(ygst_buf *b, size_t extra) {
32     size_t need;
33     size_t cap;
34     unsigned char *p;
35 20220    if (b->oom) return -1;
36 20220    if (extra > (size_t)-1 - b->len - 1) {
37 0        b->oom = 1;
38 0        return -1;
39     }
40 20220    need = b->len + extra + 1; /* room for the terminating NUL */
41 20220    if (need <= b->cap) return 0;
42 2457    cap = b->cap ? b->cap : 64;
43 4950    while (cap < need) {
44 2493        if (cap > ((size_t)-1) / 2) {
45 0            cap = need;
46 0            break;
47         }
48 2493        cap *= 2;
49     }
50 2457    p = (unsigned char *)realloc(b->data, cap);
51 2457    if (!p) {
52 0        b->oom = 1;
53 0        return -1;
54     }
55 2457    b->data = p;
56 2457    b->cap = cap;
57 2457    return 0;
58 }
59 
60 20114int ygst_buf_append(ygst_buf *b, const void *p, size_t n) {
61 20114    if (n == 0) return b->oom ? -1 : 0;
62 20107    if (ygst_buf_reserve(b, n) != 0) return -1;
63 20107    memcpy(b->data + b->len, p, n);
64 20107    b->len += n;
65 20107    b->data[b->len] = 0;
66 20107    return 0;
67 }
68 
69 2617int ygst_buf_append_str(ygst_buf *b, const char *s) {
70 2617    return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
71 }
72 
73 10162int ygst_buf_append_char(ygst_buf *b, char c) {
74 10162    return ygst_buf_append(b, &c, 1);
75 }
76 
77 72int ygst_buf_append_int(ygst_buf *b, long long v) {
78     char tmp[32];
79 72    int n = snprintf(tmp, sizeof tmp, "%lld", v);
80 72    if (n < 0) return -1;
81 72    return ygst_buf_append(b, tmp, (size_t)n);
82 }
83 
84 9234const char *ygst_buf_cstr(ygst_buf *b) {
85 9234    if (!b->data) {
86 113        if (ygst_buf_reserve(b, 0) != 0) return "";
87 113        b->data[0] = 0;
88     }
89 9234    return (const char *)b->data;
90 }
91 
92 816char *ygst_strdup(const char *s) {
93     size_t n;
94     char *p;
95 816    if (!s) return NULL;
96 815    n = strlen(s);
97 815    p = (char *)malloc(n + 1);
98 815    if (!p) return NULL;
99 815    memcpy(p, s, n + 1);
100 815    return p;
101 }
102 
103 900static int lower_ascii(int c) {
104 900    return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
105 }
106 
107 227int ygst_strcaseeq(const char *a, const char *b) {
108 227    if (!a || !b) return 0;
109 553    while (*a && *b) {
110 450        if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0;
111 327        a++;
112 327        b++;
113     }
114 103    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 488static int is_space(unsigned char c) {
128 488    return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
129 }
130 
131 144size_t ygst_trim_copy(const char *s, char *out, size_t cap) {
132     const char *e;
133     size_t n;
134 144    if (!out || cap == 0) return 0;
135 144    out[0] = 0;
136 144    if (!s) return 0;
137 149    while (*s && is_space((unsigned char)*s)) s++;
138 142    e = s + strlen(s);
139 147    while (e > s && is_space((unsigned char)e[-1])) e--;
140 142    n = (size_t)(e - s);
141 142    if (n >= cap) n = cap - 1;
142 142    memcpy(out, s, n);
143 142    out[n] = 0;
144 142    return n;
145 }
146 
147 206int ygst_is_blank(const char *s) {
148 206    if (!s) return 1;
149 208    while (*s) {
150 196        if (!is_space((unsigned char)*s)) return 0;
151 11        s++;
152     }
153 12    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 16046static 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 272782    for (i = 0; i < 16; i++) {
29 256736        w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) |
30 256736               (uint32_t)p[i * 4 + 3];
31     }
32 786254    for (i = 16; i < 64; i++) {
33 770208        uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3);
34 770208        uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10);
35 770208        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
36     }
37 16046    a = c->h[0];
38 16046    b = c->h[1];
39 16046    cc = c->h[2];
40 16046    d = c->h[3];
41 16046    e = c->h[4];
42 16046    f = c->h[5];
43 16046    g = c->h[6];
44 16046    h = c->h[7];
45 1042990    for (i = 0; i < 64; i++) {
46 1026944        uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
47 1026944        uint32_t ch = (e & f) ^ ((~e) & g);
48 1026944        uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
49 1026944        uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
50 1026944        t1 = h + S1 + ch + K[i] + w[i];
51 1026944        t2 = S0 + maj;
52 1026944        h = g;
53 1026944        g = f;
54 1026944        f = e;
55 1026944        e = d + t1;
56 1026944        d = cc;
57 1026944        cc = b;
58 1026944        b = a;
59 1026944        a = t1 + t2;
60     }
61 16046    c->h[0] += a;
62 16046    c->h[1] += b;
63 16046    c->h[2] += cc;
64 16046    c->h[3] += d;
65 16046    c->h[4] += e;
66 16046    c->h[5] += f;
67 16046    c->h[6] += g;
68 16046    c->h[7] += h;
69 16046}
70 
71 177void ygst_sha256_init(ygst_sha256_ctx *c) {
72 177    c->h[0] = 0x6a09e667u;
73 177    c->h[1] = 0xbb67ae85u;
74 177    c->h[2] = 0x3c6ef372u;
75 177    c->h[3] = 0xa54ff53au;
76 177    c->h[4] = 0x510e527fu;
77 177    c->h[5] = 0x9b05688cu;
78 177    c->h[6] = 0x1f83d9abu;
79 177    c->h[7] = 0x5be0cd19u;
80 177    c->total = 0;
81 177    c->used = 0;
82 177}
83 
84 1339void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) {
85 1339    const unsigned char *p = (const unsigned char *)data;
86 1339    c->total += (uint64_t)n;
87 1339    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 16178    while (n >= 64) {
99 14851        sha256_block(c, p);
100 14851        p += 64;
101 14851        n -= 64;
102     }
103 1327    if (n) {
104 1167        memcpy(c->block, p, n);
105 1167        c->used = n;
106     }
107 }
108 
109 177void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) {
110 177    uint64_t bits = c->total * 8u;
111     int i;
112 177    c->block[c->used++] = 0x80;
113 177    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 177    memset(c->block + c->used, 0, 56 - c->used);
119 1593    for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i));
120 177    sha256_block(c, c->block);
121 1593    for (i = 0; i < 8; i++) {
122 1416        out[i * 4] = (unsigned char)(c->h[i] >> 24);
123 1416        out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16);
124 1416        out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8);
125 1416        out[i * 4 + 3] = (unsigned char)c->h[i];
126     }
127 177}
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 69void 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 69    memset(k, 0, sizeof k);
143 69    if (key_len > 64) {
144 1        ygst_sha256(key, key_len, k);
145 68    } else if (key_len) {
146 68        memcpy(k, key, key_len);
147     }
148 4485    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36);
149 69    ygst_sha256_init(&c);
150 69    ygst_sha256_update(&c, pad, 64);
151 69    ygst_sha256_update(&c, msg, msg_len);
152 69    ygst_sha256_final(&c, inner);
153 4485    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c);
154 69    ygst_sha256_init(&c);
155 69    ygst_sha256_update(&c, pad, 64);
156 69    ygst_sha256_update(&c, inner, 32);
157 69    ygst_sha256_final(&c, out);
158 69}
159 
160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
161 
162 91size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) {
163 91    size_t need = ((n + 2) / 3) * 4;
164     size_t i;
165 91    size_t o = 0;
166 91    if (!out || cap < need + 1) return 0;
167 834    for (i = 0; i + 2 < n; i += 3) {
168 745        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2];
169 745        out[o++] = B64[(v >> 18) & 63];
170 745        out[o++] = B64[(v >> 12) & 63];
171 745        out[o++] = B64[(v >> 6) & 63];
172 745        out[o++] = B64[v & 63];
173     }
174 89    if (n - i == 1) {
175 18        uint32_t v = (uint32_t)in[i] << 16;
176 18        out[o++] = B64[(v >> 18) & 63];
177 18        out[o++] = B64[(v >> 12) & 63];
178 18        out[o++] = '=';
179 18        out[o++] = '=';
180 71    } else if (n - i == 2) {
181 68        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
182 68        out[o++] = B64[(v >> 18) & 63];
183 68        out[o++] = B64[(v >> 12) & 63];
184 68        out[o++] = B64[(v >> 6) & 63];
185 68        out[o++] = '=';
186     }
187 89    out[o] = 0;
188 89    return o;
189 }
190 
191 168void ygst_hex_lower(const unsigned char *in, size_t n, char *out) {
192     static const char H[] = "0123456789abcdef";
193     size_t i;
194 2400    for (i = 0; i < n; i++) {
195 2232        out[i * 2] = H[in[i] >> 4];
196 2232        out[i * 2 + 1] = H[in[i] & 15];
197     }
198 168    out[n * 2] = 0;
199 168}

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, error and reading progress JSON (DESIGN 6.2, 14.3), the dtLastResponse
6  * clock text, the 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 90static 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 90    z += 719468;
18 90    era = (z >= 0 ? z : z - 146096) / 146097;
19 90    doe = (unsigned)(z - era * 146097);
20 90    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
21 90    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
22 90    mp = (5 * doy + 2) / 153;
23 90    *d = doy - (153 * mp + 2) / 5 + 1;
24 90    *m = mp < 10 ? mp + 3 : mp - 9;
25 90    *y = (long long)yoe + era * 400 + (*m <= 2);
26 90}
27 
28 90int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) {
29 90    long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL;
30 90    long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL);
31 90    long long rem = ms - days * 86400000LL;
32     long long y;
33     unsigned m, d, hh, mi, ss, mss;
34 90    civil_from_days(days, &y, &m, &d);
35 90    if (y < 0) y = 0;
36 90    if (y > 9999) y = 9999;
37 90    hh = (unsigned)(rem / 3600000LL) % 24u;
38 90    mi = (unsigned)(rem / 60000LL) % 60u;
39 90    ss = (unsigned)(rem / 1000LL) % 60u;
40 90    mss = (unsigned)rem % 1000u;
41 90    snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss,
42              mss);
43 90    return 0;
44 }
45 
46 52int ygst_align_result(ygst_span result, ygst_buf *out) {
47     ygst_span sentences, sent, details, d, scores, v, so, sp;
48 52    size_t spos = 0;
49 52    size_t copied = 0; /* bytes of result already copied to out */
50 52    int added = 0;
51 52    if (!result.p) return -1;
52 98    if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 ||
53 47        ygst_json_type(sentences) != 'a') {
54 5        return ygst_buf_append(out, result.p, result.n) == 0 ? 0 : -1;
55     }
56 97    while (ygst_json_array_next(sentences, &spos, &sent) == 1) {
57 51        size_t dpos = 0;
58 97        if (ygst_json_type(sent) != 'o' || ygst_json_member(sent, "details", &details) != 1 ||
59 46            ygst_json_type(details) != 'a') {
60 6            continue;
61         }
62 301        while (ygst_json_array_next(details, &dpos, &d) == 1) {
63             int add_overall, add_pron;
64             size_t close;
65 256            if (ygst_json_type(d) != 'o' || ygst_json_member(d, "overall", &v) == 1) continue;
66 254            if (ygst_json_member(d, "scores", &scores) != 1 || ygst_json_type(scores) != 'o') continue;
67 252            add_overall = ygst_json_member(scores, "overall", &so) == 1;
68 252            add_pron = ygst_json_member(d, "pronunciation", &v) != 1 && ygst_json_member(scores, "pronunciation", &sp) == 1;
69 252            if (!add_overall && !add_pron) continue;
70 251            close = (size_t)(d.p + d.n - 1 - result.p); /* the closing brace of the item */
71 251            ygst_buf_append(out, result.p + copied, close - copied);
72 251            if (add_overall) {
73 251                ygst_buf_append_str(out, ",\"overall\":");
74 251                ygst_buf_append(out, so.p, so.n);
75 251                added++;
76             }
77 251            if (add_pron) {
78 248                ygst_buf_append_str(out, ",\"pronunciation\":");
79 248                ygst_buf_append(out, sp.p, sp.n);
80 248                added++;
81             }
82 251            copied = close;
83         }
84     }
85 46    ygst_buf_append(out, result.p + copied, result.n - copied);
86 46    return out->oom ? -1 : added;
87 }
88 
89 897static void key(ygst_buf *out, const char *k, int first) {
90 897    if (!first) ygst_buf_append_char(out, ',');
91 897    ygst_json_escape(out, k);
92 897    ygst_buf_append_char(out, ':');
93 897}
94 
95 42int 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 42    if (!result_raw.p || result_raw.n == 0) return -1;
99 41    ygst_buf_append_char(out, '{');
100 41    key(out, "tokenId", 1);
101 41    ygst_json_escape(out, token_id);
102 41    if (record_id_json && record_id_json[0]) {
103 40        key(out, "recordId", 0);
104 40        ygst_buf_append_str(out, record_id_json);
105     }
106 41    key(out, "applicationId", 0);
107 41    ygst_json_escape(out, app_key);
108 41    key(out, "userId", 0);
109 41    ygst_json_escape(out, user_id);
110 41    key(out, "refText", 0);
111 41    ygst_json_escape(out, ref_text);
112 41    key(out, "eof", 0);
113 41    ygst_buf_append_char(out, '1');
114 41    key(out, "dtLastResponse", 0);
115 41    ygst_json_escape(out, dt_last_response);
116 41    key(out, "result", 0);
117 41    if (ygst_align_result(result_raw, out) < 0) return -1;
118 41    if (params_json && params_json[0]) {
119 2        key(out, "params", 0);
120 2        ygst_buf_append_str(out, params_json);
121     }
122 41    if (audio_url && audio_url[0]) {
123 3        key(out, "audioUrl", 0);
124 3        ygst_json_escape(out, audio_url);
125     }
126 41    ygst_buf_append_char(out, '}');
127 41    ygst_buf_cstr(out);
128 41    return out->oom ? -1 : 0;
129 }
130 
131 20int ygst_envelope_error(ygst_buf *out, const char *token_id, int err_id, const char *message,
132                         const char *app_key) {
133 20    ygst_buf_append_char(out, '{');
134 20    key(out, "tokenId", 1);
135 20    ygst_json_escape(out, token_id);
136 20    key(out, "errId", 0);
137 20    ygst_buf_append_int(out, err_id);
138 20    key(out, "error", 0);
139 20    ygst_json_escape(out, message);
140 20    key(out, "eof", 0);
141 20    ygst_buf_append_char(out, '1');
142 20    key(out, "applicationId", 0);
143 20    ygst_json_escape(out, app_key);
144 20    ygst_buf_append_char(out, '}');
145 20    ygst_buf_cstr(out);
146 20    return out->oom ? -1 : 0;
147 }
148 
149 /* The progress result as the platform sent it, without the first skip items of "words". */
150 54static int append_progress_result(ygst_buf *out, ygst_span result, int skip) {
151     ygst_span words, item;
152 54    size_t pos = 0;
153 54    int i = 0;
154 54    int first = 1;
155 56    if (skip <= 0 || ygst_json_type(result) != 'o' || ygst_json_member(result, "words", &words) != 1 ||
156 2        ygst_json_type(words) != 'a') {
157 52        return ygst_buf_append(out, result.p, result.n);
158     }
159 2    ygst_buf_append(out, result.p, (size_t)(words.p - result.p));
160 2    ygst_buf_append_char(out, '[');
161 8    while (ygst_json_array_next(words, &pos, &item) == 1) {
162 6        if (i++ < skip) continue;
163 1        if (!first) ygst_buf_append_char(out, ',');
164 1        first = 0;
165 1        ygst_buf_append(out, item.p, item.n);
166     }
167 2    ygst_buf_append_char(out, ']');
168 2    ygst_buf_append(out, words.p + words.n, (size_t)(result.p + result.n - (words.p + words.n)));
169 2    return out->oom ? -1 : 0;
170 }
171 
172 56int ygst_envelope_progress(ygst_buf *out, const char *token_id, const char *app_key, const char *user_id,
173                            const char *ref_text, const char *dt_last_response, ygst_span result_raw,
174                            int skip_words) {
175 56    if (!result_raw.p || result_raw.n == 0) return -1;
176 54    ygst_buf_append_char(out, '{');
177 54    key(out, "tokenId", 1);
178 54    ygst_json_escape(out, token_id);
179     /* Shengtong's intermediate results carry recordId; the platform names the record only in the final */
180 54    key(out, "recordId", 0);
181 54    ygst_buf_append_str(out, "\"\"");
182 54    key(out, "applicationId", 0);
183 54    ygst_json_escape(out, app_key);
184 54    key(out, "userId", 0);
185 54    ygst_json_escape(out, user_id);
186 54    key(out, "refText", 0);
187 54    ygst_json_escape(out, ref_text);
188 54    key(out, "eof", 0);
189 54    ygst_buf_append_char(out, '0');
190 54    key(out, "dtLastResponse", 0);
191 54    ygst_json_escape(out, dt_last_response);
192 54    key(out, "result", 0);
193 54    if (append_progress_result(out, result_raw, skip_words) != 0) return -1;
194 54    ygst_buf_append_char(out, '}');
195 54    ygst_buf_cstr(out);
196 54    return out->oom ? -1 : 0;
197 }
198 
199 3int ygst_params_json(ygst_buf *out, const char *app_key, const char *user_id, int64_t timestamp_sec,
200                      const char *audio_type, int sample_rate, int channel, int sample_bytes,
201                      const char *core_type, const char *token_id, const ygst_fields *request_fields) {
202     size_t i;
203     char ts[32];
204 3    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
205 3    ygst_buf_append_str(out, "{\"app\":{");
206 3    key(out, "applicationId", 1);
207 3    ygst_json_escape(out, app_key);
208 3    key(out, "userId", 0);
209 3    ygst_json_escape(out, user_id);
210 3    key(out, "timestamp", 0);
211 3    ygst_json_escape(out, ts);
212 3    ygst_buf_append_str(out, "},\"audio\":{");
213 3    key(out, "audioType", 1);
214 3    ygst_json_escape(out, audio_type);
215 3    key(out, "sampleRate", 0);
216 3    ygst_buf_append_int(out, sample_rate);
217 3    key(out, "channel", 0);
218 3    ygst_buf_append_int(out, channel);
219 3    key(out, "sampleBytes", 0);
220 3    ygst_buf_append_int(out, sample_bytes);
221 3    ygst_buf_append_str(out, "},\"request\":{");
222 3    key(out, "coreType", 1);
223 3    ygst_json_escape(out, core_type);
224 3    key(out, "tokenId", 0);
225 3    ygst_json_escape(out, token_id);
226 3    if (request_fields) {
227 9        for (i = 0; i < request_fields->count; i++) {
228 7            const char *n = request_fields->items[i].name;
229 7            if (strcmp(n, "coreType") == 0 || strcmp(n, "tokenId") == 0) continue;
230 6            key(out, n, 0);
231 6            ygst_json_escape(out, request_fields->items[i].value);
232         }
233     }
234 3    ygst_buf_append_str(out, "}}");
235 3    ygst_buf_cstr(out);
236 3    return out->oom ? -1 : 0;
237 }
238 
239 /* ------------------------------------------------------------------ base URL */
240 
241 46static int prefix_ci(const char *s, const char *p) {
242 157    while (*p) {
243 144        char a = *s, b = *p;
244 144        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
245 144        if (a != b) return 0;
246 111        s++;
247 111        p++;
248     }
249 13    return 1;
250 }
251 
252 13static int host_is_shengtong(const char *host, size_t n) {
253     static const char D[] = "stkouyu.com";
254 13    size_t m = sizeof D - 1;
255     char h[256];
256     size_t i;
257 13    if (n == 0 || n >= sizeof h) return 0;
258 195    for (i = 0; i < n; i++) {
259 182        char c = host[i];
260 182        h[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
261     }
262 13    h[n] = 0;
263 13    if (n == m) return strcmp(h, D) == 0;
264 9    return n > m && strcmp(h + n - m, D) == 0 && h[n - m - 1] == '.';
265 }
266 
267 18ygst_base_kind ygst_resolve_base_url(const char *server, char *out, size_t cap) {
268     char s[1024];
269     const char *scheme;
270     const char *rest;
271     const char *host_start;
272     const char *host_end;
273     const char *end;
274     size_t n;
275     int written;
276 18    if (!out || cap == 0) return YGST_BASE_DEFAULT_EMPTY;
277 18    snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL);
278 18    n = ygst_trim_copy(server, s, sizeof s);
279 18    if (n == 0) return YGST_BASE_DEFAULT_EMPTY;
280 16    if (prefix_ci(s, "https://")) {
281 2        scheme = "https";
282 2        rest = s + 8;
283 14    } else if (prefix_ci(s, "http://")) {
284 5        scheme = "http";
285 5        rest = s + 7;
286 9    } else if (prefix_ci(s, "wss://")) {
287 2        scheme = "https";
288 2        rest = s + 6;
289 7    } else if (prefix_ci(s, "ws://")) {
290 4        scheme = "http";
291 4        rest = s + 5;
292     } else {
293 3        return host_is_shengtong(s, strcspn(s, "/:?#")) ? YGST_BASE_DEFAULT_SHENGTONG : YGST_BASE_ERR_SCHEME;
294     }
295 13    host_start = rest;
296 13    if (*host_start == '[') {
297 2        host_end = strchr(host_start, ']');
298 2        if (!host_end) return YGST_BASE_ERR_HOST;
299 1        host_end++;
300     } else {
301 11        host_end = host_start;
302 159        while (*host_end && *host_end != '/' && *host_end != ':' && *host_end != '?' && *host_end != '#') host_end++;
303     }
304 12    if (host_end == host_start) return YGST_BASE_ERR_HOST;
305 10    if (host_is_shengtong(host_start, (size_t)(host_end - host_start))) return YGST_BASE_DEFAULT_SHENGTONG;
306 7    end = rest;
307 145    while (*end && *end != '?' && *end != '#') end++;
308 9    while (end > host_end && end[-1] == '/') end--;
309 7    written = snprintf(out, cap, "%s://%.*s", scheme, (int)(end - rest), rest);
310 7    if (written < 0 || (size_t)written >= cap) {
311 1        snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL);
312 1        return YGST_BASE_ERR_HOST;
313     }
314 6    return YGST_BASE_CUSTOM;
315 }
316 
317 39int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) {
318     size_t n;
319     int w;
320 39    if (!base || !core_type || !out) return -1;
321 38    n = strlen(base);
322 39    while (n > 0 && base[n - 1] == '/') n--;
323 38    w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type);
324 38    return (w < 0 || (size_t)w >= cap) ? -1 : 0;
325 }
326 
327 53int ygst_token_id(const unsigned char rnd[16], char out[33]) {
328 53    ygst_hex_lower(rnd, 16, out);
329 53    return 0;
330 }
331 
332 48int ygst_idempotency_key_ok(const char *k) {
333 48    size_t n = 0;
334 48    if (!k) return 0;
335 1704    for (; k[n]; n++) {
336 1662        unsigned char c = (unsigned char)k[n];
337 1662        if (c < 0x21 || c > 0x7E) return 0;
338 1658        if (n >= 200) return 0;
339     }
340 42    return n >= 1;
341 }
342 
343 3void ygst_mask_app_key(const char *app_key, char out[16]) {
344 3    size_t n = app_key ? strlen(app_key) : 0;
345 3    if (n > 4) n = 4;
346 3    memcpy(out, app_key ? app_key : "", n);
347 3    memcpy(out + n, "***", 4);
348 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 46const char *ygst_http_fallback_category(int status) {
97 46    switch (status) {
98 3    case 400:
99 3        return "INVALID_PARAM";
100 3    case 401:
101 3        return "AUTH";
102 3    case 403:
103 3        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 402void ygst_fields_init(ygst_fields *f) {
14 402    f->items = NULL;
15 402    f->count = 0;
16 402    f->cap = 0;
17 402    f->oom = 0;
18 402}
19 
20 197void ygst_fields_free(ygst_fields *f) {
21     size_t i;
22 598    for (i = 0; i < f->count; i++) {
23 401        free(f->items[i].name);
24 401        free(f->items[i].value);
25     }
26 197    free(f->items);
27 197    ygst_fields_init(f);
28 197}
29 
30 424static long find_index(const ygst_fields *f, const char *name) {
31     size_t i;
32 2479    for (i = 0; i < f->count; i++) {
33 2072        if (strcmp(f->items[i].name, name) == 0) return (long)i;
34     }
35 407    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 414int ygst_fields_set(ygst_fields *f, const char *name, const char *value) {
49     long i;
50     char *v;
51 414    if (!name || f->oom) return -1;
52 413    if (!value) {
53 1        ygst_fields_remove(f, name);
54 1        return 0;
55     }
56 412    i = find_index(f, name);
57 412    v = ygst_strdup(value);
58 412    if (!v) {
59 0        f->oom = 1;
60 0        return -1;
61     }
62 412    if (i >= 0) {
63 10        free(f->items[i].value);
64 10        f->items[i].value = v;
65 10        return 0;
66     }
67 402    if (f->count == f->cap) {
68 137        size_t cap = f->cap ? f->cap * 2 : 16;
69 137        ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field));
70 137        if (!p) {
71 0            free(v);
72 0            f->oom = 1;
73 0            return -1;
74         }
75 137        f->items = p;
76 137        f->cap = cap;
77     }
78 402    f->items[f->count].name = ygst_strdup(name);
79 402    if (!f->items[f->count].name) {
80 0        free(v);
81 0        f->oom = 1;
82 0        return -1;
83     }
84 402    f->items[f->count].value = v;
85 402    f->count++;
86 402    return 0;
87 }
88 
89 10const char *ygst_fields_get(const ygst_fields *f, const char *name) {
90 10    long i = name ? find_index(f, name) : -1;
91 10    return i < 0 ? NULL : f->items[i].value;
92 }
93 
94 3int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) {
95     size_t i;
96 54    for (i = 0; i < src->count; i++) {
97 51        if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1;
98     }
99 3    return 0;
100 }
101 
102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */
103 722static uint32_t next_cp(const unsigned char **s) {
104 722    const unsigned char *p = *s;
105 722    uint32_t c = p[0];
106 722    int n = 0;
107     int i;
108 722    if (c < 0x80) {
109 712        *s = p + 1;
110 712        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 722static int utf16_units(uint32_t cp, uint32_t u[2]) {
138 722    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 718    u[0] = cp;
145 718    return 1;
146 }
147 
148 286int ygst_utf16_compare(const char *a, const char *b) {
149 286    const unsigned char *pa = (const unsigned char *)a;
150 286    const unsigned char *pb = (const unsigned char *)b;
151     uint32_t ua[2], ub[2];
152 286    int na = 0, nb = 0, ia = 0, ib = 0;
153 77    for (;;) {
154         uint32_t ca, cb;
155 363        if (ia == na) {
156 363            if (!*pa) {
157 2                na = 0;
158             } else {
159 361                na = utf16_units(next_cp(&pa), ua);
160             }
161 363            ia = 0;
162         }
163 363        if (ib == nb) {
164 363            if (!*pb) {
165 2                nb = 0;
166             } else {
167 361                nb = utf16_units(next_cp(&pb), ub);
168             }
169 363            ib = 0;
170         }
171 363        if (na == 0 || nb == 0) {
172 3            if (na == 0 && nb == 0) return 0;
173 2            return na == 0 ? -1 : 1;
174         }
175 360        ca = ua[ia++];
176 360        cb = ub[ib++];
177 360        if (ca != cb) return ca < cb ? -1 : 1;
178     }
179 }
180 
181 278static int cmp_field_ptr(const void *x, const void *y) {
182 278    const ygst_field *a = *(const ygst_field *const *)x;
183 278    const ygst_field *b = *(const ygst_field *const *)y;
184 278    return ygst_utf16_compare(a->name, b->name);
185 }
186 
187 75int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) {
188     const ygst_field **v;
189 75    size_t n = 0;
190     size_t i;
191 75    int first = 1;
192 75    if (f->oom) return -1;
193 75    v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v));
194 75    if (!v) return -1;
195 341    for (i = 0; i < f->count; i++) {
196 266        if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i];
197     }
198 75    qsort(v, n, sizeof(*v), cmp_field_ptr);
199 319    for (i = 0; i < n; i++) {
200 244        if (!first) ygst_buf_append_char(out, '&');
201 244        first = 0;
202 244        ygst_buf_append_str(out, v[i]->name);
203 244        ygst_buf_append_char(out, '=');
204 244        ygst_buf_append_str(out, v[i]->value);
205     }
206 75    free(v);
207 75    ygst_buf_cstr(out);
208 75    return out->oom ? -1 : 0;
209 }
210 
211 67int 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 67    out[0] = 0;
216 67    if (!secret) return -1;
217 66    ygst_buf_init(&payload);
218 66    rc = ygst_sign_payload(f, &payload);
219 66    if (rc == 0) {
220 66        ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"",
221                          payload.len, mac);
222 66        if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1;
223     }
224 66    ygst_buf_free(&payload);
225 66    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 5141943static const char *ws(const char *p, const char *e) {
16 11122986    while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
17 5141943    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 1411912static const char *skip_string(const char *p, const char *e) {
28 1411912    if (p >= e || *p != '"') return NULL;
29 1411909    p++;
30 10119336    while (p < e) {
31 10119334        unsigned char c = (unsigned char)*p;
32 10119334        if (c == '"') return p + 1;
33 8707430        if (c < 0x20) return NULL;
34 8707429        if (c == '\\') {
35 790            p++;
36 790            if (p >= e) return NULL;
37 790            switch (*p) {
38 765            case '"':
39             case '\\':
40             case '/':
41             case 'b':
42             case 'f':
43             case 'n':
44             case 'r':
45             case 't':
46 765                p++;
47 765                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 788            continue;
61         }
62 8706639        p++;
63     }
64 2    return NULL;
65 }
66 
67 1286429static int is_digit(char c) {
68 1286429    return c >= '0' && c <= '9';
69 }
70 
71 393857static const char *skip_number(const char *p, const char *e) {
72 393857    if (p < e && *p == '-') p++;
73 393857    if (p >= e) return NULL;
74 393856    if (*p == '0') {
75 142261        p++;
76 251595    } else if (*p >= '1' && *p <= '9') {
77 824084        while (p < e && is_digit(*p)) p++;
78     } else {
79 0        return NULL;
80     }
81 393856    if (p < e && *p == '.') {
82 22181        p++;
83 22181        if (p >= e || !is_digit(*p)) return NULL;
84 53446        while (p < e && is_digit(*p)) p++;
85     }
86 393855    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 393854    return p;
93 }
94 
95 234830static const char *skip_lit(const char *p, const char *e, const char *lit) {
96 234830    size_t n = strlen(lit);
97 234830    if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL;
98 234826    return p + n;
99 }
100 
101 1269204static const char *skip_value(const char *p, const char *e, int depth) {
102 1269204    if (depth > YGST_JSON_MAX_DEPTH) return NULL;
103 1269203    p = ws(p, e);
104 1269203    if (p >= e) return NULL;
105 1269199    switch (*p) {
106 235553    case '{':
107 235553        p = ws(p + 1, e);
108 235553        if (p < e && *p == '}') return p + 1;
109         for (;;) {
110 985657            p = skip_string(ws(p, e), e);
111 985657            if (!p) return NULL;
112 985654            p = ws(p, e);
113 985654            if (p >= e || *p != ':') return NULL;
114 985653            p = skip_value(p + 1, e, depth + 1);
115 985653            if (!p) return NULL;
116 985651            p = ws(p, e);
117 985651            if (p >= e) return NULL;
118 985651            if (*p == ',') {
119 750159                p++;
120 750159                continue;
121             }
122 235492            if (*p == '}') return p + 1;
123 1            return NULL;
124         }
125 80744    case '[':
126 80744        p = ws(p + 1, e);
127 80744        if (p < e && *p == ']') return p + 1;
128         for (;;) {
129 157353            p = skip_value(p, e, depth + 1);
130 157353            if (!p) return NULL;
131 157222            p = ws(p, e);
132 157222            if (p >= e) return NULL;
133 157222            if (*p == ',') {
134 99109                p++;
135 99109                continue;
136             }
137 58113            if (*p == ']') return p + 1;
138 1            return NULL;
139         }
140 326035    case '"':
141 326035        return skip_string(p, e);
142 110678    case 't':
143 110678        return skip_lit(p, e, "true");
144 124127    case 'f':
145 124127        return skip_lit(p, e, "false");
146 25    case 'n':
147 25        return skip_lit(p, e, "null");
148 392037    default:
149 392037        if (*p == '-' || is_digit(*p)) return skip_number(p, e);
150 6        return NULL;
151     }
152 }
153 
154 631int 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 631    if (!s) return -1;
159 630    e = s + n;
160 630    start = ws(s, e);
161 630    end = skip_value(start, e, 0);
162 630    if (!end) return -1;
163 602    if (ws(end, e) != e) return -1;
164 600    if (root) {
165 600        root->p = start;
166 600        root->n = (size_t)(end - start);
167     }
168 600    return 0;
169 }
170 
171 223087int ygst_json_type(ygst_span v) {
172 223087    if (!v.p || v.n == 0) return 0;
173 223086    switch (v.p[0]) {
174 145241    case '{':
175 145241        return 'o';
176 54999    case '[':
177 54999        return 'a';
178 6825    case '"':
179 6825        return 's';
180 6566    case 't':
181 6566        return 't';
182 6503    case 'f':
183 6503        return 'f';
184 6    case 'n':
185 6        return 'z';
186 2946    default:
187 2946        return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0;
188     }
189 }
190 
191 117837int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) {
192 117837    const char *e = obj.p + obj.n;
193     const char *p;
194     const char *k;
195     const char *v;
196 117837    if (ygst_json_type(obj) != 'o') return -1;
197 117836    p = obj.p + (*pos ? *pos : 1);
198 117836    p = ws(p, e);
199 117836    if (p >= e) return -1;
200 117836    if (*p == '}') return 0;
201 95981    if (*pos) {
202 74133        if (*p != ',') return -1;
203 74133        p = ws(p + 1, e);
204     }
205 95981    k = p;
206 95981    p = skip_string(p, e);
207 95981    if (!p) return -1;
208 95981    if (key) {
209 95981        key->p = k;
210 95981        key->n = (size_t)(p - k);
211     }
212 95981    p = ws(p, e);
213 95981    if (p >= e || *p != ':') return -1;
214 95980    v = ws(p + 1, e);
215 95980    p = skip_value(v, e, 1);
216 95980    if (!p) return -1;
217 95980    if (value) {
218 95980        value->p = v;
219 95980        value->n = (size_t)(p - v);
220     }
221 95980    *pos = (size_t)(p - obj.p);
222 95980    return 1;
223 }
224 
225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */
226 94141static int key_equals(ygst_span tok, const char *key) {
227 94141    size_t klen = strlen(key);
228 94141    if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) {
229 94136        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 21588int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) {
241 21588    size_t pos = 0;
242     ygst_span k, v;
243 21588    int found = 0;
244     int rc;
245 21588    if (ygst_json_type(obj) != 'o' || !key) return -1;
246 115728    while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) {
247 94141        if (key_equals(k, key)) {
248 20386            found = 1;
249 20386            if (out) *out = v;
250         }
251     }
252 21587    if (rc < 0) return -1;
253 21586    return found;
254 }
255 
256 30083int ygst_json_array_next(ygst_span arr, size_t *pos, ygst_span *out) {
257 30083    const char *e = arr.p + arr.n;
258     const char *p;
259     const char *v;
260 30083    if (ygst_json_type(arr) != 'a') return -1;
261 30082    p = arr.p + (*pos ? *pos : 1);
262 30082    p = ws(p, e);
263 30082    if (p >= e) return -1;
264 30082    if (*p == ']') return 0;
265 29589    if (*pos) {
266 27016        if (*p != ',') return -1;
267 27015        p = ws(p + 1, e);
268     }
269 29588    v = p;
270 29588    p = skip_value(v, e, 1);
271 29588    if (!p) return -1;
272 29588    if (out) {
273 29588        out->p = v;
274 29588        out->n = (size_t)(p - v);
275     }
276 29588    *pos = (size_t)(p - arr.p);
277 29588    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 4240int ygst_json_string(ygst_span v, ygst_buf *out) {
308     const char *p;
309     const char *e;
310 4240    if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1;
311 4238    p = v.p + 1;
312 4238    e = v.p + v.n - 1;
313 4321    while (p < e) {
314 4245        const char *run = p;
315 31027        while (p < e && *p != '\\') p++;
316 4245        if (p > run) ygst_buf_append(out, run, (size_t)(p - run));
317 4245        if (p >= e) break;
318 83        p++; /* backslash */
319 83        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 64        default: /* " \ / */
362 64            ygst_buf_append_char(out, *p);
363 64            p++;
364 64            break;
365         }
366     }
367 4238    ygst_buf_cstr(out);
368 4238    return out->oom ? -1 : 0;
369 }
370 
371 1828static int number_text(ygst_span v, char *tmp, size_t cap) {
372 1828    if (ygst_json_type(v) != 'n' || v.n >= cap) return -1;
373 1826    if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1;
374 1826    memcpy(tmp, v.p, v.n);
375 1826    tmp[v.n] = 0;
376 1826    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 1821int ygst_json_int(ygst_span v, long long *out) {
397     char tmp[64];
398     size_t i;
399 1821    int neg = 0;
400 1821    long long acc = 0;
401 1821    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
402 1820    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 1816    i = 0;
410 1816    if (tmp[0] == '-') {
411 1        neg = 1;
412 1        i = 1;
413     }
414 6311    for (; tmp[i]; i++) {
415 4496        int dgt = tmp[i] - '0';
416 4496        if (acc > (9223372036854775807LL - dgt) / 10) return -1;
417 4495        acc = acc * 10 + dgt;
418     }
419 1815    *out = neg ? -acc : acc;
420 1815    return 0;
421 }
422 
423 13069int ygst_json_bool(ygst_span v, int *out) {
424 13069    int t = ygst_json_type(v);
425 13069    if (t == 't' && v.n == 4) {
426 6565        *out = 1;
427 6565        return 0;
428     }
429 6504    if (t == 'f' && v.n == 5) {
430 6503        *out = 0;
431 6503        return 0;
432     }
433 1    return -1;
434 }
435 
436 1568int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) {
437     static const char H[] = "0123456789abcdef";
438     size_t i;
439 1568    size_t run = 0;
440 1568    ygst_buf_append_char(out, '"');
441 24962    for (i = 0; i < n; i++) {
442 23394        unsigned char c = (unsigned char)s[i];
443 23394        const char *rep = NULL;
444         char u[7];
445 23394        if (c == '"') {
446 9            rep = "\\\"";
447 23385        } else if (c == '\\') {
448 2            rep = "\\\\";
449 23383        } else if (c == '\n') {
450 3            rep = "\\n";
451 23380        } else if (c == '\r') {
452 1            rep = "\\r";
453 23379        } else if (c == '\t') {
454 1            rep = "\\t";
455 23378        } else if (c == '\b') {
456 1            rep = "\\b";
457 23377        } else if (c == '\f') {
458 1            rep = "\\f";
459 23376        } 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 23394        if (rep) {
470 20            if (i > run) ygst_buf_append(out, s + run, i - run);
471 20            ygst_buf_append_str(out, rep);
472 20            run = i + 1;
473         }
474     }
475 1568    if (n > run) ygst_buf_append(out, s + run, n - run);
476 1568    ygst_buf_append_char(out, '"');
477 1568    return out->oom ? -1 : 0;
478 }
479 
480 1568int ygst_json_escape(ygst_buf *out, const char *s) {
481 1568    if (!s) s = "";
482 1568    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 37int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) {
15     char hex[33];
16 37    ygst_hex_lower(rnd, 16, hex);
17 37    snprintf(out, 64, "----YuguSTCompat%s", hex);
18 37    return 0;
19 }
20 
21 38int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) {
22     int n;
23 38    if (!boundary || !out) return -1;
24 38    n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary);
25 38    return (n < 0 || (size_t)n >= cap) ? -1 : 0;
26 }
27 
28 119static int contains(const unsigned char *hay, size_t n, const char *needle) {
29 119    size_t m = strlen(needle);
30     size_t i;
31 119    if (m == 0 || n < m) return 0;
32 2591927    for (i = 0; i + m <= n; i++) {
33 2591886        if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1;
34     }
35 41    return 0;
36 }
37 
38 44int 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 44    if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1;
43 42    if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1;
44 121    for (i = 0; i < fields->count; i++) {
45 82        const char *v = fields->items[i].value ? fields->items[i].value : "";
46 82        if (!ygst_field_name_ok(fields->items[i].name)) return -1;
47 81        if (contains((const unsigned char *)v, strlen(v), boundary)) return -2;
48     }
49 39    if (file && contains(file, file_len, boundary)) return -2;
50 118    for (i = 0; i < fields->count; i++) {
51 80        ygst_buf_append_str(out, "--");
52 80        ygst_buf_append_str(out, boundary);
53 80        ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
54 80        ygst_buf_append_str(out, fields->items[i].name);
55 80        ygst_buf_append_str(out, "\"\r\n\r\n");
56 80        ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : "");
57 80        ygst_buf_append_str(out, "\r\n");
58     }
59 38    ygst_buf_append_str(out, "--");
60 38    ygst_buf_append_str(out, boundary);
61 38    ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
62 38    ygst_buf_append_str(out, file_field);
63 38    ygst_buf_append_str(out, "\"; filename=\"");
64 38    ygst_buf_append_str(out, filename);
65 38    ygst_buf_append_str(out, "\"\r\nContent-Type: ");
66 38    ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream");
67 38    ygst_buf_append_str(out, "\r\n\r\n");
68 38    if (file_len) ygst_buf_append(out, file, file_len);
69 38    ygst_buf_append_str(out, "\r\n--");
70 38    ygst_buf_append_str(out, boundary);
71 38    ygst_buf_append_str(out, "--\r\n");
72 38    return out->oom ? -1 : 0;
73 }
74 
75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */
76 53const 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 53    if (ygst_is_blank(audio_type)) return "wav";
82 51    ygst_trim_copy(audio_type, t, sizeof t);
83 51    if (t[0] == '.') memmove(t, t + 1, strlen(t));
84 108    for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) {
85 105        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 47const char *ygst_audio_content_type(const char *ext) {
93 47    if (!ext) return "application/octet-stream";
94 46    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 108ygst_ct_status ygst_coretype_check(const char *core_type) {
63     size_t i;
64 108    if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN;
65 824    for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) {
66 789        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 62ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) {
75     const char *mapped;
76 62    if (!out || cap == 0) return YGST_CT_UNKNOWN;
77 61    out[0] = 0;
78 61    if (!ygst_is_blank(core_type_ns)) {
79 55        ygst_trim_copy(core_type_ns, out, cap);
80 55        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 176int ygst_coretype_is_paragraph(const char *core_type) {
94 176    return core_type && strncmp(core_type, "para.", 5) == 0;
95 }
96 
97 88int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) {
98 88    if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1");
99 81    return 0;
100 }
101 
102 60int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text,
103                           const char *ref_pinyin) {
104 60    if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED;
105 55    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 178int ygst_field_name_ok(const char *name) {
178 178    if (!name || !name[0]) return 0;
179 1716    for (; *name; name++) {
180 1545        if (*name == '"' || *name == '\r' || *name == '\n') return 0;
181     }
182 171    return 1;
183 }
184 
185 /* ------------------------------------------------------------------ mapping */
186 
187 81void ygst_test_params_init(ygst_test_params *p) {
188 81    memset(p, 0, sizeof *p);
189 81}
190 
191 863static int set_str(ygst_fields *f, const char *name, const char *v) {
192 863    if (!v || !v[0]) return 0;
193 67    return ygst_fields_set(f, name, v);
194 }
195 
196 602static int set_flag(ygst_fields *f, const char *name, int on) {
197 602    return on ? ygst_fields_set(f, name, "1") : 0;
198 }
199 
200 431static int set_num(ygst_fields *f, const char *name, double v) {
201     char tmp[64];
202 431    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 86int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) {
213     static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"};
214 86    int rc = 0;
215 86    rc |= set_str(out, "refText", p->ref_text);
216 86    rc |= set_str(out, "refPinyin", p->ref_pinyin);
217 86    if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group);
218 86    rc |= set_num(out, "scale", p->scale);
219 86    rc |= set_num(out, "precision", p->precision);
220 86    rc |= set_num(out, "slack", p->slack);
221 86    rc |= set_flag(out, "paragraph_need_word_score",
222 86                   p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type));
223 86    rc |= set_flag(out, "phoneme_output", p->phoneme_output);
224 86    rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url);
225 86    if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]);
226 86    rc |= set_str(out, "dict_dialect", p->dict_dialect);
227 86    rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json);
228 86    rc |= set_str(out, "customized_pron", p->customized_pron_json);
229 86    if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis);
230 86    rc |= set_flag(out, "output_rawtext", p->output_rawtext);
231 86    rc |= set_flag(out, "punctuate", p->punctuate);
232 86    rc |= set_num(out, "itn", p->itn);
233 86    rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable);
234     /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */
235 86    rc |= set_flag(out, "vad_detction", p->vad_detection);
236 86    rc |= set_str(out, "keywords", p->keywords);
237 86    rc |= set_str(out, "keypoints", p->keypoints_json);
238 86    rc |= set_num(out, "keypoints_weight", p->keypoints_weight);
239 86    rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json);
240 86    rc |= set_str(out, "negativeReftext", p->negative_reftext);
241 86    if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home");
242 86    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 86    if (p->custom_params) {
246         size_t i;
247 57        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 86    rc |= set_str(out, "request", p->request_json);
254     /* customParams cannot switch the paragraph word details off */
255 86    rc |= ygst_fields_apply_coretype_rules(out, p->core_type);
256 86    return (rc != 0 || out->oom) ? -1 : 0;
257 }

Sources/STKouyuEngine/core/ygst_realtime.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Realtime feedback (2.1.0, DESIGN 14.3, CONTRACT 5.2 and 5.5): the compat WebSocket round of one
6  * evaluation. Handshake URL and its signature, parameter frame, classification of platform
7  * frames, the words dedupe of the reconnect rule and the round controller that decides
8  * connecting, streaming, ending, falling back to REST before end and reconnecting after end.
9  */
10 #include "ygst_core.h"
11 
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 
16 /* ------------------------------------------------------------------ URL */
17 
18 198int ygst_url_encode(ygst_buf *out, const char *s) {
19     static const char H[] = "0123456789ABCDEF";
20 198    const unsigned char *p = (const unsigned char *)(s ? s : "");
21 3231    for (; *p; p++) {
22 3033        unsigned char c = *p;
23 3033        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ||
24 73            c == '_' || c == '~') {
25 2961            ygst_buf_append_char(out, (char)c);
26         } else {
27             char e[3];
28 72            e[0] = '%';
29 72            e[1] = H[c >> 4];
30 72            e[2] = H[c & 15];
31 72            ygst_buf_append(out, e, 3);
32         }
33     }
34 198    ygst_buf_cstr(out);
35 198    return out->oom ? -1 : 0;
36 }
37 
38 57static int starts_ci(const char *s, const char *prefix) {
39 316    while (*prefix) {
40 290        char a = *s;
41 290        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
42 290        if (a != *prefix) return 0;
43 259        s++;
44 259        prefix++;
45     }
46 26    return 1;
47 }
48 
49 30int ygst_ws_base_url(const char *rest_base, char *out, size_t cap) {
50     const char *scheme;
51     const char *rest;
52     size_t n;
53     int w;
54 30    if (!out || cap == 0) return -1;
55 29    out[0] = 0;
56 29    if (!rest_base) return -1;
57 28    if (starts_ci(rest_base, "https://")) {
58 6        scheme = "wss://";
59 6        rest = rest_base + 8;
60 22    } else if (starts_ci(rest_base, "http://")) {
61 18        scheme = "ws://";
62 18        rest = rest_base + 7;
63 4    } else if (starts_ci(rest_base, "wss://")) {
64 1        scheme = "wss://";
65 1        rest = rest_base + 6;
66 3    } else if (starts_ci(rest_base, "ws://")) {
67 1        scheme = "ws://";
68 1        rest = rest_base + 5;
69     } else {
70 2        return -1;
71     }
72 26    n = strlen(rest);
73 30    while (n > 0 && rest[n - 1] == '/') n--;
74 26    if (n == 0 || rest[0] == '/') return -1;
75 24    w = snprintf(out, cap, "%s%.*s", scheme, (int)n, rest);
76 24    if (w < 0 || (size_t)w >= cap) {
77 1        out[0] = 0;
78 1        return -1;
79     }
80 23    return 0;
81 }
82 
83 /* Query parameters the handshake sets itself. */
84 9static int own_query_name(const char *n) {
85 8    return strcmp(n, "appKey") == 0 || strcmp(n, "timestamp") == 0 || strcmp(n, "nonce") == 0 ||
86 17           strcmp(n, "idempotencyKey") == 0 || strcmp(n, "signature") == 0;
87 }
88 
89 25int ygst_ws_handshake_url(const char *rest_base, const char *core_type, const char *app_key, const char *secret,
90                           int64_t timestamp_sec, const char *nonce, const char *idempotency_key,
91                           const ygst_fields *extra, ygst_buf *out) {
92     char base[1024];
93     char ts[32];
94     char sig[45];
95     ygst_fields q;
96     size_t i;
97 25    int rc = -1;
98 25    if (!out || !core_type || !core_type[0] || ygst_is_blank(app_key) || !secret || !secret[0] || !nonce ||
99 21        !nonce[0] || !ygst_idempotency_key_ok(idempotency_key)) {
100 6        return -1;
101     }
102 19    if (ygst_ws_base_url(rest_base, base, sizeof base) != 0) return -1;
103 18    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
104 18    ygst_fields_init(&q);
105 18    ygst_fields_set(&q, "appKey", app_key);
106 18    ygst_fields_set(&q, "timestamp", ts);
107 18    ygst_fields_set(&q, "nonce", nonce);
108 18    ygst_fields_set(&q, "idempotencyKey", idempotency_key);
109 18    if (extra) {
110 27        for (i = 0; i < extra->count; i++) {
111 10            const ygst_field *f = &extra->items[i];
112 10            if (!f->name || !f->name[0] || !f->value || !f->value[0] || own_query_name(f->name)) continue;
113 7            ygst_fields_set(&q, f->name, f->value);
114         }
115     }
116 18    if (q.oom || ygst_sign(&q, secret, sig) != 0) goto done;
117 18    ygst_buf_append_str(out, base);
118 18    ygst_buf_append_char(out, '/');
119 18    ygst_url_encode(out, core_type);
120 97    for (i = 0; i < q.count; i++) {
121 79        ygst_buf_append_char(out, i == 0 ? '?' : '&');
122 79        ygst_url_encode(out, q.items[i].name);
123 79        ygst_buf_append_char(out, '=');
124 79        ygst_url_encode(out, q.items[i].value);
125     }
126 18    ygst_buf_append_str(out, "&signature=");
127 18    ygst_url_encode(out, sig);
128 18    ygst_buf_cstr(out);
129 18    rc = out->oom ? -1 : 0;
130 18done:
131 18    ygst_fields_free(&q);
132 18    return rc;
133 }
134 
135 /* ------------------------------------------------------------------ parameter frame */
136 
137 /* The platform reads these names as commands (cmd, end, data) or sets them from its own fields. */
138 43static int reserved_frame_name(const char *n) {
139 42    return strcmp(n, "cmd") == 0 || strcmp(n, "end") == 0 || strcmp(n, "data") == 0 ||
140 85           strcmp(n, "realtime_feedback") == 0 || strcmp(n, "idempotencyKey") == 0;
141 }
142 
143 21int ygst_ws_param_frame(const ygst_fields *fields, const char *idempotency_key, ygst_buf *out) {
144     size_t i;
145 21    int skipped = 0;
146 21    if (!fields || !out || fields->oom || !ygst_idempotency_key_ok(idempotency_key)) return -1;
147 19    ygst_buf_append_char(out, '{');
148 62    for (i = 0; i < fields->count; i++) {
149 43        const ygst_field *f = &fields->items[i];
150 43        if (!f->name || reserved_frame_name(f->name)) {
151 5            skipped++;
152 5            continue;
153         }
154 38        ygst_json_escape(out, f->name);
155 38        ygst_buf_append_char(out, ':');
156 38        ygst_json_escape(out, f->value ? f->value : "");
157 38        ygst_buf_append_char(out, ',');
158     }
159 19    ygst_buf_append_str(out, "\"realtime_feedback\":true,\"idempotencyKey\":");
160 19    ygst_json_escape(out, idempotency_key);
161 19    ygst_buf_append_char(out, '}');
162 19    ygst_buf_cstr(out);
163 19    return out->oom ? -1 : skipped;
164 }
165 
166 /* ------------------------------------------------------------------ frames */
167 
168 /* An integer member, also when a gateway quoted it. 1 found, 0 absent or not an integer. */
169 1063static int int_member(ygst_span obj, const char *key, long long *out) {
170     ygst_span v;
171 1063    if (ygst_json_member(obj, key, &v) != 1) return 0;
172 1019    if (ygst_json_type(v) == 's') {
173         ygst_buf b;
174 6        int ok = 0;
175 6        ygst_buf_init(&b);
176 6        if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 20) {
177 6            char *end = NULL;
178 6            long long x = strtoll(ygst_buf_cstr(&b), &end, 10);
179 6            if (end && *end == 0) {
180 6                *out = x;
181 6                ok = 1;
182             }
183         }
184 6        ygst_buf_free(&b);
185 6        return ok;
186     }
187 1013    return ygst_json_int(v, out) == 0;
188 }
189 
190 8285static int bool_member(ygst_span obj, const char *key) {
191     ygst_span v;
192 8285    int b = 0;
193 8285    if (ygst_json_member(obj, key, &v) != 1 || ygst_json_bool(v, &b) != 0) return 0;
194 8265    return b;
195 }
196 
197 596static int count_value(long long v) {
198 596    if (v < 0) return -1;
199 596    return v > 2147483647LL ? 2147483647 : (int)v;
200 }
201 
202 /* The event name when it is a short JSON string, else "". */
203 130static void event_name(ygst_span ev, char *out, size_t cap) {
204     ygst_buf b;
205 130    out[0] = 0;
206 130    if (ygst_json_type(ev) != 's') return;
207 129    ygst_buf_init(&b);
208 129    if (ygst_json_string(ev, &b) == 0 && b.len < cap) memcpy(out, ygst_buf_cstr(&b), b.len + 1);
209 129    ygst_buf_free(&b);
210 }
211 
212 203static void parse_progress(ygst_ws_frame *f) {
213     ygst_span r, list, item;
214     long long v;
215 203    size_t pos = 0;
216 203    int read = 0, matched = 0, total = 0;
217 203    if (ygst_json_member(f->root, "result", &r) != 1 || ygst_json_type(r) != 'o') return;
218 202    f->result = r;
219 202    if (int_member(r, "bytes", &v) && v >= 0) f->bytes = v;
220 202    f->read_units = int_member(r, "readUnits", &v) ? count_value(v) : -1;
221 202    f->matched_units = int_member(r, "matchedUnits", &v) ? count_value(v) : -1;
222 202    f->total_units = int_member(r, "totalUnits", &v) ? count_value(v) : -1;
223     /* every unit with its state */
224 202    if (ygst_json_member(r, "progress", &list) == 1 && ygst_json_type(list) == 'a') {
225 191        f->has_progress = 1;
226 4323        while (ygst_json_array_next(list, &pos, &item) == 1) {
227 4132            total++;
228 4132            if (ygst_json_type(item) != 'o') continue;
229 4131            if (bool_member(item, "read")) read++;
230 4131            if (bool_member(item, "matched")) matched++;
231         }
232 191        if (f->read_units < 0) f->read_units = read;
233 191        if (f->matched_units < 0) f->matched_units = matched;
234 191        if (f->total_units < 0) f->total_units = total;
235     }
236     /* Shengtong items of the units read since the previous frame */
237 202    if (ygst_json_member(r, "words", &list) == 1 && ygst_json_type(list) == 'a') {
238 169        f->has_words = 1;
239 169        f->words = list;
240 169        pos = 0;
241 481        while (ygst_json_array_next(list, &pos, &item) == 1) f->word_count++;
242     }
243 }
244 
245 23static void parse_final(ygst_ws_frame *f) {
246     ygst_span r;
247 23    if (ygst_json_member(f->root, "result", &r) == 1) f->result = r;
248 23    f->replayed = bool_member(f->root, "replayed");
249 23}
250 
251 360int ygst_ws_frame_parse(const char *text, size_t n, ygst_ws_frame *f) {
252     ygst_span root, v;
253 360    if (!f) return -1;
254 359    memset(f, 0, sizeof *f);
255 359    f->kind = YGST_WSF_INVALID;
256 359    f->bytes = -1;
257 359    f->read_units = -1;
258 359    f->matched_units = -1;
259 359    f->total_units = -1;
260 359    if (!text || ygst_json_parse(text, n, &root) != 0 || ygst_json_type(root) != 'o') return -1;
261 355    f->root = root;
262 355    if (ygst_json_member(root, "event", &v) == 1) {
263         char ev[16];
264 130        event_name(v, ev, sizeof ev);
265 130        if (strcmp(ev, "connected") == 0) {
266 53            f->kind = YGST_WSF_CONNECTED;
267 77        } else if (strcmp(ev, "started") == 0) {
268 53            f->kind = YGST_WSF_STARTED;
269 24        } else if (strcmp(ev, "pong") == 0) {
270 2            f->kind = YGST_WSF_PONG;
271 22        } else if (strcmp(ev, "error") == 0) {
272 18            long long code = 0;
273 18            f->kind = YGST_WSF_ERROR;
274 18            if (int_member(root, "code", &code) && code > 0) f->code = count_value(code);
275 4        } else if (strcmp(ev, "result") == 0) {
276 2            f->kind = YGST_WSF_FINAL;
277 2            parse_final(f);
278         } else {
279 2            f->kind = YGST_WSF_OTHER;
280         }
281 130        return 0;
282     }
283 225    if (ygst_json_member(root, "eof", &v) == 1) {
284 222        long long eof = -1;
285 222        if (int_member(root, "eof", &eof) && eof == 0) {
286 203            f->kind = YGST_WSF_PROGRESS;
287 203            parse_progress(f);
288         } else {
289 19            f->kind = YGST_WSF_FINAL;
290 19            parse_final(f);
291         }
292 222        return 0;
293     }
294 3    if (ygst_json_member(root, "result", NULL) == 1 || ygst_json_member(root, "recordId", NULL) == 1) {
295 2        f->kind = YGST_WSF_FINAL;
296 2        parse_final(f);
297 2        return 0;
298     }
299 1    f->kind = YGST_WSF_OTHER;
300 1    return 0;
301 }
302 
303 15int ygst_ws_classify_error(ygst_span frame, ygst_classification *c) {
304     static const char *const KEYS[] = {"message", "msg", "error", NULL};
305     const char *const *k;
306 15    long long code = 0;
307 15    c->success = 0;
308 15    c->http_status = 0;
309 15    c->local_code = 0;
310 15    c->biz_code = 0;
311 15    c->has_audio_url = 0;
312 15    c->has_record_id = 0;
313 15    c->record_id_raw.p = NULL;
314 15    c->record_id_raw.n = 0;
315 15    c->result.p = NULL;
316 15    c->result.n = 0;
317 15    ygst_buf_reset(&c->record_id);
318 15    ygst_buf_reset(&c->audio_url);
319 15    ygst_buf_reset(&c->message);
320 15    if (ygst_json_type(frame) == 'o') {
321 15        if (int_member(frame, "code", &code) && code > 0) c->biz_code = count_value(code);
322 19        for (k = KEYS; *k; k++) {
323             ygst_span v;
324 18            if (ygst_json_member(frame, *k, &v) == 1 && ygst_json_type(v) == 's') {
325 14                ygst_json_string(v, &c->message);
326 14                break;
327             }
328         }
329     }
330 15    if (!c->message.len) ygst_buf_append_str(&c->message, "error frame without a message");
331 15    ygst_buf_cstr(&c->message);
332 15    ygst_buf_cstr(&c->record_id);
333 15    ygst_buf_cstr(&c->audio_url);
334     /* without a business code the platform failed while evaluating: replay like a 5xx */
335 15    c->retryable = c->biz_code ? ygst_is_retryable(0, c->biz_code, 0) : 1;
336 15    c->err_id = ygst_errid_for(0, c->biz_code, 0, c->retryable);
337 15    return c->message.oom ? -1 : 0;
338 }
339 
340 /* ------------------------------------------------------------------ words dedupe */
341 
342 41void ygst_words_dedupe_init(ygst_words_dedupe *d) {
343 41    memset(d, 0, sizeof *d);
344 41}
345 
346 97int ygst_words_dedupe_skip(ygst_words_dedupe *d, int read_units, int word_count) {
347     long start;
348     long skip;
349 97    if (word_count <= 0) return 0;
350 96    if (read_units < 0) read_units = d->delivered + word_count; /* position unknown: the next units */
351 96    start = (long)read_units - word_count;
352 96    skip = d->delivered > start ? (long)d->delivered - start : 0;
353 96    if (skip >= word_count) return word_count; /* every item was delivered already */
354 72    if (read_units > d->delivered) d->delivered = read_units;
355 72    return (int)skip;
356 }
357 
358 /* ------------------------------------------------------------------ round controller */
359 
360 39void ygst_rt_init(ygst_rt *rt) {
361 39    memset(rt, 0, sizeof *rt);
362 39    ygst_retry_policy_default(&rt->policy);
363 39    rt->connect_timeout_ms = 20000;
364 39    rt->attempt_timeout_ms = 60000;
365 39    rt->ping_interval_ms = 15000;
366 39    rt->pong_timeout_ms = 30000;
367 39    rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
368 39    rt->state = YGST_RT_IDLE;
369 39    rt->frame.kind = YGST_WSF_INVALID;
370 39    ygst_words_dedupe_init(&rt->dedupe);
371 39    ygst_classification_init(&rt->cls);
372 39    ygst_buf_init(&rt->failure);
373 39}
374 
375 39void ygst_rt_free(ygst_rt *rt) {
376 39    ygst_classification_free(&rt->cls);
377 39    ygst_buf_free(&rt->failure);
378 39}
379 
380 1184static ygst_rt_actions no_actions(void) {
381     ygst_rt_actions a;
382 1184    memset(&a, 0, sizeof a);
383 1184    return a;
384 }
385 
386 795static int on_connection(ygst_rt_state s) {
387 795    return s == YGST_RT_CONNECTING || s == YGST_RT_STARTING || s == YGST_RT_STREAMING || s == YGST_RT_ENDING;
388 }
389 
390 167static void clear_timers(ygst_rt *rt) {
391 167    rt->deadline_ms = 0;
392 167    rt->next_ping_ms = 0;
393 167    rt->pong_deadline_ms = 0;
394 167}
395 
396 35static void note_failure(ygst_rt *rt, const char *why) {
397 35    ygst_buf_reset(&rt->failure);
398 35    ygst_buf_append_str(&rt->failure, why ? why : "");
399 35    ygst_buf_cstr(&rt->failure);
400 35}
401 
402 53static void open_connection(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
403 53    a->connect = 1;
404 53    rt->state = YGST_RT_CONNECTING;
405 53    rt->connections++;
406 53    rt->sent = 0;
407 53    clear_timers(rt);
408 53    rt->deadline_ms = now + rt->connect_timeout_ms;
409 53}
410 
411 198static void send_pending_audio(ygst_rt *rt, ygst_rt_actions *a) {
412 198    if (rt->total > rt->sent) {
413 174        a->audio_from = rt->sent;
414 174        a->audio_to = rt->total;
415 174        rt->sent = rt->total;
416     }
417 198}
418 
419 29static void start_streaming(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
420 29    rt->state = YGST_RT_STREAMING;
421 29    clear_timers(rt);
422 29    if (rt->ping_interval_ms > 0) rt->next_ping_ms = now + rt->ping_interval_ms;
423 29    send_pending_audio(rt, a);
424 29}
425 
426 /* All audio from the current offset, then end. The first end starts the attempts of the REST
427  * call controller; a reconnect only sends it again. */
428 37static void send_end(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
429 37    send_pending_audio(rt, a);
430 37    a->send_end = 1;
431 37    if (!rt->end_sent) {
432 25        rt->end_sent = 1;
433 25        ygst_call_init(&rt->call, &rt->policy, rt->seed, now);
434     }
435 37    rt->state = YGST_RT_ENDING;
436     /* no heartbeat while the platform evaluates: the result timeout bounds the wait, as a REST
437      * attempt's read timeout does */
438 37    clear_timers(rt);
439 37    rt->deadline_ms = now + rt->attempt_timeout_ms;
440 37}
441 
442 /* Before end: the platform never evaluated this round, REST evaluates it with the same tokenId. */
443 9static void fall_back(ygst_rt *rt, ygst_rt_actions *a, const char *why) {
444 9    a->close = 1;
445 9    rt->state = YGST_RT_FALLBACK;
446 9    clear_timers(rt);
447 9    note_failure(rt, why);
448 9    if (rt->end_requested) a->fallback = 1;
449 9}
450 
451 /* After end: rt->cls holds the outcome of this attempt; the REST call controller decides. */
452 26static void attempt_failed(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
453 26    ygst_action next = ygst_call_next(&rt->call, &rt->cls, -1, now);
454 26    a->close = 1;
455 26    clear_timers(rt);
456 26    note_failure(rt, ygst_buf_cstr(&rt->cls.message));
457 26    if (next.kind == YGST_ACT_RETRY) {
458 16        rt->state = YGST_RT_BACKOFF;
459 16        rt->reconnect_at_ms = now + next.delay_ms;
460 16        a->retry = 1;
461 16        a->delay_ms = next.delay_ms;
462 16        return;
463     }
464 10    rt->state = YGST_RT_DONE;
465 10    a->fail = 1;
466 10    a->err_id = next.kind == YGST_ACT_FAIL ? next.err_id : YGST_LOCAL_PROTOCOL;
467 }
468 
469 23static void connection_failed(ygst_rt *rt, ygst_rt_actions *a, int local_code, int http_status, const char *message,
470                               int64_t now) {
471     ygst_outcome o;
472 23    if (!rt->end_sent) {
473 7        fall_back(rt, a, message);
474 7        return;
475     }
476 16    memset(&o, 0, sizeof o);
477 16    o.local_code = local_code;
478 16    o.http_status = local_code ? 0 : http_status;
479 16    o.retry_after_ms = -1;
480 16    o.local_message = message;
481 16    ygst_classify(&o, &rt->cls);
482 16    if (!local_code && http_status > 0 && message && message[0]) {
483         /* a rejected handshake has no body: say so instead of repeating the status */
484 2        ygst_buf_reset(&rt->cls.message);
485 2        ygst_buf_append_str(&rt->cls.message, message);
486 2        ygst_buf_cstr(&rt->cls.message);
487     }
488 16    attempt_failed(rt, a, now);
489 }
490 
491 39ygst_rt_actions ygst_rt_start(ygst_rt *rt, int64_t now_ms) {
492 39    ygst_rt_actions a = no_actions();
493 39    if (rt->state != YGST_RT_IDLE) return a;
494 38    if (rt->max_frame_bytes == 0) rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
495 38    open_connection(rt, &a, now_ms);
496 38    return a;
497 }
498 
499 249ygst_rt_actions ygst_rt_on_audio(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
500 249    ygst_rt_actions a = no_actions();
501     (void)now_ms;
502 249    if (rt->end_requested) return a; /* the round audio is complete */
503 248    if (total_bytes > rt->total) rt->total = total_bytes;
504 248    if (rt->state == YGST_RT_STREAMING) send_pending_audio(rt, &a);
505 248    return a;
506 }
507 
508 33ygst_rt_actions ygst_rt_on_stop(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
509 33    ygst_rt_actions a = no_actions();
510 33    if (rt->end_requested || rt->state == YGST_RT_DONE) return a;
511 31    if (total_bytes > rt->total) rt->total = total_bytes;
512 31    rt->end_requested = 1;
513 31    switch (rt->state) {
514 5    case YGST_RT_IDLE:
515     case YGST_RT_FALLBACK:
516 5        rt->state = YGST_RT_FALLBACK;
517 5        a.fallback = 1;
518 5        break;
519 21    case YGST_RT_STREAMING:
520 21        send_end(rt, &a, now_ms);
521 21        break;
522 5    default:
523 5        break; /* CONNECTING, STARTING: end goes out right after "started" */
524     }
525 31    return a;
526 }
527 
528 232ygst_rt_actions ygst_rt_on_text(ygst_rt *rt, const char *text, size_t n, int64_t now_ms) {
529 232    ygst_rt_actions a = no_actions();
530 232    if (!on_connection(rt->state)) return a;
531 228    if (ygst_ws_frame_parse(text, n, &rt->frame) != 0) return a;
532 227    switch (rt->frame.kind) {
533 47    case YGST_WSF_CONNECTED:
534 47        if (rt->state == YGST_RT_CONNECTING) {
535 46            a.send_params = 1;
536 46            rt->state = YGST_RT_STARTING;
537         }
538 47        break;
539 46    case YGST_WSF_STARTED:
540 46        if (rt->state == YGST_RT_STARTING) {
541 45            if (rt->end_sent || rt->end_requested) {
542 16                send_end(rt, &a, now_ms);
543             } else {
544 29                start_streaming(rt, &a, now_ms);
545             }
546         }
547 46        break;
548 110    case YGST_WSF_PROGRESS:
549         /* delivered only with new words; empty words and bytes-only frames are not */
550 110        if (rt->frame.word_count > 0) {
551 87            int skip = ygst_words_dedupe_skip(&rt->dedupe, rt->frame.read_units, rt->frame.word_count);
552 87            if (skip < rt->frame.word_count) {
553 65                a.progress = 1;
554 65                a.words_skip = skip;
555             } else {
556 22                a.progress_held = 1;
557             }
558         }
559 110        break;
560 13    case YGST_WSF_FINAL:
561 13        if (rt->state != YGST_RT_ENDING) {
562 1            if (!rt->end_sent) fall_back(rt, &a, "result frame before end");
563 1            break;
564         }
565         {
566             ygst_outcome o;
567 12            memset(&o, 0, sizeof o);
568 12            o.http_status = 200;
569 12            o.body = text;
570 12            o.body_len = n;
571 12            o.retry_after_ms = -1;
572 12            o.replayed = rt->frame.replayed;
573 12            ygst_classify(&o, &rt->cls);
574         }
575 12        if (rt->cls.success) {
576 11            (void)ygst_call_next(&rt->call, &rt->cls, -1, now_ms);
577 11            rt->state = YGST_RT_DONE;
578 11            clear_timers(rt);
579 11            a.close = 1;
580 11            a.final = 1;
581         } else {
582 1            attempt_failed(rt, &a, now_ms);
583         }
584 12        break;
585 10    case YGST_WSF_ERROR:
586 10        ygst_ws_classify_error(rt->frame.root, &rt->cls);
587 10        if (!rt->end_sent) {
588 1            fall_back(rt, &a, ygst_buf_cstr(&rt->cls.message));
589         } else {
590 9            attempt_failed(rt, &a, now_ms);
591         }
592 10        break;
593 1    default:
594 1        break; /* pong, other events */
595     }
596 227    return a;
597 }
598 
599 3ygst_rt_actions ygst_rt_on_pong(ygst_rt *rt, int64_t now_ms) {
600 3    ygst_rt_actions a = no_actions();
601 3    if (rt->state == YGST_RT_STREAMING && rt->pong_deadline_ms) {
602 1        rt->pong_deadline_ms = 0;
603 1        if (rt->ping_interval_ms > 0) rt->next_ping_ms = now_ms + rt->ping_interval_ms;
604     }
605 3    return a;
606 }
607 
608 19ygst_rt_actions ygst_rt_on_failure(ygst_rt *rt, int local_code, int http_status, const char *message,
609                                    int64_t now_ms) {
610 19    ygst_rt_actions a = no_actions();
611 19    if (!on_connection(rt->state)) return a;
612 17    connection_failed(rt, &a, local_code, http_status, message, now_ms);
613 17    return a;
614 }
615 
616 609ygst_rt_actions ygst_rt_on_tick(ygst_rt *rt, int64_t now_ms) {
617 609    ygst_rt_actions a = no_actions();
618     char msg[96];
619 609    if (rt->state == YGST_RT_BACKOFF) {
620 77        if (now_ms >= rt->reconnect_at_ms) open_connection(rt, &a, now_ms);
621 77        return a;
622     }
623 532    if (!on_connection(rt->state)) return a;
624 388    if (rt->deadline_ms && now_ms >= rt->deadline_ms) {
625 4        int ending = rt->state == YGST_RT_ENDING;
626 4        if (ending) {
627 2            snprintf(msg, sizeof msg, "timeout: no result within %d ms", rt->attempt_timeout_ms);
628         } else {
629 2            snprintf(msg, sizeof msg, "timeout: not started within %d ms", rt->connect_timeout_ms);
630         }
631 4        connection_failed(rt, &a, ending ? YGST_LOCAL_RESULT_TIMEOUT : YGST_LOCAL_TIMEOUT, 0, msg, now_ms);
632 4        return a;
633     }
634 384    if (rt->pong_deadline_ms && now_ms >= rt->pong_deadline_ms) {
635 2        snprintf(msg, sizeof msg, "timeout: no pong within %d ms", rt->pong_timeout_ms);
636 2        connection_failed(rt, &a, YGST_LOCAL_TIMEOUT, 0, msg, now_ms);
637 2        return a;
638     }
639 382    if (rt->state == YGST_RT_STREAMING && rt->next_ping_ms && !rt->pong_deadline_ms && now_ms >= rt->next_ping_ms) {
640 4        a.send_ping = 1;
641 4        rt->next_ping_ms = 0;
642 4        rt->pong_deadline_ms = now_ms + rt->pong_timeout_ms;
643     }
644 382    return a;
645 }
646 
647 2void ygst_rt_cancel(ygst_rt *rt) {
648 2    rt->state = YGST_RT_DONE;
649 2    clear_timers(rt);
650 2}
651 
652 12int ygst_rt_active(const ygst_rt *rt) {
653 12    return on_connection(rt->state) || rt->state == YGST_RT_BACKOFF;
654 }
655 
656 5int ygst_rt_error_message(const ygst_rt *rt, int err_id, ygst_buf *out) {
657 5    return ygst_error_message(&rt->cls, err_id, rt->call.attempt, out);
658 }

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 272static const ygst_error_entry *find_in(const ygst_error_entry *t, size_t n, int code) {
19     size_t i;
20 3776    for (i = 0; i < n; i++) {
21 3762        if (t[i].code == code) return &t[i];
22     }
23 14    return NULL;
24 }
25 
26 195const ygst_error_entry *ygst_error_find(int code) {
27 195    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 68const ygst_error_entry *ygst_local_find(int code) {
35 68    return find_in(ygst_table_local, ygst_table_local_count, code);
36 }
37 
38 34const char *ygst_compat_message(int err_id) {
39     size_t i;
40 158    for (i = 0; i < ygst_table_compat_count; i++) {
41 154        if (ygst_table_compat[i].err_id == err_id) return ygst_table_compat[i].message;
42     }
43 4    return NULL;
44 }
45 
46 46int ygst_http_retryable(int status) {
47     size_t i;
48 291    for (i = 0; i < ygst_table_retryable_http_count; i++) {
49 267        if (ygst_table_retryable_http[i] == status) return 1;
50     }
51 24    return 0;
52 }
53 
54 /* ------------------------------------------------------------------ RNG (splitmix64) */
55 
56 77void ygst_rng_seed(ygst_rng *r, uint64_t seed) {
57 77    r->s = seed;
58 77}
59 
60 1074uint64_t ygst_rng_next(ygst_rng *r) {
61 1074    uint64_t z = (r->s += 0x9E3779B97F4A7C15ull);
62 1074    z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
63 1074    z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
64 1074    return z ^ (z >> 31);
65 }
66 
67 1072double ygst_rng_uniform(ygst_rng *r) {
68 1072    return (double)(ygst_rng_next(r) >> 11) * (1.0 / 9007199254740992.0);
69 }
70 
71 /* ------------------------------------------------------------------ policy */
72 
73 96void ygst_retry_policy_default(ygst_retry_policy *p) {
74 96    memset(p, 0, sizeof *p);
75 96    p->max_retries = 2;
76 96    p->initial_delay_ms = 200;
77 96    p->multiplier = 2.0;
78 96    p->max_delay_ms = 4000;
79 96    p->jitter = 0.3;
80 96    p->respect_retry_after = 1;
81 96    p->max_retry_after_ms = 30000;
82 96    p->total_timeout_ms = 300000;
83 96    p->auto_retry = 0;
84 96    p->max_auto_retries = 2;
85 96    p->err_ids[0] = YGST_ERRID_RETRYABLE;
86 96    p->n_err_ids = 1;
87 96}
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 40int ygst_retry_policy_has_err_id(const ygst_retry_policy *p, int err_id) {
98     int i;
99 280    for (i = 0; i < p->n_err_ids; i++) {
100 253        if (p->err_ids[i] == err_id) return 1;
101     }
102 27    return 0;
103 }
104 
105 81long ygst_backoff_delay_ms(const ygst_retry_policy *p, int n, ygst_rng *rng) {
106     double base;
107 81    double factor = 1.0;
108     double d;
109 81    if (n < 1) n = 1;
110 81    base = (double)p->initial_delay_ms * pow(p->multiplier > 0 ? p->multiplier : 1.0, (double)(n - 1));
111 81    if (base > p->max_delay_ms) base = p->max_delay_ms;
112 81    if (p->jitter > 0 && rng) factor = 1.0 + (ygst_rng_uniform(rng) * 2.0 - 1.0) * p->jitter;
113 81    d = base * factor;
114 81    if (d < 0) d = 0;
115 81    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 235int ygst_is_retryable(int local_code, int biz_code, int http_status) {
177     const ygst_error_entry *e;
178 235    if (local_code) {
179 50        return local_code == YGST_LOCAL_NETWORK || local_code == YGST_LOCAL_TIMEOUT ||
180                local_code == YGST_LOCAL_RESULT_TIMEOUT;
181     }
182 185    if (biz_code) {
183 151        e = ygst_error_find(biz_code);
184 151        if (e) return e->retryable;
185     }
186 38    return ygst_http_retryable(http_status);
187 }
188 
189 23int ygst_http_fallback_code(int status) {
190 23    const char *cat = ygst_http_fallback_category(status);
191 23    if (strcmp(cat, "AUTH") == 0) return 40100;
192 21    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 204int ygst_errid_for(int local_code, int biz_code, int http_status, int retryable) {
204 204    if (local_code >= 60001 && local_code <= 60009) return local_code;
205 193    if (retryable) return YGST_ERRID_RETRYABLE;
206 100    if (biz_code) return biz_code;
207 44    if (local_code) return local_code;
208 23    return ygst_http_fallback_code(http_status);
209 }
210 
211 /* ------------------------------------------------------------------ classification */
212 
213 198void ygst_classification_init(ygst_classification *c) {
214 198    memset(c, 0, sizeof *c);
215 198    ygst_buf_init(&c->record_id);
216 198    ygst_buf_init(&c->audio_url);
217 198    ygst_buf_init(&c->message);
218 198}
219 
220 198void ygst_classification_free(ygst_classification *c) {
221 198    ygst_buf_free(&c->record_id);
222 198    ygst_buf_free(&c->audio_url);
223 198    ygst_buf_free(&c->message);
224 198}
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 59static void set_message(ygst_buf *m, const char *s) {
278 59    ygst_buf_reset(m);
279 59    ygst_buf_append_str(m, s ? s : "");
280 59    ygst_buf_cstr(m);
281 59}
282 
283 218int 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 218    int parsed = 0;
287 218    long long code = 0;
288 218    c->success = 0;
289 218    c->http_status = o->http_status;
290 218    c->local_code = o->local_code;
291 218    c->biz_code = 0;
292 218    c->has_audio_url = 0;
293 218    c->has_record_id = 0;
294 218    c->record_id_raw.p = NULL;
295 218    c->record_id_raw.n = 0;
296 218    c->result.p = NULL;
297 218    c->result.n = 0;
298 218    ygst_buf_reset(&c->record_id);
299 218    ygst_buf_reset(&c->audio_url);
300 218    ygst_buf_reset(&c->message);
301 
302 218    if (o->local_code || o->http_status <= 0) {
303         const ygst_error_entry *e;
304 43        if (!c->local_code) c->local_code = YGST_LOCAL_NETWORK;
305 43        e = ygst_local_find(c->local_code);
306 65        set_message(&c->message, (o->local_message && o->local_message[0]) ? o->local_message
307 22                                                                            : (e ? e->message : "network error"));
308 43        c->retryable = ygst_is_retryable(c->local_code, 0, 0);
309 43        c->err_id = ygst_errid_for(c->local_code, 0, 0, c->retryable);
310 43        return 0;
311     }
312 
313 175    if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o';
314 
315 175    if (o->http_status >= 200 && o->http_status < 300) {
316         ygst_span result, v;
317 62        if (parsed && ygst_json_member(root, "result", &result) == 1) {
318             ygst_span inner;
319 59            c->success = 1;
320 59            c->result = result;
321 59            if (ygst_json_member(root, "recordId", &v) == 1) {
322 56                c->has_record_id = 1;
323 56                c->record_id_raw = v;
324 56                if (ygst_json_type(v) == 's') {
325 55                    ygst_json_string(v, &c->record_id);
326                 } else {
327 1                    ygst_buf_append(&c->record_id, v.p, v.n);
328                 }
329             }
330 59            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 56            } 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 59            if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0;
337 59            ygst_buf_cstr(&c->record_id);
338 59            ygst_buf_cstr(&c->audio_url);
339 59            c->retryable = 0;
340 59            c->err_id = 0;
341 59            return 0;
342         }
343 3        c->local_code = YGST_LOCAL_PROTOCOL;
344 3        set_message(&c->message, parsed ? "invalid platform response: platform response has no result"
345                                         : "invalid platform response: not a JSON object");
346 3        c->retryable = 0;
347 3        c->err_id = YGST_LOCAL_PROTOCOL;
348 3        return 0;
349     }
350 
351 113    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 113    if (!c->message.len) {
371         char tmp[32];
372 6        snprintf(tmp, sizeof tmp, "HTTP %d", o->http_status);
373 6        set_message(&c->message, tmp);
374     }
375 113    ygst_buf_cstr(&c->message);
376 113    c->retryable = ygst_is_retryable(0, c->biz_code, o->http_status);
377 113    c->err_id = ygst_errid_for(0, c->biz_code, o->http_status, c->retryable);
378 113    return 0;
379 }
380 
381 25int ygst_error_message(const ygst_classification *c, int err_id, int attempts, ygst_buf *out) {
382 25    const char *m = c->message.data ? (const char *)c->message.data : "";
383 25    if (err_id == YGST_ERRID_RETRYABLE) {
384 10        const char *base = ygst_compat_message(YGST_ERRID_RETRYABLE);
385         size_t mark;
386 10        ygst_buf_append_str(out, base ? base : "");
387 10        ygst_buf_append_str(out, ": ");
388 10        mark = out->len;
389 10        if (c->http_status > 0) {
390 4            ygst_buf_append_str(out, "HTTP ");
391 4            ygst_buf_append_int(out, c->http_status);
392 4            if (c->biz_code) {
393 2                ygst_buf_append_str(out, " code=");
394 2                ygst_buf_append_int(out, c->biz_code);
395             }
396 6        } else if (c->local_code) {
397 3            const ygst_error_entry *e = ygst_local_find(c->local_code);
398 3            if (e) {
399 2                ygst_buf_append_str(out, e->name);
400             } else {
401 1                ygst_buf_append_str(out, "code=");
402 1                ygst_buf_append_int(out, c->local_code);
403             }
404 3        } else if (c->biz_code) {
405             /* a WebSocket error frame: a business code without an HTTP status */
406 2            ygst_buf_append_str(out, "code=");
407 2            ygst_buf_append_int(out, c->biz_code);
408         }
409 10        if (m[0]) {
410 10            if (out->len > mark) ygst_buf_append_char(out, ' ');
411 10            ygst_buf_append_str(out, m);
412         }
413 10        if (attempts > 0) {
414 9            ygst_buf_append_str(out, " (attempts ");
415 9            ygst_buf_append_int(out, attempts);
416 9            ygst_buf_append_char(out, ')');
417         }
418 15    } else if (m[0]) {
419 11        ygst_buf_append_str(out, m);
420     } else {
421 4        const char *cm = ygst_compat_message(err_id);
422 4        const ygst_error_entry *e = cm ? NULL : ygst_error_find(err_id);
423 4        if (!cm && !e) e = ygst_local_find(err_id);
424 4        ygst_buf_append_str(out, cm ? cm : (e ? e->message : ""));
425     }
426 25    ygst_buf_cstr(out);
427 25    return out->oom ? -1 : 0;
428 }
429 
430 /* ------------------------------------------------------------------ controller */
431 
432 73void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) {
433 73    memset(c, 0, sizeof *c);
434 73    if (p) {
435 72        c->policy = *p;
436     } else {
437 1        ygst_retry_policy_default(&c->policy);
438     }
439 73    ygst_rng_seed(&c->rng, seed);
440 73    c->start_ms = now_ms;
441 73}
442 
443 60static long apply_retry_after(const ygst_retry_policy *p, long delay, long retry_after_ms) {
444 60    if (p->respect_retry_after && retry_after_ms >= 0) {
445 4        long ra = retry_after_ms > p->max_retry_after_ms ? p->max_retry_after_ms : retry_after_ms;
446 4        if (ra > delay) delay = ra;
447     }
448 60    return delay;
449 }
450 
451 124ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) {
452     ygst_action a;
453 124    int64_t deadline = c->start_ms + c->policy.total_timeout_ms;
454 124    a.kind = YGST_ACT_FAIL;
455 124    a.delay_ms = 0;
456 124    a.err_id = cls->err_id;
457 124    a.auto_retry = 0;
458 124    c->attempt++;
459 124    c->total_attempts++;
460 124    if (cls->success) {
461 42        a.kind = YGST_ACT_SUCCESS;
462 42        a.err_id = 0;
463 42        return a;
464     }
465 82    if (cls->retryable && c->attempt <= c->policy.max_retries) {
466 49        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->attempt, &c->rng), retry_after_ms);
467 49        if (now_ms + d <= deadline) {
468 47            a.kind = YGST_ACT_RETRY;
469 47            a.delay_ms = d;
470 47            return a;
471         }
472 2        return a;
473     }
474 46    if (c->policy.auto_retry && c->submission < c->policy.max_auto_retries &&
475 13        ygst_retry_policy_has_err_id(&c->policy, cls->err_id)) {
476 11        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->submission + 1, &c->rng),
477                                    retry_after_ms);
478 11        if (now_ms + d <= deadline) {
479 10            c->submission++;
480 10            c->attempt = 0;
481 10            a.kind = YGST_ACT_RETRY;
482 10            a.delay_ms = d;
483 10            a.auto_retry = 1;
484 10            return a;
485         }
486     }
487 23    return a;
488 }