STKouyuEngine C core line coverage

Total 98.90% (2438 of 2465 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.c463463100.00%
Sources/STKouyuEngine/core/ygst_retry.c327327100.00%
Sources/STKouyuEngine/core/ygst_timing.c276276100.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 80static void put_le32(unsigned char *p, uint32_t v) {
14 80    p[0] = (unsigned char)v;
15 80    p[1] = (unsigned char)(v >> 8);
16 80    p[2] = (unsigned char)(v >> 16);
17 80    p[3] = (unsigned char)(v >> 24);
18 80}
19 
20 64static void put_le16(unsigned char *p, uint16_t v) {
21 64    p[0] = (unsigned char)v;
22 64    p[1] = (unsigned char)(v >> 8);
23 64}
24 
25 445static uint32_t le32(const unsigned char *p) {
26 445    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
27 }
28 
29 339static uint16_t le16(const unsigned char *p) {
30 339    return (uint16_t)(p[0] | (p[1] << 8));
31 }
32 
33 16void ygst_wav_header(unsigned char out[44], uint32_t data_len, int sample_rate, int channels, int bits) {
34 16    uint32_t block = (uint32_t)(channels * (bits / 8));
35 16    memcpy(out, "RIFF", 4);
36 16    put_le32(out + 4, data_len > 0xFFFFFFFFu - 36u ? 0xFFFFFFFFu : data_len + 36u);
37 16    memcpy(out + 8, "WAVE", 4);
38 16    memcpy(out + 12, "fmt ", 4);
39 16    put_le32(out + 16, 16);
40 16    put_le16(out + 20, 1);
41 16    put_le16(out + 22, (uint16_t)channels);
42 16    put_le32(out + 24, (uint32_t)sample_rate);
43 16    put_le32(out + 28, (uint32_t)sample_rate * block);
44 16    put_le16(out + 32, (uint16_t)block);
45 16    put_le16(out + 34, (uint16_t)bits);
46 16    memcpy(out + 36, "data", 4);
47 16    put_le32(out + 40, data_len);
48 16}
49 
50 122int ygst_is_riff_wave(const unsigned char *p, size_t n) {
51 122    return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0;
52 }
53 
54 120int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) {
55 120    size_t pos = 12;
56 120    int have_fmt = 0;
57 120    memset(info, 0, sizeof *info);
58 120    if (!ygst_is_riff_wave(p, n)) return -1;
59 332    while (pos + 8 <= n) {
60 332        uint32_t size = le32(p + pos + 4);
61 332        const unsigned char *id = p + pos;
62 332        size_t body = pos + 8;
63 332        if (memcmp(id, "fmt ", 4) == 0) {
64 114            if (size < 16 || body + 16 > n) return -1;
65 113            info->format = le16(p + body);
66 113            info->channels = le16(p + body + 2);
67 113            info->sample_rate = (int)le32(p + body + 4);
68 113            info->bits = le16(p + body + 14);
69 113            have_fmt = 1;
70 218        } else if (memcmp(id, "data", 4) == 0) {
71 114            if (!have_fmt) return -1;
72 113            info->data_offset = body;
73             /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */
74 113            info->data_len = (size == 0 || size > n - body) ? n - body : size;
75 113            return 0;
76         }
77 217        if (size > n - body) return -1;
78 216        pos = body + size + (size & 1u);
79     }
80 0    return -1;
81 }
82 
83 79long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) {
84     double per_second;
85 79    if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1;
86 77    per_second = (double)sample_rate * channels * (bits / 8.0);
87 77    if (per_second <= 0) return -1;
88 77    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 67static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) {
93 67    double per_second = (double)sample_rate * channels * (bits / 8.0);
94 67    if (per_second <= 0) return 0;
95 66    return (double)bytes * 1000.0 > (double)ms * per_second;
96 }
97 
98 27int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) {
99     long ms;
100 27    if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY;
101 26    if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE;
102 25    ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits);
103 25    if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
104 23    if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE;
105 21    return 0;
106 }
107 
108 53int ygst_audio_check_upload(const unsigned char *data, size_t len) {
109     ygst_wav_info info;
110 53    if (!data || len == 0) return YGST_ERRID_AUDIO_EMPTY;
111 51    if (len > YGST_MAX_AUDIO_BYTES) return YGST_ERRID_AUDIO_TOO_LARGE;
112 50    if (ygst_wav_parse(data, len, &info) == 0) {
113         long ms;
114 48        if (info.data_len == 0) return YGST_ERRID_AUDIO_EMPTY;
115 46        if (info.format == 1 || info.format == 3 || info.format == 0xFFFE) {
116 46            ms = ygst_pcm_duration_ms(info.data_len, info.sample_rate, info.channels, info.bits);
117 46            if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
118 44            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 45    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 5276void ygst_buf_init(ygst_buf *b) {
14 5276    b->data = NULL;
15 5276    b->len = 0;
16 5276    b->cap = 0;
17 5276    b->oom = 0;
18 5276}
19 
20 2638void ygst_buf_free(ygst_buf *b) {
21 2638    free(b->data);
22 2638    ygst_buf_init(b);
23 2638}
24 
25 4049void ygst_buf_reset(ygst_buf *b) {
26 4049    b->len = 0;
27 4049    b->oom = 0;
28 4049    if (b->data) b->data[0] = 0;
29 4049}
30 
31 29695int ygst_buf_reserve(ygst_buf *b, size_t extra) {
32     size_t need;
33     size_t cap;
34     unsigned char *p;
35 29695    if (b->oom) return -1;
36 29695    if (extra > (size_t)-1 - b->len - 1) {
37 0        b->oom = 1;
38 0        return -1;
39     }
40 29695    need = b->len + extra + 1; /* room for the terminating NUL */
41 29695    if (need <= b->cap) return 0;
42 3091    cap = b->cap ? b->cap : 64;
43 6394    while (cap < need) {
44 3303        if (cap > ((size_t)-1) / 2) {
45 0            cap = need;
46 0            break;
47         }
48 3303        cap *= 2;
49     }
50 3091    p = (unsigned char *)realloc(b->data, cap);
51 3091    if (!p) {
52 0        b->oom = 1;
53 0        return -1;
54     }
55 3091    b->data = p;
56 3091    b->cap = cap;
57 3091    return 0;
58 }
59 
60 29568int ygst_buf_append(ygst_buf *b, const void *p, size_t n) {
61 29568    if (n == 0) return b->oom ? -1 : 0;
62 29555    if (ygst_buf_reserve(b, n) != 0) return -1;
63 29555    memcpy(b->data + b->len, p, n);
64 29555    b->len += n;
65 29555    b->data[b->len] = 0;
66 29555    return 0;
67 }
68 
69 6013int ygst_buf_append_str(ygst_buf *b, const char *s) {
70 6013    return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
71 }
72 
73 14489int ygst_buf_append_char(ygst_buf *b, char c) {
74 14489    return ygst_buf_append(b, &c, 1);
75 }
76 
77 1095int ygst_buf_append_int(ygst_buf *b, long long v) {
78     char tmp[32];
79 1095    int n = snprintf(tmp, sizeof tmp, "%lld", v);
80 1095    if (n < 0) return -1;
81 1095    return ygst_buf_append(b, tmp, (size_t)n);
82 }
83 
84 9872const char *ygst_buf_cstr(ygst_buf *b) {
85 9872    if (!b->data) {
86 140        if (ygst_buf_reserve(b, 0) != 0) return "";
87 140        b->data[0] = 0;
88     }
89 9872    return (const char *)b->data;
90 }
91 
92 1092char *ygst_strdup(const char *s) {
93     size_t n;
94     char *p;
95 1092    if (!s) return NULL;
96 1091    n = strlen(s);
97 1091    p = (char *)malloc(n + 1);
98 1091    if (!p) return NULL;
99 1091    memcpy(p, s, n + 1);
100 1091    return p;
101 }
102 
103 990static int lower_ascii(int c) {
104 990    return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
105 }
106 
107 242int ygst_strcaseeq(const char *a, const char *b) {
108 242    if (!a || !b) return 0;
109 613    while (*a && *b) {
110 495        if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0;
111 372        a++;
112 372        b++;
113     }
114 118    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 564static int is_space(unsigned char c) {
128 564    return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
129 }
130 
131 164size_t ygst_trim_copy(const char *s, char *out, size_t cap) {
132     const char *e;
133     size_t n;
134 164    if (!out || cap == 0) return 0;
135 164    out[0] = 0;
136 164    if (!s) return 0;
137 169    while (*s && is_space((unsigned char)*s)) s++;
138 162    e = s + strlen(s);
139 167    while (e > s && is_space((unsigned char)e[-1])) e--;
140 162    n = (size_t)(e - s);
141 162    if (n >= cap) n = cap - 1;
142 162    memcpy(out, s, n);
143 162    out[n] = 0;
144 162    return n;
145 }
146 
147 242int ygst_is_blank(const char *s) {
148 242    if (!s) return 1;
149 244    while (*s) {
150 232        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 16097static 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 273649    for (i = 0; i < 16; i++) {
29 257552        w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) |
30 257552               (uint32_t)p[i * 4 + 3];
31     }
32 788753    for (i = 16; i < 64; i++) {
33 772656        uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3);
34 772656        uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10);
35 772656        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
36     }
37 16097    a = c->h[0];
38 16097    b = c->h[1];
39 16097    cc = c->h[2];
40 16097    d = c->h[3];
41 16097    e = c->h[4];
42 16097    f = c->h[5];
43 16097    g = c->h[6];
44 16097    h = c->h[7];
45 1046305    for (i = 0; i < 64; i++) {
46 1030208        uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
47 1030208        uint32_t ch = (e & f) ^ ((~e) & g);
48 1030208        uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
49 1030208        uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
50 1030208        t1 = h + S1 + ch + K[i] + w[i];
51 1030208        t2 = S0 + maj;
52 1030208        h = g;
53 1030208        g = f;
54 1030208        f = e;
55 1030208        e = d + t1;
56 1030208        d = cc;
57 1030208        cc = b;
58 1030208        b = a;
59 1030208        a = t1 + t2;
60     }
61 16097    c->h[0] += a;
62 16097    c->h[1] += b;
63 16097    c->h[2] += cc;
64 16097    c->h[3] += d;
65 16097    c->h[4] += e;
66 16097    c->h[5] += f;
67 16097    c->h[6] += g;
68 16097    c->h[7] += h;
69 16097}
70 
71 199void ygst_sha256_init(ygst_sha256_ctx *c) {
72 199    c->h[0] = 0x6a09e667u;
73 199    c->h[1] = 0xbb67ae85u;
74 199    c->h[2] = 0x3c6ef372u;
75 199    c->h[3] = 0xa54ff53au;
76 199    c->h[4] = 0x510e527fu;
77 199    c->h[5] = 0x9b05688cu;
78 199    c->h[6] = 0x1f83d9abu;
79 199    c->h[7] = 0x5be0cd19u;
80 199    c->total = 0;
81 199    c->used = 0;
82 199}
83 
84 1383void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) {
85 1383    const unsigned char *p = (const unsigned char *)data;
86 1383    c->total += (uint64_t)n;
87 1383    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 16251    while (n >= 64) {
99 14880        sha256_block(c, p);
100 14880        p += 64;
101 14880        n -= 64;
102     }
103 1371    if (n) {
104 1189        memcpy(c->block, p, n);
105 1189        c->used = n;
106     }
107 }
108 
109 199void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) {
110 199    uint64_t bits = c->total * 8u;
111     int i;
112 199    c->block[c->used++] = 0x80;
113 199    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 199    memset(c->block + c->used, 0, 56 - c->used);
119 1791    for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i));
120 199    sha256_block(c, c->block);
121 1791    for (i = 0; i < 8; i++) {
122 1592        out[i * 4] = (unsigned char)(c->h[i] >> 24);
123 1592        out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16);
124 1592        out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8);
125 1592        out[i * 4 + 3] = (unsigned char)c->h[i];
126     }
127 199}
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 80void 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 80    memset(k, 0, sizeof k);
143 80    if (key_len > 64) {
144 1        ygst_sha256(key, key_len, k);
145 79    } else if (key_len) {
146 79        memcpy(k, key, key_len);
147     }
148 5200    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36);
149 80    ygst_sha256_init(&c);
150 80    ygst_sha256_update(&c, pad, 64);
151 80    ygst_sha256_update(&c, msg, msg_len);
152 80    ygst_sha256_final(&c, inner);
153 5200    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c);
154 80    ygst_sha256_init(&c);
155 80    ygst_sha256_update(&c, pad, 64);
156 80    ygst_sha256_update(&c, inner, 32);
157 80    ygst_sha256_final(&c, out);
158 80}
159 
160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
161 
162 108size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) {
163 108    size_t need = ((n + 2) / 3) * 4;
164     size_t i;
165 108    size_t o = 0;
166 108    if (!out || cap < need + 1) return 0;
167 991    for (i = 0; i + 2 < n; i += 3) {
168 885        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2];
169 885        out[o++] = B64[(v >> 18) & 63];
170 885        out[o++] = B64[(v >> 12) & 63];
171 885        out[o++] = B64[(v >> 6) & 63];
172 885        out[o++] = B64[v & 63];
173     }
174 106    if (n - i == 1) {
175 24        uint32_t v = (uint32_t)in[i] << 16;
176 24        out[o++] = B64[(v >> 18) & 63];
177 24        out[o++] = B64[(v >> 12) & 63];
178 24        out[o++] = '=';
179 24        out[o++] = '=';
180 82    } else if (n - i == 2) {
181 79        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
182 79        out[o++] = B64[(v >> 18) & 63];
183 79        out[o++] = B64[(v >> 12) & 63];
184 79        out[o++] = B64[(v >> 6) & 63];
185 79        out[o++] = '=';
186     }
187 106    out[o] = 0;
188 106    return o;
189 }
190 
191 194void ygst_hex_lower(const unsigned char *in, size_t n, char *out) {
192     static const char H[] = "0123456789abcdef";
193     size_t i;
194 2754    for (i = 0; i < n; i++) {
195 2560        out[i * 2] = H[in[i] >> 4];
196 2560        out[i * 2 + 1] = H[in[i] & 15];
197     }
198 194    out[n * 2] = 0;
199 194}

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 106static 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 106    z += 719468;
18 106    era = (z >= 0 ? z : z - 146096) / 146097;
19 106    doe = (unsigned)(z - era * 146097);
20 106    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
21 106    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
22 106    mp = (5 * doy + 2) / 153;
23 106    *d = doy - (153 * mp + 2) / 5 + 1;
24 106    *m = mp < 10 ? mp + 3 : mp - 9;
25 106    *y = (long long)yoe + era * 400 + (*m <= 2);
26 106}
27 
28 106int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) {
29 106    long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL;
30 106    long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL);
31 106    long long rem = ms - days * 86400000LL;
32     long long y;
33     unsigned m, d, hh, mi, ss, mss;
34 106    civil_from_days(days, &y, &m, &d);
35 106    if (y < 0) y = 0;
36 106    if (y > 9999) y = 9999;
37 106    hh = (unsigned)(rem / 3600000LL) % 24u;
38 106    mi = (unsigned)(rem / 60000LL) % 60u;
39 106    ss = (unsigned)(rem / 1000LL) % 60u;
40 106    mss = (unsigned)rem % 1000u;
41 106    snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss,
42              mss);
43 106    return 0;
44 }
45 
46 64int ygst_align_result(ygst_span result, ygst_buf *out) {
47     ygst_span sentences, sent, details, d, scores, v, so, sp;
48 64    size_t spos = 0;
49 64    size_t copied = 0; /* bytes of result already copied to out */
50 64    int added = 0;
51 64    if (!result.p) return -1;
52 116    if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 ||
53 53        ygst_json_type(sentences) != 'a') {
54 11        return ygst_buf_append(out, result.p, result.n) == 0 ? 0 : -1;
55     }
56 109    while (ygst_json_array_next(sentences, &spos, &sent) == 1) {
57 57        size_t dpos = 0;
58 109        if (ygst_json_type(sent) != 'o' || ygst_json_member(sent, "details", &details) != 1 ||
59 52            ygst_json_type(details) != 'a') {
60 6            continue;
61         }
62 343        while (ygst_json_array_next(details, &dpos, &d) == 1) {
63             int add_overall, add_pron;
64             size_t close;
65 292            if (ygst_json_type(d) != 'o' || ygst_json_member(d, "overall", &v) == 1) continue;
66 290            if (ygst_json_member(d, "scores", &scores) != 1 || ygst_json_type(scores) != 'o') continue;
67 288            add_overall = ygst_json_member(scores, "overall", &so) == 1;
68 288            add_pron = ygst_json_member(d, "pronunciation", &v) != 1 && ygst_json_member(scores, "pronunciation", &sp) == 1;
69 288            if (!add_overall && !add_pron) continue;
70 287            close = (size_t)(d.p + d.n - 1 - result.p); /* the closing brace of the item */
71 287            ygst_buf_append(out, result.p + copied, close - copied);
72 287            if (add_overall) {
73 287                ygst_buf_append_str(out, ",\"overall\":");
74 287                ygst_buf_append(out, so.p, so.n);
75 287                added++;
76             }
77 287            if (add_pron) {
78 284                ygst_buf_append_str(out, ",\"pronunciation\":");
79 284                ygst_buf_append(out, sp.p, sp.n);
80 284                added++;
81             }
82 287            copied = close;
83         }
84     }
85 52    ygst_buf_append(out, result.p + copied, result.n - copied);
86 52    return out->oom ? -1 : added;
87 }
88 
89 1053static void key(ygst_buf *out, const char *k, int first) {
90 1053    if (!first) ygst_buf_append_char(out, ',');
91 1053    ygst_json_escape(out, k);
92 1053    ygst_buf_append_char(out, ':');
93 1053}
94 
95 54int 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 54    if (!result_raw.p || result_raw.n == 0) return -1;
99 53    ygst_buf_append_char(out, '{');
100 53    key(out, "tokenId", 1);
101 53    ygst_json_escape(out, token_id);
102 53    if (record_id_json && record_id_json[0]) {
103 52        key(out, "recordId", 0);
104 52        ygst_buf_append_str(out, record_id_json);
105     }
106 53    key(out, "applicationId", 0);
107 53    ygst_json_escape(out, app_key);
108 53    key(out, "userId", 0);
109 53    ygst_json_escape(out, user_id);
110 53    key(out, "refText", 0);
111 53    ygst_json_escape(out, ref_text);
112 53    key(out, "eof", 0);
113 53    ygst_buf_append_char(out, '1');
114 53    key(out, "dtLastResponse", 0);
115 53    ygst_json_escape(out, dt_last_response);
116 53    key(out, "result", 0);
117 53    if (ygst_align_result(result_raw, out) < 0) return -1;
118 53    if (params_json && params_json[0]) {
119 3        key(out, "params", 0);
120 3        ygst_buf_append_str(out, params_json);
121     }
122 53    if (audio_url && audio_url[0]) {
123 3        key(out, "audioUrl", 0);
124 3        ygst_json_escape(out, audio_url);
125     }
126 53    ygst_buf_append_char(out, '}');
127 53    ygst_buf_cstr(out);
128 53    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 60static int append_progress_result(ygst_buf *out, ygst_span result, int skip) {
151     ygst_span words, item;
152 60    size_t pos = 0;
153 60    int i = 0;
154 60    int first = 1;
155 62    if (skip <= 0 || ygst_json_type(result) != 'o' || ygst_json_member(result, "words", &words) != 1 ||
156 2        ygst_json_type(words) != 'a') {
157 58        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 62int 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 62    if (!result_raw.p || result_raw.n == 0) return -1;
176 60    ygst_buf_append_char(out, '{');
177 60    key(out, "tokenId", 1);
178 60    ygst_json_escape(out, token_id);
179     /* Shengtong's intermediate results carry recordId; the platform names the record only in the final */
180 60    key(out, "recordId", 0);
181 60    ygst_buf_append_str(out, "\"\"");
182 60    key(out, "applicationId", 0);
183 60    ygst_json_escape(out, app_key);
184 60    key(out, "userId", 0);
185 60    ygst_json_escape(out, user_id);
186 60    key(out, "refText", 0);
187 60    ygst_json_escape(out, ref_text);
188 60    key(out, "eof", 0);
189 60    ygst_buf_append_char(out, '0');
190 60    key(out, "dtLastResponse", 0);
191 60    ygst_json_escape(out, dt_last_response);
192 60    key(out, "result", 0);
193 60    if (append_progress_result(out, result_raw, skip_words) != 0) return -1;
194 60    ygst_buf_append_char(out, '}');
195 60    ygst_buf_cstr(out);
196 60    return out->oom ? -1 : 0;
197 }
198 
199 4int 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 4    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
205 4    ygst_buf_append_str(out, "{\"app\":{");
206 4    key(out, "applicationId", 1);
207 4    ygst_json_escape(out, app_key);
208 4    key(out, "userId", 0);
209 4    ygst_json_escape(out, user_id);
210 4    key(out, "timestamp", 0);
211 4    ygst_json_escape(out, ts);
212 4    ygst_buf_append_str(out, "},\"audio\":{");
213 4    key(out, "audioType", 1);
214 4    ygst_json_escape(out, audio_type);
215 4    key(out, "sampleRate", 0);
216 4    ygst_buf_append_int(out, sample_rate);
217 4    key(out, "channel", 0);
218 4    ygst_buf_append_int(out, channel);
219 4    key(out, "sampleBytes", 0);
220 4    ygst_buf_append_int(out, sample_bytes);
221 4    ygst_buf_append_str(out, "},\"request\":{");
222 4    key(out, "coreType", 1);
223 4    ygst_json_escape(out, core_type);
224 4    key(out, "tokenId", 0);
225 4    ygst_json_escape(out, token_id);
226 4    if (request_fields) {
227 12        for (i = 0; i < request_fields->count; i++) {
228 9            const char *n = request_fields->items[i].name;
229 9            if (strcmp(n, "coreType") == 0 || strcmp(n, "tokenId") == 0) continue;
230 8            key(out, n, 0);
231 8            ygst_json_escape(out, request_fields->items[i].value);
232         }
233     }
234 4    ygst_buf_append_str(out, "}}");
235 4    ygst_buf_cstr(out);
236 4    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 44int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) {
318     size_t n;
319     int w;
320 44    if (!base || !core_type || !out) return -1;
321 43    n = strlen(base);
322 44    while (n > 0 && base[n - 1] == '/') n--;
323 43    w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type);
324 43    return (w < 0 || (size_t)w >= cap) ? -1 : 0;
325 }
326 
327 63int ygst_token_id(const unsigned char rnd[16], char out[33]) {
328 63    ygst_hex_lower(rnd, 16, out);
329 63    return 0;
330 }
331 
332 65int ygst_idempotency_key_ok(const char *k) {
333 65    size_t n = 0;
334 65    if (!k) return 0;
335 2265    for (; k[n]; n++) {
336 2206        unsigned char c = (unsigned char)k[n];
337 2206        if (c < 0x21 || c > 0x7E) return 0;
338 2202        if (n >= 200) return 0;
339     }
340 59    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 550void ygst_fields_init(ygst_fields *f) {
14 550    f->items = NULL;
15 550    f->count = 0;
16 550    f->cap = 0;
17 550    f->oom = 0;
18 550}
19 
20 271void ygst_fields_free(ygst_fields *f) {
21     size_t i;
22 810    for (i = 0; i < f->count; i++) {
23 539        free(f->items[i].name);
24 539        free(f->items[i].value);
25     }
26 271    free(f->items);
27 271    ygst_fields_init(f);
28 271}
29 
30 562static long find_index(const ygst_fields *f, const char *name) {
31     size_t i;
32 2727    for (i = 0; i < f->count; i++) {
33 2182        if (strcmp(f->items[i].name, name) == 0) return (long)i;
34     }
35 545    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 552int ygst_fields_set(ygst_fields *f, const char *name, const char *value) {
49     long i;
50     char *v;
51 552    if (!name || f->oom) return -1;
52 551    if (!value) {
53 1        ygst_fields_remove(f, name);
54 1        return 0;
55     }
56 550    i = find_index(f, name);
57 550    v = ygst_strdup(value);
58 550    if (!v) {
59 0        f->oom = 1;
60 0        return -1;
61     }
62 550    if (i >= 0) {
63 10        free(f->items[i].value);
64 10        f->items[i].value = v;
65 10        return 0;
66     }
67 540    if (f->count == f->cap) {
68 195        size_t cap = f->cap ? f->cap * 2 : 16;
69 195        ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field));
70 195        if (!p) {
71 0            free(v);
72 0            f->oom = 1;
73 0            return -1;
74         }
75 195        f->items = p;
76 195        f->cap = cap;
77     }
78 540    f->items[f->count].name = ygst_strdup(name);
79 540    if (!f->items[f->count].name) {
80 0        free(v);
81 0        f->oom = 1;
82 0        return -1;
83     }
84 540    f->items[f->count].value = v;
85 540    f->count++;
86 540    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 44int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) {
95     size_t i;
96 183    for (i = 0; i < src->count; i++) {
97 139        if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1;
98     }
99 44    return 0;
100 }
101 
102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */
103 800static uint32_t next_cp(const unsigned char **s) {
104 800    const unsigned char *p = *s;
105 800    uint32_t c = p[0];
106 800    int n = 0;
107     int i;
108 800    if (c < 0x80) {
109 790        *s = p + 1;
110 790        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 800static int utf16_units(uint32_t cp, uint32_t u[2]) {
138 800    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 796    u[0] = cp;
145 796    return 1;
146 }
147 
148 325int ygst_utf16_compare(const char *a, const char *b) {
149 325    const unsigned char *pa = (const unsigned char *)a;
150 325    const unsigned char *pb = (const unsigned char *)b;
151     uint32_t ua[2], ub[2];
152 325    int na = 0, nb = 0, ia = 0, ib = 0;
153 77    for (;;) {
154         uint32_t ca, cb;
155 402        if (ia == na) {
156 402            if (!*pa) {
157 2                na = 0;
158             } else {
159 400                na = utf16_units(next_cp(&pa), ua);
160             }
161 402            ia = 0;
162         }
163 402        if (ib == nb) {
164 402            if (!*pb) {
165 2                nb = 0;
166             } else {
167 400                nb = utf16_units(next_cp(&pb), ub);
168             }
169 402            ib = 0;
170         }
171 402        if (na == 0 || nb == 0) {
172 3            if (na == 0 && nb == 0) return 0;
173 2            return na == 0 ? -1 : 1;
174         }
175 399        ca = ua[ia++];
176 399        cb = ub[ib++];
177 399        if (ca != cb) return ca < cb ? -1 : 1;
178     }
179 }
180 
181 317static int cmp_field_ptr(const void *x, const void *y) {
182 317    const ygst_field *a = *(const ygst_field *const *)x;
183 317    const ygst_field *b = *(const ygst_field *const *)y;
184 317    return ygst_utf16_compare(a->name, b->name);
185 }
186 
187 86int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) {
188     const ygst_field **v;
189 86    size_t n = 0;
190     size_t i;
191 86    int first = 1;
192 86    if (f->oom) return -1;
193 86    v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v));
194 86    if (!v) return -1;
195 388    for (i = 0; i < f->count; i++) {
196 302        if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i];
197     }
198 86    qsort(v, n, sizeof(*v), cmp_field_ptr);
199 366    for (i = 0; i < n; i++) {
200 280        if (!first) ygst_buf_append_char(out, '&');
201 280        first = 0;
202 280        ygst_buf_append_str(out, v[i]->name);
203 280        ygst_buf_append_char(out, '=');
204 280        ygst_buf_append_str(out, v[i]->value);
205     }
206 86    free(v);
207 86    ygst_buf_cstr(out);
208 86    return out->oom ? -1 : 0;
209 }
210 
211 78int 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 78    out[0] = 0;
216 78    if (!secret) return -1;
217 77    ygst_buf_init(&payload);
218 77    rc = ygst_sign_payload(f, &payload);
219 77    if (rc == 0) {
220 77        ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"",
221                          payload.len, mac);
222 77        if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1;
223     }
224 77    ygst_buf_free(&payload);
225 77    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 5664988static const char *ws(const char *p, const char *e) {
16 11646031    while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
17 5664988    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 1555671static const char *skip_string(const char *p, const char *e) {
28 1555671    if (p >= e || *p != '"') return NULL;
29 1555668    p++;
30 11230030    while (p < e) {
31 11230028        unsigned char c = (unsigned char)*p;
32 11230028        if (c == '"') return p + 1;
33 9674365        if (c < 0x20) return NULL;
34 9674364        if (c == '\\') {
35 792            p++;
36 792            if (p >= e) return NULL;
37 792            switch (*p) {
38 767            case '"':
39             case '\\':
40             case '/':
41             case 'b':
42             case 'f':
43             case 'n':
44             case 'r':
45             case 't':
46 767                p++;
47 767                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 790            continue;
61         }
62 9673572        p++;
63     }
64 2    return NULL;
65 }
66 
67 1483640static int is_digit(char c) {
68 1483640    return c >= '0' && c <= '9';
69 }
70 
71 453879static const char *skip_number(const char *p, const char *e) {
72 453879    if (p < e && *p == '-') p++;
73 453879    if (p >= e) return NULL;
74 453878    if (*p == '0') {
75 162568        p++;
76 291310    } else if (*p >= '1' && *p <= '9') {
77 953260        while (p < e && is_digit(*p)) p++;
78     } else {
79 0        return NULL;
80     }
81 453878    if (p < e && *p == '.') {
82 24802        p++;
83 24802        if (p >= e || !is_digit(*p)) return NULL;
84 59713        while (p < e && is_digit(*p)) p++;
85     }
86 453877    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 453876    return p;
93 }
94 
95 240886static const char *skip_lit(const char *p, const char *e, const char *lit) {
96 240886    size_t n = strlen(lit);
97 240886    if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL;
98 240882    return p + n;
99 }
100 
101 1398134static const char *skip_value(const char *p, const char *e, int depth) {
102 1398134    if (depth > YGST_JSON_MAX_DEPTH) return NULL;
103 1398133    p = ws(p, e);
104 1398133    if (p >= e) return NULL;
105 1398129    switch (*p) {
106 257923    case '{':
107 257923        p = ws(p + 1, e);
108 257923        if (p < e && *p == '}') return p + 1;
109         for (;;) {
110 1095090            p = skip_string(ws(p, e), e);
111 1095090            if (!p) return NULL;
112 1095087            p = ws(p, e);
113 1095087            if (p >= e || *p != ':') return NULL;
114 1095086            p = skip_value(p + 1, e, depth + 1);
115 1095086            if (!p) return NULL;
116 1095084            p = ws(p, e);
117 1095084            if (p >= e) return NULL;
118 1095084            if (*p == ',') {
119 837222                p++;
120 837222                continue;
121             }
122 257862            if (*p == '}') return p + 1;
123 1            return NULL;
124         }
125 91346    case '[':
126 91346        p = ws(p + 1, e);
127 91346        if (p < e && *p == ']') return p + 1;
128         for (;;) {
129 172231            p = skip_value(p, e, depth + 1);
130 172231            if (!p) return NULL;
131 172100            p = ws(p, e);
132 172100            if (p >= e) return NULL;
133 172100            if (*p == ',') {
134 106187                p++;
135 106187                continue;
136             }
137 65913            if (*p == ']') return p + 1;
138 1            return NULL;
139         }
140 355966    case '"':
141 355966        return skip_string(p, e);
142 112839    case 't':
143 112839        return skip_lit(p, e, "true");
144 128022    case 'f':
145 128022        return skip_lit(p, e, "false");
146 25    case 'n':
147 25        return skip_lit(p, e, "null");
148 452008    default:
149 452008        if (*p == '-' || is_digit(*p)) return skip_number(p, e);
150 6        return NULL;
151     }
152 }
153 
154 675int 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 675    if (!s) return -1;
159 674    e = s + n;
160 674    start = ws(s, e);
161 674    end = skip_value(start, e, 0);
162 674    if (!end) return -1;
163 646    if (ws(end, e) != e) return -1;
164 644    if (root) {
165 644        root->p = start;
166 644        root->n = (size_t)(end - start);
167     }
168 644    return 0;
169 }
170 
171 230033int ygst_json_type(ygst_span v) {
172 230033    if (!v.p || v.n == 0) return 0;
173 230032    switch (v.p[0]) {
174 151450    case '{':
175 151450        return 'o';
176 55260    case '[':
177 55260        return 'a';
178 6881    case '"':
179 6881        return 's';
180 6720    case 't':
181 6720        return 't';
182 6649    case 'f':
183 6649        return 'f';
184 6    case 'n':
185 6        return 'z';
186 3066    default:
187 3066        return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0;
188     }
189 }
190 
191 122944int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) {
192 122944    const char *e = obj.p + obj.n;
193     const char *p;
194     const char *k;
195     const char *v;
196 122944    if (ygst_json_type(obj) != 'o') return -1;
197 122943    p = obj.p + (*pos ? *pos : 1);
198 122943    p = ws(p, e);
199 122943    if (p >= e) return -1;
200 122943    if (*p == '}') return 0;
201 100345    if (*pos) {
202 77754        if (*p != ',') return -1;
203 77754        p = ws(p + 1, e);
204     }
205 100345    k = p;
206 100345    p = skip_string(p, e);
207 100345    if (!p) return -1;
208 100345    if (key) {
209 100345        key->p = k;
210 100345        key->n = (size_t)(p - k);
211     }
212 100345    p = ws(p, e);
213 100345    if (p >= e || *p != ':') return -1;
214 100344    v = ws(p + 1, e);
215 100344    p = skip_value(v, e, 1);
216 100344    if (!p) return -1;
217 100344    if (value) {
218 100344        value->p = v;
219 100344        value->n = (size_t)(p - v);
220     }
221 100344    *pos = (size_t)(p - obj.p);
222 100344    return 1;
223 }
224 
225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */
226 98495static int key_equals(ygst_span tok, const char *key) {
227 98495    size_t klen = strlen(key);
228 98495    if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) {
229 98490        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 22328int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) {
241 22328    size_t pos = 0;
242     ygst_span k, v;
243 22328    int found = 0;
244     int rc;
245 22328    if (ygst_json_type(obj) != 'o' || !key) return -1;
246 120822    while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) {
247 98495        if (key_equals(k, key)) {
248 20951            found = 1;
249 20951            if (out) *out = v;
250         }
251     }
252 22327    if (rc < 0) return -1;
253 22326    return found;
254 }
255 
256 30318int ygst_json_array_next(ygst_span arr, size_t *pos, ygst_span *out) {
257 30318    const char *e = arr.p + arr.n;
258     const char *p;
259     const char *v;
260 30318    if (ygst_json_type(arr) != 'a') return -1;
261 30317    p = arr.p + (*pos ? *pos : 1);
262 30317    p = ws(p, e);
263 30317    if (p >= e) return -1;
264 30317    if (*p == ']') return 0;
265 29800    if (*pos) {
266 27203        if (*p != ',') return -1;
267 27202        p = ws(p + 1, e);
268     }
269 29799    v = p;
270 29799    p = skip_value(v, e, 1);
271 29799    if (!p) return -1;
272 29799    if (out) {
273 29799        out->p = v;
274 29799        out->n = (size_t)(p - v);
275     }
276 29799    *pos = (size_t)(p - arr.p);
277 29799    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 4271int ygst_json_string(ygst_span v, ygst_buf *out) {
308     const char *p;
309     const char *e;
310 4271    if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1;
311 4269    p = v.p + 1;
312 4269    e = v.p + v.n - 1;
313 4352    while (p < e) {
314 4276        const char *run = p;
315 31392        while (p < e && *p != '\\') p++;
316 4276        if (p > run) ygst_buf_append(out, run, (size_t)(p - run));
317 4276        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 4269    ygst_buf_cstr(out);
368 4269    return out->oom ? -1 : 0;
369 }
370 
371 1879static int number_text(ygst_span v, char *tmp, size_t cap) {
372 1879    if (ygst_json_type(v) != 'n' || v.n >= cap) return -1;
373 1877    if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1;
374 1877    memcpy(tmp, v.p, v.n);
375 1877    tmp[v.n] = 0;
376 1877    return 0;
377 }
378 
379 21int ygst_json_number(ygst_span v, double *out) {
380     char tmp[64];
381 21    char *end = NULL;
382 21    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
383 20    *out = strtod(tmp, &end);
384 20    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 20    return 0;
394 }
395 
396 1858int ygst_json_int(ygst_span v, long long *out) {
397     char tmp[64];
398     size_t i;
399 1858    int neg = 0;
400 1858    long long acc = 0;
401 1858    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
402 1857    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 1853    i = 0;
410 1853    if (tmp[0] == '-') {
411 1        neg = 1;
412 1        i = 1;
413     }
414 6430    for (; tmp[i]; i++) {
415 4578        int dgt = tmp[i] - '0';
416 4578        if (acc > (9223372036854775807LL - dgt) / 10) return -1;
417 4577        acc = acc * 10 + dgt;
418     }
419 1852    *out = neg ? -acc : acc;
420 1852    return 0;
421 }
422 
423 13369int ygst_json_bool(ygst_span v, int *out) {
424 13369    int t = ygst_json_type(v);
425 13369    if (t == 't' && v.n == 4) {
426 6719        *out = 1;
427 6719        return 0;
428     }
429 6650    if (t == 'f' && v.n == 5) {
430 6649        *out = 0;
431 6649        return 0;
432     }
433 1    return -1;
434 }
435 
436 2049int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) {
437     static const char H[] = "0123456789abcdef";
438     size_t i;
439 2049    size_t run = 0;
440 2049    ygst_buf_append_char(out, '"');
441 49453    for (i = 0; i < n; i++) {
442 47404        unsigned char c = (unsigned char)s[i];
443 47404        const char *rep = NULL;
444         char u[7];
445 47404        if (c == '"') {
446 17            rep = "\\\"";
447 47387        } else if (c == '\\') {
448 2            rep = "\\\\";
449 47385        } else if (c == '\n') {
450 3            rep = "\\n";
451 47382        } else if (c == '\r') {
452 1            rep = "\\r";
453 47381        } else if (c == '\t') {
454 1            rep = "\\t";
455 47380        } else if (c == '\b') {
456 1            rep = "\\b";
457 47379        } else if (c == '\f') {
458 1            rep = "\\f";
459 47378        } 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 47404        if (rep) {
470 28            if (i > run) ygst_buf_append(out, s + run, i - run);
471 28            ygst_buf_append_str(out, rep);
472 28            run = i + 1;
473         }
474     }
475 2049    if (n > run) ygst_buf_append(out, s + run, n - run);
476 2049    ygst_buf_append_char(out, '"');
477 2049    return out->oom ? -1 : 0;
478 }
479 
480 2049int ygst_json_escape(ygst_buf *out, const char *s) {
481 2049    if (!s) s = "";
482 2049    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 42int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) {
15     char hex[33];
16 42    ygst_hex_lower(rnd, 16, hex);
17 42    snprintf(out, 64, "----YuguSTCompat%s", hex);
18 42    return 0;
19 }
20 
21 43int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) {
22     int n;
23 43    if (!boundary || !out) return -1;
24 43    n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary);
25 43    return (n < 0 || (size_t)n >= cap) ? -1 : 0;
26 }
27 
28 136static int contains(const unsigned char *hay, size_t n, const char *needle) {
29 136    size_t m = strlen(needle);
30     size_t i;
31 136    if (m == 0 || n < m) return 0;
32 2983774    for (i = 0; i + m <= n; i++) {
33 2983727        if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1;
34     }
35 47    return 0;
36 }
37 
38 49int 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 49    if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1;
43 47    if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1;
44 138    for (i = 0; i < fields->count; i++) {
45 94        const char *v = fields->items[i].value ? fields->items[i].value : "";
46 94        if (!ygst_field_name_ok(fields->items[i].name)) return -1;
47 93        if (contains((const unsigned char *)v, strlen(v), boundary)) return -2;
48     }
49 44    if (file && contains(file, file_len, boundary)) return -2;
50 135    for (i = 0; i < fields->count; i++) {
51 92        ygst_buf_append_str(out, "--");
52 92        ygst_buf_append_str(out, boundary);
53 92        ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
54 92        ygst_buf_append_str(out, fields->items[i].name);
55 92        ygst_buf_append_str(out, "\"\r\n\r\n");
56 92        ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : "");
57 92        ygst_buf_append_str(out, "\r\n");
58     }
59 43    ygst_buf_append_str(out, "--");
60 43    ygst_buf_append_str(out, boundary);
61 43    ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
62 43    ygst_buf_append_str(out, file_field);
63 43    ygst_buf_append_str(out, "\"; filename=\"");
64 43    ygst_buf_append_str(out, filename);
65 43    ygst_buf_append_str(out, "\"\r\nContent-Type: ");
66 43    ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream");
67 43    ygst_buf_append_str(out, "\r\n\r\n");
68 43    if (file_len) ygst_buf_append(out, file, file_len);
69 43    ygst_buf_append_str(out, "\r\n--");
70 43    ygst_buf_append_str(out, boundary);
71 43    ygst_buf_append_str(out, "--\r\n");
72 43    return out->oom ? -1 : 0;
73 }
74 
75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */
76 63const 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 63    if (ygst_is_blank(audio_type)) return "wav";
82 61    ygst_trim_copy(audio_type, t, sizeof t);
83 61    if (t[0] == '.') memmove(t, t + 1, strlen(t));
84 118    for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) {
85 115        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 52const char *ygst_audio_content_type(const char *ext) {
93 52    if (!ext) return "application/octet-stream";
94 51    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 118ygst_ct_status ygst_coretype_check(const char *core_type) {
63     size_t i;
64 118    if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN;
65 894    for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) {
66 859        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 72ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) {
75     const char *mapped;
76 72    if (!out || cap == 0) return YGST_CT_UNKNOWN;
77 71    out[0] = 0;
78 71    if (!ygst_is_blank(core_type_ns)) {
79 65        ygst_trim_copy(core_type_ns, out, cap);
80 65        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 196int ygst_coretype_is_paragraph(const char *core_type) {
94 196    return core_type && strncmp(core_type, "para.", 5) == 0;
95 }
96 
97 98int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) {
98 98    if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1");
99 91    return 0;
100 }
101 
102 70int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text,
103                           const char *ref_pinyin) {
104 70    if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED;
105 65    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 200int ygst_field_name_ok(const char *name) {
178 200    if (!name || !name[0]) return 0;
179 1925    for (; *name; name++) {
180 1732        if (*name == '"' || *name == '\r' || *name == '\n') return 0;
181     }
182 193    return 1;
183 }
184 
185 /* ------------------------------------------------------------------ mapping */
186 
187 91void ygst_test_params_init(ygst_test_params *p) {
188 91    memset(p, 0, sizeof *p);
189 91}
190 
191 963static int set_str(ygst_fields *f, const char *name, const char *v) {
192 963    if (!v || !v[0]) return 0;
193 77    return ygst_fields_set(f, name, v);
194 }
195 
196 672static int set_flag(ygst_fields *f, const char *name, int on) {
197 672    return on ? ygst_fields_set(f, name, "1") : 0;
198 }
199 
200 481static int set_num(ygst_fields *f, const char *name, double v) {
201     char tmp[64];
202 481    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 96int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) {
213     static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"};
214 96    int rc = 0;
215 96    rc |= set_str(out, "refText", p->ref_text);
216 96    rc |= set_str(out, "refPinyin", p->ref_pinyin);
217 96    if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group);
218 96    rc |= set_num(out, "scale", p->scale);
219 96    rc |= set_num(out, "precision", p->precision);
220 96    rc |= set_num(out, "slack", p->slack);
221 96    rc |= set_flag(out, "paragraph_need_word_score",
222 96                   p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type));
223 96    rc |= set_flag(out, "phoneme_output", p->phoneme_output);
224 96    rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url);
225 96    if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]);
226 96    rc |= set_str(out, "dict_dialect", p->dict_dialect);
227 96    rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json);
228 96    rc |= set_str(out, "customized_pron", p->customized_pron_json);
229 96    if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis);
230 96    rc |= set_flag(out, "output_rawtext", p->output_rawtext);
231 96    rc |= set_flag(out, "punctuate", p->punctuate);
232 96    rc |= set_num(out, "itn", p->itn);
233 96    rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable);
234     /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */
235 96    rc |= set_flag(out, "vad_detction", p->vad_detection);
236 96    rc |= set_str(out, "keywords", p->keywords);
237 96    rc |= set_str(out, "keypoints", p->keypoints_json);
238 96    rc |= set_num(out, "keypoints_weight", p->keypoints_weight);
239 96    rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json);
240 96    rc |= set_str(out, "negativeReftext", p->negative_reftext);
241 96    if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home");
242 96    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 96    if (p->custom_params) {
246         size_t i;
247 67        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 96    rc |= set_str(out, "request", p->request_json);
254     /* customParams cannot switch the paragraph word details off */
255 96    rc |= ygst_fields_apply_coretype_rules(out, p->core_type);
256 96    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  * 2.1.1: the same round streams every recording by default; the parameter frame asks for reading
10  * progress only with realtime_feedback.
11  */
12 #include "ygst_core.h"
13 
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <string.h>
17 
18 /* ------------------------------------------------------------------ URL */
19 
20 258int ygst_url_encode(ygst_buf *out, const char *s) {
21     static const char H[] = "0123456789ABCDEF";
22 258    const unsigned char *p = (const unsigned char *)(s ? s : "");
23 4251    for (; *p; p++) {
24 3993        unsigned char c = *p;
25 3993        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ||
26 84            c == '_' || c == '~') {
27 3910            ygst_buf_append_char(out, (char)c);
28         } else {
29             char e[3];
30 83            e[0] = '%';
31 83            e[1] = H[c >> 4];
32 83            e[2] = H[c & 15];
33 83            ygst_buf_append(out, e, 3);
34         }
35     }
36 258    ygst_buf_cstr(out);
37 258    return out->oom ? -1 : 0;
38 }
39 
40 69static int starts_ci(const char *s, const char *prefix) {
41 394    while (*prefix) {
42 362        char a = *s;
43 362        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
44 362        if (a != *prefix) return 0;
45 325        s++;
46 325        prefix++;
47     }
48 32    return 1;
49 }
50 
51 36int ygst_ws_base_url(const char *rest_base, char *out, size_t cap) {
52     const char *scheme;
53     const char *rest;
54     size_t n;
55     int w;
56 36    if (!out || cap == 0) return -1;
57 35    out[0] = 0;
58 35    if (!rest_base) return -1;
59 34    if (starts_ci(rest_base, "https://")) {
60 6        scheme = "wss://";
61 6        rest = rest_base + 8;
62 28    } else if (starts_ci(rest_base, "http://")) {
63 24        scheme = "ws://";
64 24        rest = rest_base + 7;
65 4    } else if (starts_ci(rest_base, "wss://")) {
66 1        scheme = "wss://";
67 1        rest = rest_base + 6;
68 3    } else if (starts_ci(rest_base, "ws://")) {
69 1        scheme = "ws://";
70 1        rest = rest_base + 5;
71     } else {
72 2        return -1;
73     }
74 32    n = strlen(rest);
75 36    while (n > 0 && rest[n - 1] == '/') n--;
76 32    if (n == 0 || rest[0] == '/') return -1;
77 30    w = snprintf(out, cap, "%s%.*s", scheme, (int)n, rest);
78 30    if (w < 0 || (size_t)w >= cap) {
79 1        out[0] = 0;
80 1        return -1;
81     }
82 29    return 0;
83 }
84 
85 /* Query parameters the handshake sets itself. */
86 9static int own_query_name(const char *n) {
87 8    return strcmp(n, "appKey") == 0 || strcmp(n, "timestamp") == 0 || strcmp(n, "nonce") == 0 ||
88 17           strcmp(n, "idempotencyKey") == 0 || strcmp(n, "signature") == 0;
89 }
90 
91 31int ygst_ws_handshake_url(const char *rest_base, const char *core_type, const char *app_key, const char *secret,
92                           int64_t timestamp_sec, const char *nonce, const char *idempotency_key,
93                           const ygst_fields *extra, ygst_buf *out) {
94     char base[1024];
95     char ts[32];
96     char sig[45];
97     ygst_fields q;
98     size_t i;
99 31    int rc = -1;
100 31    if (!out || !core_type || !core_type[0] || ygst_is_blank(app_key) || !secret || !secret[0] || !nonce ||
101 27        !nonce[0] || !ygst_idempotency_key_ok(idempotency_key)) {
102 6        return -1;
103     }
104 25    if (ygst_ws_base_url(rest_base, base, sizeof base) != 0) return -1;
105 24    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
106 24    ygst_fields_init(&q);
107 24    ygst_fields_set(&q, "appKey", app_key);
108 24    ygst_fields_set(&q, "timestamp", ts);
109 24    ygst_fields_set(&q, "nonce", nonce);
110 24    ygst_fields_set(&q, "idempotencyKey", idempotency_key);
111 24    if (extra) {
112 33        for (i = 0; i < extra->count; i++) {
113 10            const ygst_field *f = &extra->items[i];
114 10            if (!f->name || !f->name[0] || !f->value || !f->value[0] || own_query_name(f->name)) continue;
115 7            ygst_fields_set(&q, f->name, f->value);
116         }
117     }
118 24    if (q.oom || ygst_sign(&q, secret, sig) != 0) goto done;
119 24    ygst_buf_append_str(out, base);
120 24    ygst_buf_append_char(out, '/');
121 24    ygst_url_encode(out, core_type);
122 127    for (i = 0; i < q.count; i++) {
123 103        ygst_buf_append_char(out, i == 0 ? '?' : '&');
124 103        ygst_url_encode(out, q.items[i].name);
125 103        ygst_buf_append_char(out, '=');
126 103        ygst_url_encode(out, q.items[i].value);
127     }
128 24    ygst_buf_append_str(out, "&signature=");
129 24    ygst_url_encode(out, sig);
130 24    ygst_buf_cstr(out);
131 24    rc = out->oom ? -1 : 0;
132 24done:
133 24    ygst_fields_free(&q);
134 24    return rc;
135 }
136 
137 /* ------------------------------------------------------------------ parameter frame */
138 
139 /* The platform reads these names as commands (cmd, end, data) or the frame sets them itself. */
140 69static int reserved_frame_name(const char *n, int flags) {
141 68    return strcmp(n, "cmd") == 0 || strcmp(n, "end") == 0 || strcmp(n, "data") == 0 ||
142 200           strcmp(n, "realtime_feedback") == 0 || strcmp(n, "idempotencyKey") == 0 ||
143 63           ((flags & YGST_WSP_TIMING) && strcmp(n, "timing") == 0);
144 }
145 
146 32int ygst_ws_param_frame(const ygst_fields *fields, const char *idempotency_key, int flags, ygst_buf *out) {
147     size_t i;
148 32    int skipped = 0;
149 32    if (!fields || !out || fields->oom || !ygst_idempotency_key_ok(idempotency_key)) return -1;
150 30    ygst_buf_append_char(out, '{');
151 99    for (i = 0; i < fields->count; i++) {
152 69        const ygst_field *f = &fields->items[i];
153 69        if (!f->name || reserved_frame_name(f->name, flags)) {
154 7            skipped++;
155 7            continue;
156         }
157 62        ygst_json_escape(out, f->name);
158 62        ygst_buf_append_char(out, ':');
159 62        ygst_json_escape(out, f->value ? f->value : "");
160 62        ygst_buf_append_char(out, ',');
161     }
162     /* 2.1.1: progress frames only when the app asked for them, the timing object only for the log */
163 30    if (flags & YGST_WSP_REALTIME_FEEDBACK) ygst_buf_append_str(out, "\"realtime_feedback\":true,");
164 30    if (flags & YGST_WSP_TIMING) ygst_buf_append_str(out, "\"timing\":1,");
165 30    ygst_buf_append_str(out, "\"idempotencyKey\":");
166 30    ygst_json_escape(out, idempotency_key);
167 30    ygst_buf_append_char(out, '}');
168 30    ygst_buf_cstr(out);
169 30    return out->oom ? -1 : skipped;
170 }
171 
172 /* ------------------------------------------------------------------ frames */
173 
174 /* An integer member, also when a gateway quoted it. 1 found, 0 absent or not an integer. */
175 1100static int int_member(ygst_span obj, const char *key, long long *out) {
176     ygst_span v;
177 1100    if (ygst_json_member(obj, key, &v) != 1) return 0;
178 1055    if (ygst_json_type(v) == 's') {
179         ygst_buf b;
180 6        int ok = 0;
181 6        ygst_buf_init(&b);
182 6        if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 20) {
183 6            char *end = NULL;
184 6            long long x = strtoll(ygst_buf_cstr(&b), &end, 10);
185 6            if (end && *end == 0) {
186 6                *out = x;
187 6                ok = 1;
188             }
189         }
190 6        ygst_buf_free(&b);
191 6        return ok;
192     }
193 1049    return ygst_json_int(v, out) == 0;
194 }
195 
196 8591static int bool_member(ygst_span obj, const char *key) {
197     ygst_span v;
198 8591    int b = 0;
199 8591    if (ygst_json_member(obj, key, &v) != 1 || ygst_json_bool(v, &b) != 0) return 0;
200 8565    return b;
201 }
202 
203 614static int count_value(long long v) {
204 614    if (v < 0) return -1;
205 614    return v > 2147483647LL ? 2147483647 : (int)v;
206 }
207 
208 /* The event name when it is a short JSON string, else "". */
209 140static void event_name(ygst_span ev, char *out, size_t cap) {
210     ygst_buf b;
211 140    out[0] = 0;
212 140    if (ygst_json_type(ev) != 's') return;
213 139    ygst_buf_init(&b);
214 139    if (ygst_json_string(ev, &b) == 0 && b.len < cap) memcpy(out, ygst_buf_cstr(&b), b.len + 1);
215 139    ygst_buf_free(&b);
216 }
217 
218 209static void parse_progress(ygst_ws_frame *f) {
219     ygst_span r, list, item;
220     long long v;
221 209    size_t pos = 0;
222 209    int read = 0, matched = 0, total = 0;
223 209    if (ygst_json_member(f->root, "result", &r) != 1 || ygst_json_type(r) != 'o') return;
224 208    f->result = r;
225 208    if (int_member(r, "bytes", &v) && v >= 0) f->bytes = v;
226 208    f->read_units = int_member(r, "readUnits", &v) ? count_value(v) : -1;
227 208    f->matched_units = int_member(r, "matchedUnits", &v) ? count_value(v) : -1;
228 208    f->total_units = int_member(r, "totalUnits", &v) ? count_value(v) : -1;
229     /* every unit with its state */
230 208    if (ygst_json_member(r, "progress", &list) == 1 && ygst_json_type(list) == 'a') {
231 197        f->has_progress = 1;
232 4479        while (ygst_json_array_next(list, &pos, &item) == 1) {
233 4282            total++;
234 4282            if (ygst_json_type(item) != 'o') continue;
235 4281            if (bool_member(item, "read")) read++;
236 4281            if (bool_member(item, "matched")) matched++;
237         }
238 197        if (f->read_units < 0) f->read_units = read;
239 197        if (f->matched_units < 0) f->matched_units = matched;
240 197        if (f->total_units < 0) f->total_units = total;
241     }
242     /* Shengtong items of the units read since the previous frame */
243 208    if (ygst_json_member(r, "words", &list) == 1 && ygst_json_type(list) == 'a') {
244 175        f->has_words = 1;
245 175        f->words = list;
246 175        pos = 0;
247 506        while (ygst_json_array_next(list, &pos, &item) == 1) f->word_count++;
248     }
249 }
250 
251 29static void parse_final(ygst_ws_frame *f) {
252     ygst_span r;
253 29    if (ygst_json_member(f->root, "result", &r) == 1) f->result = r;
254 29    f->replayed = bool_member(f->root, "replayed");
255 29}
256 
257 382int ygst_ws_frame_parse(const char *text, size_t n, ygst_ws_frame *f) {
258     ygst_span root, v;
259 382    if (!f) return -1;
260 381    memset(f, 0, sizeof *f);
261 381    f->kind = YGST_WSF_INVALID;
262 381    f->bytes = -1;
263 381    f->read_units = -1;
264 381    f->matched_units = -1;
265 381    f->total_units = -1;
266 381    if (!text || ygst_json_parse(text, n, &root) != 0 || ygst_json_type(root) != 'o') return -1;
267 377    f->root = root;
268 377    if (ygst_json_member(root, "event", &v) == 1) {
269         char ev[16];
270 140        event_name(v, ev, sizeof ev);
271 140        if (strcmp(ev, "connected") == 0) {
272 58            f->kind = YGST_WSF_CONNECTED;
273 82        } else if (strcmp(ev, "started") == 0) {
274 58            f->kind = YGST_WSF_STARTED;
275 24        } else if (strcmp(ev, "pong") == 0) {
276 2            f->kind = YGST_WSF_PONG;
277 22        } else if (strcmp(ev, "error") == 0) {
278 18            long long code = 0;
279 18            f->kind = YGST_WSF_ERROR;
280 18            if (int_member(root, "code", &code) && code > 0) f->code = count_value(code);
281 4        } else if (strcmp(ev, "result") == 0) {
282 2            f->kind = YGST_WSF_FINAL;
283 2            parse_final(f);
284         } else {
285 2            f->kind = YGST_WSF_OTHER;
286         }
287 140        return 0;
288     }
289 237    if (ygst_json_member(root, "eof", &v) == 1) {
290 234        long long eof = -1;
291 234        if (int_member(root, "eof", &eof) && eof == 0) {
292 209            f->kind = YGST_WSF_PROGRESS;
293 209            parse_progress(f);
294         } else {
295 25            f->kind = YGST_WSF_FINAL;
296 25            parse_final(f);
297         }
298 234        return 0;
299     }
300 3    if (ygst_json_member(root, "result", NULL) == 1 || ygst_json_member(root, "recordId", NULL) == 1) {
301 2        f->kind = YGST_WSF_FINAL;
302 2        parse_final(f);
303 2        return 0;
304     }
305 1    f->kind = YGST_WSF_OTHER;
306 1    return 0;
307 }
308 
309 16int ygst_ws_classify_error(ygst_span frame, ygst_classification *c) {
310     static const char *const KEYS[] = {"message", "msg", "error", NULL};
311     const char *const *k;
312 16    long long code = 0;
313 16    c->success = 0;
314 16    c->http_status = 0;
315 16    c->local_code = 0;
316 16    c->biz_code = 0;
317 16    c->has_audio_url = 0;
318 16    c->has_record_id = 0;
319 16    c->record_id_raw.p = NULL;
320 16    c->record_id_raw.n = 0;
321 16    c->root.p = NULL;
322 16    c->root.n = 0;
323 16    c->result.p = NULL;
324 16    c->result.n = 0;
325 16    ygst_buf_reset(&c->record_id);
326 16    ygst_buf_reset(&c->audio_url);
327 16    ygst_buf_reset(&c->message);
328 16    if (ygst_json_type(frame) == 'o') {
329 16        if (int_member(frame, "code", &code) && code > 0) c->biz_code = count_value(code);
330 23        for (k = KEYS; *k; k++) {
331             ygst_span v;
332 21            if (ygst_json_member(frame, *k, &v) == 1 && ygst_json_type(v) == 's') {
333 14                ygst_json_string(v, &c->message);
334 14                break;
335             }
336         }
337     }
338 16    if (!c->message.len) ygst_buf_append_str(&c->message, "error frame without a message");
339 16    ygst_buf_cstr(&c->message);
340 16    ygst_buf_cstr(&c->record_id);
341 16    ygst_buf_cstr(&c->audio_url);
342     /* without a business code the platform failed while evaluating: replay like a 5xx */
343 16    c->retryable = c->biz_code ? ygst_is_retryable(0, c->biz_code, 0) : 1;
344 16    c->err_id = ygst_errid_for(0, c->biz_code, 0, c->retryable);
345 16    return c->message.oom ? -1 : 0;
346 }
347 
348 /* ------------------------------------------------------------------ words dedupe */
349 
350 47void ygst_words_dedupe_init(ygst_words_dedupe *d) {
351 47    memset(d, 0, sizeof *d);
352 47}
353 
354 103int ygst_words_dedupe_skip(ygst_words_dedupe *d, int read_units, int word_count) {
355     long start;
356     long skip;
357 103    if (word_count <= 0) return 0;
358 102    if (read_units < 0) read_units = d->delivered + word_count; /* position unknown: the next units */
359 102    start = (long)read_units - word_count;
360 102    skip = d->delivered > start ? (long)d->delivered - start : 0;
361 102    if (skip >= word_count) return word_count; /* every item was delivered already */
362 78    if (read_units > d->delivered) d->delivered = read_units;
363 78    return (int)skip;
364 }
365 
366 /* ------------------------------------------------------------------ round controller */
367 
368 45void ygst_rt_init(ygst_rt *rt) {
369 45    memset(rt, 0, sizeof *rt);
370 45    ygst_retry_policy_default(&rt->policy);
371 45    rt->connect_timeout_ms = 20000;
372 45    rt->attempt_timeout_ms = 60000;
373 45    rt->ping_interval_ms = 15000;
374 45    rt->pong_timeout_ms = 30000;
375 45    rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
376 45    rt->state = YGST_RT_IDLE;
377 45    rt->frame.kind = YGST_WSF_INVALID;
378 45    ygst_words_dedupe_init(&rt->dedupe);
379 45    ygst_classification_init(&rt->cls);
380 45    ygst_buf_init(&rt->failure);
381 45}
382 
383 45void ygst_rt_free(ygst_rt *rt) {
384 45    ygst_classification_free(&rt->cls);
385 45    ygst_buf_free(&rt->failure);
386 45}
387 
388 1398static ygst_rt_actions no_actions(void) {
389     ygst_rt_actions a;
390 1398    memset(&a, 0, sizeof a);
391 1398    return a;
392 }
393 
394 910static int on_connection(ygst_rt_state s) {
395 910    return s == YGST_RT_CONNECTING || s == YGST_RT_STARTING || s == YGST_RT_STREAMING || s == YGST_RT_ENDING;
396 }
397 
398 186static void clear_timers(ygst_rt *rt) {
399 186    rt->deadline_ms = 0;
400 186    rt->next_ping_ms = 0;
401 186    rt->pong_deadline_ms = 0;
402 186}
403 
404 36static void note_failure(ygst_rt *rt, const char *why) {
405 36    ygst_buf_reset(&rt->failure);
406 36    ygst_buf_append_str(&rt->failure, why ? why : "");
407 36    ygst_buf_cstr(&rt->failure);
408 36}
409 
410 59static void open_connection(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
411 59    a->connect = 1;
412 59    rt->state = YGST_RT_CONNECTING;
413 59    rt->connections++;
414 59    rt->sent = 0;
415 59    clear_timers(rt);
416 59    rt->deadline_ms = now + rt->connect_timeout_ms;
417 59}
418 
419 250static void send_pending_audio(ygst_rt *rt, ygst_rt_actions *a) {
420 250    if (rt->total > rt->sent) {
421 224        a->audio_from = rt->sent;
422 224        a->audio_to = rt->total;
423 224        rt->sent = rt->total;
424     }
425 250}
426 
427 31static void start_streaming(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
428 31    rt->state = YGST_RT_STREAMING;
429 31    clear_timers(rt);
430 31    if (rt->ping_interval_ms > 0) rt->next_ping_ms = now + rt->ping_interval_ms;
431 31    send_pending_audio(rt, a);
432 31}
433 
434 /* All audio from the current offset, then end. The first end starts the attempts of the REST
435  * call controller; a reconnect only sends it again. */
436 42static void send_end(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
437 42    send_pending_audio(rt, a);
438 42    a->send_end = 1;
439 42    if (!rt->end_sent) {
440 30        rt->end_sent = 1;
441 30        ygst_call_init(&rt->call, &rt->policy, rt->seed, now);
442     }
443 42    rt->state = YGST_RT_ENDING;
444     /* no heartbeat while the platform evaluates: the result timeout bounds the wait, as a REST
445      * attempt's read timeout does */
446 42    clear_timers(rt);
447 42    rt->deadline_ms = now + rt->attempt_timeout_ms;
448 42}
449 
450 /* Before end: the platform never evaluated this round, REST evaluates it with the same tokenId. */
451 10static void fall_back(ygst_rt *rt, ygst_rt_actions *a, const char *why) {
452 10    a->close = 1;
453 10    rt->state = YGST_RT_FALLBACK;
454 10    clear_timers(rt);
455 10    note_failure(rt, why);
456 10    if (rt->end_requested) a->fallback = 1;
457 10}
458 
459 /* After end: rt->cls holds the outcome of this attempt; the REST call controller decides. */
460 26static void attempt_failed(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
461 26    ygst_action next = ygst_call_next(&rt->call, &rt->cls, -1, now);
462 26    a->close = 1;
463 26    clear_timers(rt);
464 26    note_failure(rt, ygst_buf_cstr(&rt->cls.message));
465 26    if (next.kind == YGST_ACT_RETRY) {
466 16        rt->state = YGST_RT_BACKOFF;
467 16        rt->reconnect_at_ms = now + next.delay_ms;
468 16        a->retry = 1;
469 16        a->delay_ms = next.delay_ms;
470 16        return;
471     }
472 10    rt->state = YGST_RT_DONE;
473 10    a->fail = 1;
474 10    a->err_id = next.kind == YGST_ACT_FAIL ? next.err_id : YGST_LOCAL_PROTOCOL;
475 }
476 
477 24static void connection_failed(ygst_rt *rt, ygst_rt_actions *a, int local_code, int http_status, const char *message,
478                               int64_t now) {
479     ygst_outcome o;
480 24    if (!rt->end_sent) {
481 8        fall_back(rt, a, message);
482 8        return;
483     }
484 16    memset(&o, 0, sizeof o);
485 16    o.local_code = local_code;
486 16    o.http_status = local_code ? 0 : http_status;
487 16    o.retry_after_ms = -1;
488 16    o.local_message = message;
489 16    ygst_classify(&o, &rt->cls);
490 16    if (!local_code && http_status > 0 && message && message[0]) {
491         /* a rejected handshake has no body: say so instead of repeating the status */
492 2        ygst_buf_reset(&rt->cls.message);
493 2        ygst_buf_append_str(&rt->cls.message, message);
494 2        ygst_buf_cstr(&rt->cls.message);
495     }
496 16    attempt_failed(rt, a, now);
497 }
498 
499 45ygst_rt_actions ygst_rt_start(ygst_rt *rt, int64_t now_ms) {
500 45    ygst_rt_actions a = no_actions();
501 45    if (rt->state != YGST_RT_IDLE) return a;
502 44    if (rt->max_frame_bytes == 0) rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
503 44    open_connection(rt, &a, now_ms);
504 44    return a;
505 }
506 
507 333ygst_rt_actions ygst_rt_on_audio(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
508 333    ygst_rt_actions a = no_actions();
509     (void)now_ms;
510 333    if (rt->end_requested) return a; /* the round audio is complete */
511 332    if (total_bytes > rt->total) rt->total = total_bytes;
512 332    if (rt->state == YGST_RT_STREAMING) send_pending_audio(rt, &a);
513 332    return a;
514 }
515 
516 39ygst_rt_actions ygst_rt_on_stop(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
517 39    ygst_rt_actions a = no_actions();
518 39    if (rt->end_requested || rt->state == YGST_RT_DONE) return a;
519 37    if (total_bytes > rt->total) rt->total = total_bytes;
520 37    rt->end_requested = 1;
521 37    switch (rt->state) {
522 6    case YGST_RT_IDLE:
523     case YGST_RT_FALLBACK:
524 6        rt->state = YGST_RT_FALLBACK;
525 6        a.fallback = 1;
526 6        break;
527 23    case YGST_RT_STREAMING:
528 23        send_end(rt, &a, now_ms);
529 23        break;
530 8    default:
531 8        break; /* CONNECTING, STARTING: end goes out right after "started" */
532     }
533 37    return a;
534 }
535 
536 253ygst_rt_actions ygst_rt_on_text(ygst_rt *rt, const char *text, size_t n, int64_t now_ms) {
537 253    ygst_rt_actions a = no_actions();
538 253    if (!on_connection(rt->state)) return a;
539 249    if (ygst_ws_frame_parse(text, n, &rt->frame) != 0) return a;
540 248    switch (rt->frame.kind) {
541 52    case YGST_WSF_CONNECTED:
542 52        if (rt->state == YGST_RT_CONNECTING) {
543 51            a.send_params = 1;
544 51            rt->state = YGST_RT_STARTING;
545         }
546 52        break;
547 51    case YGST_WSF_STARTED:
548 51        if (rt->state == YGST_RT_STARTING) {
549 50            if (rt->end_sent || rt->end_requested) {
550 19                send_end(rt, &a, now_ms);
551             } else {
552 31                start_streaming(rt, &a, now_ms);
553             }
554         }
555 51        break;
556 116    case YGST_WSF_PROGRESS:
557         /* delivered only with new words; empty words and bytes-only frames are not */
558 116        if (rt->frame.word_count > 0) {
559 93            int skip = ygst_words_dedupe_skip(&rt->dedupe, rt->frame.read_units, rt->frame.word_count);
560 93            if (skip < rt->frame.word_count) {
561 71                a.progress = 1;
562 71                a.words_skip = skip;
563             } else {
564 22                a.progress_held = 1;
565             }
566         }
567 116        break;
568 18    case YGST_WSF_FINAL:
569 18        if (rt->state != YGST_RT_ENDING) {
570 1            if (!rt->end_sent) fall_back(rt, &a, "result frame before end");
571 1            break;
572         }
573         {
574             ygst_outcome o;
575 17            memset(&o, 0, sizeof o);
576 17            o.http_status = 200;
577 17            o.body = text;
578 17            o.body_len = n;
579 17            o.retry_after_ms = -1;
580 17            o.replayed = rt->frame.replayed;
581 17            ygst_classify(&o, &rt->cls);
582         }
583 17        if (rt->cls.success) {
584 16            (void)ygst_call_next(&rt->call, &rt->cls, -1, now_ms);
585 16            rt->state = YGST_RT_DONE;
586 16            clear_timers(rt);
587 16            a.close = 1;
588 16            a.final = 1;
589         } else {
590 1            attempt_failed(rt, &a, now_ms);
591         }
592 17        break;
593 10    case YGST_WSF_ERROR:
594 10        ygst_ws_classify_error(rt->frame.root, &rt->cls);
595 10        if (!rt->end_sent) {
596 1            fall_back(rt, &a, ygst_buf_cstr(&rt->cls.message));
597         } else {
598 9            attempt_failed(rt, &a, now_ms);
599         }
600 10        break;
601 1    default:
602 1        break; /* pong, other events */
603     }
604 248    return a;
605 }
606 
607 3ygst_rt_actions ygst_rt_on_pong(ygst_rt *rt, int64_t now_ms) {
608 3    ygst_rt_actions a = no_actions();
609 3    if (rt->state == YGST_RT_STREAMING && rt->pong_deadline_ms) {
610 1        rt->pong_deadline_ms = 0;
611 1        if (rt->ping_interval_ms > 0) rt->next_ping_ms = now_ms + rt->ping_interval_ms;
612     }
613 3    return a;
614 }
615 
616 20ygst_rt_actions ygst_rt_on_failure(ygst_rt *rt, int local_code, int http_status, const char *message,
617                                    int64_t now_ms) {
618 20    ygst_rt_actions a = no_actions();
619 20    if (!on_connection(rt->state)) return a;
620 18    connection_failed(rt, &a, local_code, http_status, message, now_ms);
621 18    return a;
622 }
623 
624 705ygst_rt_actions ygst_rt_on_tick(ygst_rt *rt, int64_t now_ms) {
625 705    ygst_rt_actions a = no_actions();
626     char msg[96];
627 705    if (rt->state == YGST_RT_BACKOFF) {
628 80        if (now_ms >= rt->reconnect_at_ms) open_connection(rt, &a, now_ms);
629 80        return a;
630     }
631 625    if (!on_connection(rt->state)) return a;
632 460    if (rt->deadline_ms && now_ms >= rt->deadline_ms) {
633 4        int ending = rt->state == YGST_RT_ENDING;
634 4        if (ending) {
635 2            snprintf(msg, sizeof msg, "timeout: no result within %d ms", rt->attempt_timeout_ms);
636         } else {
637 2            snprintf(msg, sizeof msg, "timeout: not started within %d ms", rt->connect_timeout_ms);
638         }
639 4        connection_failed(rt, &a, ending ? YGST_LOCAL_RESULT_TIMEOUT : YGST_LOCAL_TIMEOUT, 0, msg, now_ms);
640 4        return a;
641     }
642 456    if (rt->pong_deadline_ms && now_ms >= rt->pong_deadline_ms) {
643 2        snprintf(msg, sizeof msg, "timeout: no pong within %d ms", rt->pong_timeout_ms);
644 2        connection_failed(rt, &a, YGST_LOCAL_TIMEOUT, 0, msg, now_ms);
645 2        return a;
646     }
647 454    if (rt->state == YGST_RT_STREAMING && rt->next_ping_ms && !rt->pong_deadline_ms && now_ms >= rt->next_ping_ms) {
648 4        a.send_ping = 1;
649 4        rt->next_ping_ms = 0;
650 4        rt->pong_deadline_ms = now_ms + rt->pong_timeout_ms;
651     }
652 454    return a;
653 }
654 
655 2void ygst_rt_cancel(ygst_rt *rt) {
656 2    rt->state = YGST_RT_DONE;
657 2    clear_timers(rt);
658 2}
659 
660 12int ygst_rt_active(const ygst_rt *rt) {
661 12    return on_connection(rt->state) || rt->state == YGST_RT_BACKOFF;
662 }
663 
664 5int ygst_rt_error_message(const ygst_rt *rt, int err_id, ygst_buf *out) {
665 5    return ygst_error_message(&rt->cls, err_id, rt->call.attempt, out);
666 }

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 273static const ygst_error_entry *find_in(const ygst_error_entry *t, size_t n, int code) {
19     size_t i;
20 3789    for (i = 0; i < n; i++) {
21 3775        if (t[i].code == code) return &t[i];
22     }
23 14    return NULL;
24 }
25 
26 196const ygst_error_entry *ygst_error_find(int code) {
27 196    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 87void ygst_rng_seed(ygst_rng *r, uint64_t seed) {
57 87    r->s = seed;
58 87}
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 113void ygst_retry_policy_default(ygst_retry_policy *p) {
74 113    memset(p, 0, sizeof *p);
75 113    p->max_retries = 2;
76 113    p->initial_delay_ms = 200;
77 113    p->multiplier = 2.0;
78 113    p->max_delay_ms = 4000;
79 113    p->jitter = 0.3;
80 113    p->respect_retry_after = 1;
81 113    p->max_retry_after_ms = 30000;
82 113    p->total_timeout_ms = 300000;
83 113    p->auto_retry = 0;
84 113    p->max_auto_retries = 2;
85 113    p->err_ids[0] = YGST_ERRID_RETRYABLE;
86 113    p->n_err_ids = 1;
87 113}
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 236int ygst_is_retryable(int local_code, int biz_code, int http_status) {
177     const ygst_error_entry *e;
178 236    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 186    if (biz_code) {
183 152        e = ygst_error_find(biz_code);
184 152        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 206int ygst_errid_for(int local_code, int biz_code, int http_status, int retryable) {
204 206    if (local_code >= 60001 && local_code <= 60009) return local_code;
205 195    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 211void ygst_classification_init(ygst_classification *c) {
214 211    memset(c, 0, sizeof *c);
215 211    ygst_buf_init(&c->record_id);
216 211    ygst_buf_init(&c->audio_url);
217 211    ygst_buf_init(&c->message);
218 211}
219 
220 211void ygst_classification_free(ygst_classification *c) {
221 211    ygst_buf_free(&c->record_id);
222 211    ygst_buf_free(&c->audio_url);
223 211    ygst_buf_free(&c->message);
224 211}
225 
226 121static int member_int(ygst_span obj, const char *key, long long *out) {
227     ygst_span v;
228 121    if (ygst_json_member(obj, key, &v) != 1) return 0;
229 98    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 97    return ygst_json_int(v, out) == 0;
246 }
247 
248 /* First of the keys whose value is a JSON string. */
249 109static int first_string(ygst_span obj, const char *const *keys, ygst_buf *out) {
250 141    for (; *keys; keys++) {
251         ygst_span v;
252 132        if (ygst_json_member(obj, *keys, &v) == 1 && ygst_json_type(v) == 's') {
253 100            ygst_buf_reset(out);
254 100            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 231int 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 231    int parsed = 0;
287 231    long long code = 0;
288 231    c->success = 0;
289 231    c->http_status = o->http_status;
290 231    c->local_code = o->local_code;
291 231    c->biz_code = 0;
292 231    c->has_audio_url = 0;
293 231    c->has_record_id = 0;
294 231    c->record_id_raw.p = NULL;
295 231    c->record_id_raw.n = 0;
296 231    c->root.p = NULL;
297 231    c->root.n = 0;
298 231    c->result.p = NULL;
299 231    c->result.n = 0;
300 231    ygst_buf_reset(&c->record_id);
301 231    ygst_buf_reset(&c->audio_url);
302 231    ygst_buf_reset(&c->message);
303 
304 231    if (o->local_code || o->http_status <= 0) {
305         const ygst_error_entry *e;
306 43        if (!c->local_code) c->local_code = YGST_LOCAL_NETWORK;
307 43        e = ygst_local_find(c->local_code);
308 65        set_message(&c->message, (o->local_message && o->local_message[0]) ? o->local_message
309 22                                                                            : (e ? e->message : "network error"));
310 43        c->retryable = ygst_is_retryable(c->local_code, 0, 0);
311 43        c->err_id = ygst_errid_for(c->local_code, 0, 0, c->retryable);
312 43        return 0;
313     }
314 
315 188    if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o';
316 
317 188    if (o->http_status >= 200 && o->http_status < 300) {
318         ygst_span result, v;
319 74        if (parsed && ygst_json_member(root, "result", &result) == 1) {
320             ygst_span inner;
321 71            c->success = 1;
322 71            c->root = root;
323 71            c->result = result;
324 71            if (ygst_json_member(root, "recordId", &v) == 1) {
325 68                c->has_record_id = 1;
326 68                c->record_id_raw = v;
327 68                if (ygst_json_type(v) == 's') {
328 67                    ygst_json_string(v, &c->record_id);
329                 } else {
330 1                    ygst_buf_append(&c->record_id, v.p, v.n);
331                 }
332             }
333 71            if (ygst_json_member(root, "audioUrl", &v) == 1) {
334 5                if (ygst_json_type(v) == 's') c->has_audio_url = ygst_json_string(v, &c->audio_url) == 0;
335 68            } else if (ygst_json_type(result) == 'o' && ygst_json_member(result, "audioUrl", &inner) == 1 &&
336 2                       ygst_json_type(inner) == 's') {
337 1                c->has_audio_url = ygst_json_string(inner, &c->audio_url) == 0;
338             }
339 71            if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0;
340 71            ygst_buf_cstr(&c->record_id);
341 71            ygst_buf_cstr(&c->audio_url);
342 71            c->retryable = 0;
343 71            c->err_id = 0;
344 71            return 0;
345         }
346 3        c->local_code = YGST_LOCAL_PROTOCOL;
347 3        set_message(&c->message, parsed ? "invalid platform response: platform response has no result"
348                                         : "invalid platform response: not a JSON object");
349 3        c->retryable = 0;
350 3        c->err_id = YGST_LOCAL_PROTOCOL;
351 3        return 0;
352     }
353 
354 114    if (parsed) {
355         ygst_span detail;
356 109        if (member_int(root, "code", &code) && code != 0) c->biz_code = (int)code;
357 109        if (!c->biz_code && member_int(root, "errId", &code) && code != 0) c->biz_code = (int)code;
358 109        first_string(root, MESSAGE_KEYS, &c->message);
359 109        if (ygst_json_member(root, "detail", &detail) == 1) {
360 9            if (ygst_json_type(detail) == 's') {
361                 ygst_buf d;
362 7                ygst_buf_init(&d);
363 7                ygst_json_string(detail, &d);
364 7                if (!c->biz_code) c->biz_code = bracket_code(ygst_buf_cstr(&d));
365 7                if (!c->message.len) set_message(&c->message, ygst_buf_cstr(&d));
366 7                ygst_buf_free(&d);
367 2            } else if (!c->message.len) {
368 1                ygst_buf_reset(&c->message);
369 1                ygst_buf_append(&c->message, detail.p, detail.n > 300 ? 300 : detail.n);
370             }
371         }
372     }
373 114    if (!c->message.len) {
374         char tmp[32];
375 6        snprintf(tmp, sizeof tmp, "HTTP %d", o->http_status);
376 6        set_message(&c->message, tmp);
377     }
378 114    ygst_buf_cstr(&c->message);
379 114    c->retryable = ygst_is_retryable(0, c->biz_code, o->http_status);
380 114    c->err_id = ygst_errid_for(0, c->biz_code, o->http_status, c->retryable);
381 114    return 0;
382 }
383 
384 25int ygst_error_message(const ygst_classification *c, int err_id, int attempts, ygst_buf *out) {
385 25    const char *m = c->message.data ? (const char *)c->message.data : "";
386 25    if (err_id == YGST_ERRID_RETRYABLE) {
387 10        const char *base = ygst_compat_message(YGST_ERRID_RETRYABLE);
388         size_t mark;
389 10        ygst_buf_append_str(out, base ? base : "");
390 10        ygst_buf_append_str(out, ": ");
391 10        mark = out->len;
392 10        if (c->http_status > 0) {
393 4            ygst_buf_append_str(out, "HTTP ");
394 4            ygst_buf_append_int(out, c->http_status);
395 4            if (c->biz_code) {
396 2                ygst_buf_append_str(out, " code=");
397 2                ygst_buf_append_int(out, c->biz_code);
398             }
399 6        } else if (c->local_code) {
400 3            const ygst_error_entry *e = ygst_local_find(c->local_code);
401 3            if (e) {
402 2                ygst_buf_append_str(out, e->name);
403             } else {
404 1                ygst_buf_append_str(out, "code=");
405 1                ygst_buf_append_int(out, c->local_code);
406             }
407 3        } else if (c->biz_code) {
408             /* a WebSocket error frame: a business code without an HTTP status */
409 2            ygst_buf_append_str(out, "code=");
410 2            ygst_buf_append_int(out, c->biz_code);
411         }
412 10        if (m[0]) {
413 10            if (out->len > mark) ygst_buf_append_char(out, ' ');
414 10            ygst_buf_append_str(out, m);
415         }
416 10        if (attempts > 0) {
417 9            ygst_buf_append_str(out, " (attempts ");
418 9            ygst_buf_append_int(out, attempts);
419 9            ygst_buf_append_char(out, ')');
420         }
421 15    } else if (m[0]) {
422 11        ygst_buf_append_str(out, m);
423     } else {
424 4        const char *cm = ygst_compat_message(err_id);
425 4        const ygst_error_entry *e = cm ? NULL : ygst_error_find(err_id);
426 4        if (!cm && !e) e = ygst_local_find(err_id);
427 4        ygst_buf_append_str(out, cm ? cm : (e ? e->message : ""));
428     }
429 25    ygst_buf_cstr(out);
430 25    return out->oom ? -1 : 0;
431 }
432 
433 /* ------------------------------------------------------------------ controller */
434 
435 83void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) {
436 83    memset(c, 0, sizeof *c);
437 83    if (p) {
438 82        c->policy = *p;
439     } else {
440 1        ygst_retry_policy_default(&c->policy);
441     }
442 83    ygst_rng_seed(&c->rng, seed);
443 83    c->start_ms = now_ms;
444 83}
445 
446 60static long apply_retry_after(const ygst_retry_policy *p, long delay, long retry_after_ms) {
447 60    if (p->respect_retry_after && retry_after_ms >= 0) {
448 4        long ra = retry_after_ms > p->max_retry_after_ms ? p->max_retry_after_ms : retry_after_ms;
449 4        if (ra > delay) delay = ra;
450     }
451 60    return delay;
452 }
453 
454 134ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) {
455     ygst_action a;
456 134    int64_t deadline = c->start_ms + c->policy.total_timeout_ms;
457 134    a.kind = YGST_ACT_FAIL;
458 134    a.delay_ms = 0;
459 134    a.err_id = cls->err_id;
460 134    a.auto_retry = 0;
461 134    c->attempt++;
462 134    c->total_attempts++;
463 134    if (cls->success) {
464 52        a.kind = YGST_ACT_SUCCESS;
465 52        a.err_id = 0;
466 52        return a;
467     }
468 82    if (cls->retryable && c->attempt <= c->policy.max_retries) {
469 49        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->attempt, &c->rng), retry_after_ms);
470 49        if (now_ms + d <= deadline) {
471 47            a.kind = YGST_ACT_RETRY;
472 47            a.delay_ms = d;
473 47            return a;
474         }
475 2        return a;
476     }
477 46    if (c->policy.auto_retry && c->submission < c->policy.max_auto_retries &&
478 13        ygst_retry_policy_has_err_id(&c->policy, cls->err_id)) {
479 11        long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->submission + 1, &c->rng),
480                                    retry_after_ms);
481 11        if (now_ms + d <= deadline) {
482 10            c->submission++;
483 10            c->attempt = 0;
484 10            a.kind = YGST_ACT_RETRY;
485 10            a.delay_ms = d;
486 10            a.auto_retry = 1;
487 10            return a;
488         }
489     }
490 23    return a;
491 }

Sources/STKouyuEngine/core/ygst_timing.c 100.00%

1 /*
2  * Copyright 2026 优谷雅言 open.shengzhiai.com
3  * SPDX-License-Identifier: Apache-2.0
4  *
5  * Per-phase timing of one evaluation (2.1.1, BRIEF C and D): the Server-Timing header of the compat
6  * REST responses, the "timing" object the platform adds to a final JSON on request, the INFO
7  * summary line and the DEBUG detail line. The Objective-C layer and the integration driver record
8  * the phases; everything that turns them into text is here, so that the Linux tests cover it.
9  */
10 #include "ygst_core.h"
11 
12 #include <stdio.h>
13 #include <string.h>
14 
15 75void ygst_timing_init(ygst_timing *t) {
16 75    if (!t) return;
17 74    memset(t, 0, sizeof *t);
18 74    t->mode = YGST_TM_NONE;
19 74    t->upload_ms = -1;
20 74    t->wait_ms = -1;
21 74    t->download_ms = -1;
22 74    t->audio_bytes = -1;
23 74    t->upload_bytes = -1;
24 74    t->queued_at_stop = -1;
25 74    t->wire_bytes = -1;
26 74    t->body_bytes = -1;
27 74    t->server_total_ms = -1;
28 74    t->server_engine_ms = -1;
29 74    t->server_platform_ms = -1;
30 }
31 
32 5int ygst_timing_wanted(int log_level) {
33 5    return log_level >= 2;
34 }
35 
36 /* ------------------------------------------------------------------ Server-Timing */
37 
38 536static int is_ows(char c) {
39 536    return c == ' ' || c == '\t';
40 }
41 
42 /* The n bytes at s equal word (lower case) ignoring ASCII case. */
43 67static int word_eq(const char *s, size_t n, const char *word) {
44     size_t i;
45 67    if (strlen(word) != n) return 0;
46 223    for (i = 0; i < n; i++) {
47 180        char c = s[i];
48 180        if (c >= 'A' && c <= 'Z') c = (char)(c + 32);
49 180        if (c != word[i]) return 0;
50     }
51 43    return 1;
52 }
53 
54 /* A plain non-negative decimal (5321, 12.75) in the n bytes at s, rounded half up to whole
55  * milliseconds without the C locale's strtod. -1 when it is not one. */
56 26static long long decimal_ms(const char *s, size_t n) {
57 26    long long whole = 0;
58 26    size_t i = 0;
59 26    int digits = 0;
60 26    int up = 0;
61 90    while (i < n && s[i] >= '0' && s[i] <= '9') {
62 65        if (whole > 900000000000000LL) return -1;
63 64        whole = whole * 10 + (s[i] - '0');
64 64        digits++;
65 64        i++;
66     }
67 25    if (i < n && s[i] == '.') {
68 6        i++;
69 6        if (i < n && s[i] >= '0' && s[i] <= '9') up = s[i] >= '5';
70 12        while (i < n && s[i] >= '0' && s[i] <= '9') {
71 6            digits++;
72 6            i++;
73         }
74     }
75 25    if (!digits || i != n) return -1;
76 20    return whole + up;
77 }
78 
79 20int ygst_server_timing_parse(const char *value, ygst_timing *t) {
80 20    const char *p = value;
81 20    unsigned found = 0;
82 20    if (!value || !t) return 0;
83 29    for (;;) {
84         const char *name;
85         size_t name_n;
86 47        long long dur = -1;
87 73        while (*p == ',' || is_ows(*p)) p++;
88 47        if (!*p) break;
89 29        name = p;
90 182        while (*p && *p != ';' && *p != ',' && !is_ows(*p)) p++;
91 29        name_n = (size_t)(p - name);
92 29        for (;;) { /* ;param=value, value a token or a quoted string */
93             const char *pn;
94             const char *pv;
95             size_t pn_n;
96             size_t pv_n;
97 60            while (is_ows(*p)) p++;
98 58            if (*p != ';') break;
99 29            p++;
100 30            while (is_ows(*p)) p++;
101 29            pn = p;
102 118            while (*p && *p != '=' && *p != ';' && *p != ',' && !is_ows(*p)) p++;
103 29            pn_n = (size_t)(p - pn);
104 30            while (is_ows(*p)) p++;
105 29            if (*p != '=') continue;
106 28            p++;
107 29            while (is_ows(*p)) p++;
108 28            if (*p == '"') {
109 3                pv = ++p;
110 16                while (*p && *p != '"') {
111 13                    if (*p == '\\' && p[1]) p++;
112 13                    p++;
113                 }
114 3                pv_n = (size_t)(p - pv);
115 3                if (*p == '"') p++;
116             } else {
117 25                pv = p;
118 108                while (*p && *p != ';' && *p != ',' && !is_ows(*p)) p++;
119 25                pv_n = (size_t)(p - pv);
120             }
121 28            if (word_eq(pn, pn_n, "dur")) dur = decimal_ms(pv, pv_n);
122         }
123 29        while (*p && *p != ',') p++; /* anything else of this metric */
124 29        if (dur < 0) continue;
125 20        if (word_eq(name, name_n, "total")) {
126 8            t->server_total_ms = dur;
127 8            found |= 1u;
128 12        } else if (word_eq(name, name_n, "engine")) {
129 5            t->server_engine_ms = dur;
130 5            found |= 2u;
131 7        } else if (word_eq(name, name_n, "platform")) {
132 4            t->server_platform_ms = dur;
133 4            found |= 4u;
134         }
135     }
136 18    return (int)((found & 1u) + ((found >> 1) & 1u) + ((found >> 2) & 1u));
137 }
138 
139 /* ------------------------------------------------------------------ timing object */
140 
141 15static void number_ms_member(ygst_span obj, const char *key, long long *out) {
142     ygst_span v;
143     double d;
144 15    if (ygst_json_member(obj, key, &v) != 1 || ygst_json_type(v) != 'n' || ygst_json_number(v, &d) != 0) return;
145 14    if (d >= 0 && d < 9.0e15) *out = (long long)(d + 0.5);
146 }
147 
148 54int ygst_timing_member(ygst_span root, ygst_timing *t, ygst_span *timing) {
149     ygst_span v;
150 54    if (timing) {
151 53        timing->p = NULL;
152 53        timing->n = 0;
153     }
154 54    if (!root.p || ygst_json_type(root) != 'o') return -1;
155 52    if (ygst_json_member(root, "timing", &v) != 1 || ygst_json_type(v) != 'o') return 0;
156 7    if (timing) *timing = v;
157 7    if (t) {
158 5        number_ms_member(v, "totalMs", &t->server_total_ms);
159 5        number_ms_member(v, "engineMs", &t->server_engine_ms);
160 5        number_ms_member(v, "platformMs", &t->server_platform_ms);
161     }
162 7    return 1;
163 }
164 
165 /* ------------------------------------------------------------------ log lines */
166 
167 9void ygst_timing_set_fallback(ygst_timing *t, const char *reason) {
168 9    size_t i = 0;
169 9    size_t n = 0;
170 9    if (!t) return;
171 251    while (reason && reason[i]) {
172 244        unsigned char lead = (unsigned char)reason[i];
173 244        size_t len = lead < 0x80 ? 1 : (lead >= 0xF0 ? 4 : (lead >= 0xE0 ? 3 : 2));
174         size_t k;
175 244        if (n + len >= sizeof t->fallback) break; /* whole UTF-8 sequences only */
176 592        for (k = 0; k < len && reason[i + k]; k++) {
177 349            char c = reason[i + k];
178 349            if (c == '"') c = '\'';
179 349            if (c == '\r' || c == '\n' || c == '\t') c = ' ';
180 349            t->fallback[n++] = c;
181         }
182 243        i += k;
183     }
184 8    t->fallback[n] = 0;
185 }
186 
187 292static long long between(int64_t from, int64_t to) {
188 292    if (!from || !to || to < from) return -1;
189 267    return (long long)(to - from);
190 }
191 
192 406static void put_ms(ygst_buf *b, const char *key, long long ms) {
193 406    ygst_buf_append_char(b, ' ');
194 406    ygst_buf_append_str(b, key);
195 406    ygst_buf_append_char(b, '=');
196 406    if (ms < 0) {
197 29        ygst_buf_append_char(b, '-');
198 29        return;
199     }
200 377    ygst_buf_append_int(b, ms);
201 377    ygst_buf_append_str(b, "ms");
202 }
203 
204 183static void put_bytes(ygst_buf *b, const char *key, long long n) {
205 183    ygst_buf_append_char(b, ' ');
206 183    ygst_buf_append_str(b, key);
207 183    ygst_buf_append_char(b, '=');
208 183    if (n < 0) {
209 11        ygst_buf_append_char(b, '-');
210 11        return;
211     }
212 172    ygst_buf_append_int(b, n);
213 172    ygst_buf_append_char(b, 'B');
214 }
215 
216 61static const char *mode_name(ygst_tm_mode m) {
217 61    switch (m) {
218 39    case YGST_TM_REST:
219 39        return "rest";
220 16    case YGST_TM_WS:
221 16        return "ws";
222 5    case YGST_TM_WS_REST:
223 5        return "ws>rest";
224 1    default:
225 1        return "local";
226     }
227 }
228 
229 61static void put_audio(ygst_buf *b, const ygst_timing *t) {
230     char s[48];
231 120    if (t->audio_bytes >= 0 && t->audio_bytes_per_sec > 0) {
232 59        long long bps = t->audio_bytes_per_sec;
233 59        long long hundredths = (t->audio_bytes * 100 + bps / 2) / bps;
234 59        snprintf(s, sizeof s, " audio=%lld.%02llds", hundredths / 100, hundredths % 100);
235 59        ygst_buf_append_str(b, s);
236     } else {
237 2        ygst_buf_append_str(b, " audio=-");
238     }
239 61}
240 
241 120static void put_token(ygst_buf *b, const char *token_id) {
242 120    ygst_buf_append_str(b, "timing tokenId=");
243 120    ygst_buf_append_str(b, token_id ? token_id : "");
244 120}
245 
246 63int ygst_timing_summary(const ygst_timing *t, const char *token_id, ygst_buf *out) {
247     int ws;
248     int64_t done;
249     long long upload, wait, download;
250 63    if (!t || !out) return -1;
251 61    ws = t->mode == YGST_TM_WS;
252 61    if (ws) {
253 16        int64_t first = t->ws_final_first_ms;
254 16        done = t->ws_final_ms;
255 16        upload = between(t->stop_ms, t->ws_end_ms);
256 16        wait = between(t->ws_end_ms, first ? first : done);
257 16        download = between(first, done);
258     } else {
259         /* the split of the last attempt from the transport, placed after its start */
260 45        int64_t body_sent = t->rest_start_ms && t->upload_ms >= 0 ? t->rest_start_ms + t->upload_ms : 0;
261 45        done = t->rest_done_ms;
262 45        upload = between(t->stop_ms, body_sent);
263 45        wait = t->wait_ms;
264 45        download = t->download_ms;
265     }
266 61    put_token(out, token_id);
267 61    ygst_buf_append_str(out, " mode=");
268 61    ygst_buf_append_str(out, mode_name(t->mode));
269 61    put_audio(out, t);
270 61    put_ms(out, "afterStop", between(t->stop_ms, t->delivered_ms));
271 61    put_ms(out, "upload", upload);
272 61    put_ms(out, "wait", wait);
273 61    put_ms(out, "download", download);
274 61    put_ms(out, "parse", between(done, t->parsed_ms));
275 61    put_ms(out, "deliver", between(t->parsed_ms, t->delivered_ms));
276 61    if (ws) {
277 16        put_ms(out, "handshake", between(t->ws_connect_ms, t->ws_handshake_ms));
278 16        put_bytes(out, "queued", t->queued_at_stop);
279 16        put_bytes(out, "final", t->body_bytes);
280 16        put_bytes(out, "wire", t->wire_bytes);
281 16        if (t->deflate) ygst_buf_append_str(out, " deflate");
282 16        if (t->attempts > 1) {
283 4            ygst_buf_append_str(out, " connections=");
284 4            ygst_buf_append_int(out, t->attempts);
285         }
286     } else {
287 45        ygst_buf_append_str(out, " attempts=");
288 45        ygst_buf_append_int(out, t->attempts);
289 45        put_bytes(out, "sent", t->upload_bytes);
290 45        put_bytes(out, "body", t->body_bytes);
291 45        put_bytes(out, "wire", t->wire_bytes);
292 45        if (t->encoding[0]) {
293 1            ygst_buf_append_char(out, ' ');
294 1            ygst_buf_append_str(out, t->encoding);
295         }
296     }
297 61    if (t->server_total_ms >= 0) put_ms(out, "server.total", t->server_total_ms);
298 61    if (t->server_engine_ms >= 0) put_ms(out, "server.engine", t->server_engine_ms);
299 61    if (t->server_platform_ms >= 0) put_ms(out, "server.platform", t->server_platform_ms);
300 61    if (t->replayed) ygst_buf_append_str(out, " replayed");
301 61    if (!t->ok && t->err_id) {
302 11        ygst_buf_append_str(out, " errId=");
303 11        ygst_buf_append_int(out, t->err_id);
304     }
305 61    if (t->mode == YGST_TM_WS_REST && t->fallback[0]) {
306 5        ygst_buf_append_str(out, " fallback=\"");
307 5        ygst_buf_append_str(out, t->fallback);
308 5        ygst_buf_append_char(out, '"');
309     }
310 61    ygst_buf_cstr(out);
311 61    return out->oom ? -1 : 0;
312 }
313 
314 57int ygst_timing_phases(const ygst_timing *t, const char *token_id, ygst_buf *out) {
315     static const char *const names[] = {"start",       "connect",      "handshake",    "params", "firstAudio",
316                                         "stop",        "endWritten",   "finalFirstByte", "final", "requestStart",
317                                         "responseDone", "parsed",      "delivered"};
318     int64_t at[sizeof names / sizeof names[0]];
319 57    int64_t origin = 0;
320     size_t i;
321 57    if (!t || !out) return -1;
322 56    at[0] = t->start_ms;
323 56    at[1] = t->ws_connect_ms;
324 56    at[2] = t->ws_handshake_ms;
325 56    at[3] = t->ws_params_ms;
326 56    at[4] = t->ws_first_audio_ms;
327 56    at[5] = t->stop_ms;
328 56    at[6] = t->ws_end_ms;
329 56    at[7] = t->ws_final_first_ms;
330 56    at[8] = t->ws_final_ms;
331 56    at[9] = t->rest_start_ms;
332 56    at[10] = t->rest_done_ms;
333 56    at[11] = t->parsed_ms;
334 56    at[12] = t->delivered_ms;
335 784    for (i = 0; i < sizeof names / sizeof names[0]; i++) {
336 728        if (at[i] && (!origin || at[i] < origin)) origin = at[i];
337     }
338 56    put_token(out, token_id);
339 56    ygst_buf_append_str(out, " phases");
340 784    for (i = 0; i < sizeof names / sizeof names[0]; i++) {
341 728        if (!at[i]) continue;
342 399        ygst_buf_append_char(out, ' ');
343 399        ygst_buf_append_str(out, names[i]);
344 399        ygst_buf_append_str(out, "=+");
345 399        ygst_buf_append_int(out, (long long)(at[i] - origin));
346 399        ygst_buf_append_str(out, "ms");
347     }
348 56    ygst_buf_cstr(out);
349 56    return out->oom ? -1 : 0;
350 }
351 
352 /* " key=<number as the platform wrote it>" */
353 12static void put_raw_number(ygst_buf *b, ygst_span obj, const char *key) {
354     ygst_span v;
355 12    if (ygst_json_member(obj, key, &v) != 1 || ygst_json_type(v) != 'n') return;
356 8    ygst_buf_append_char(b, ' ');
357 8    ygst_buf_append_str(b, key);
358 8    ygst_buf_append_char(b, '=');
359 8    ygst_buf_append(b, v.p, v.n);
360 }
361 
362 58int ygst_timing_stages(ygst_span timing, const char *token_id, ygst_buf *out) {
363     ygst_buf members;
364     ygst_span stages;
365     int rc;
366 58    if (!out) return -1;
367 57    if (!timing.p || ygst_json_type(timing) != 'o') return 0;
368 4    ygst_buf_init(&members);
369 4    if (ygst_json_member(timing, "engineStages", &stages) == 1 && ygst_json_type(stages) == 'o') {
370 3        size_t pos = 0;
371         ygst_span key, value;
372 13        while (ygst_json_object_next(stages, &pos, &key, &value) == 1) {
373 10            if (ygst_json_type(value) != 'n') continue;
374 8            ygst_buf_append_char(&members, ' ');
375 8            ygst_json_string(key, &members);
376 8            ygst_buf_append_char(&members, '=');
377 8            ygst_buf_append(&members, value.p, value.n);
378         }
379     }
380 4    put_raw_number(&members, timing, "receivedAt");
381 4    put_raw_number(&members, timing, "wsFirstAudioAt");
382 4    put_raw_number(&members, timing, "audioBytes");
383 4    rc = members.oom ? -1 : 0;
384 4    if (rc == 0 && members.len) {
385 3        put_token(out, token_id);
386 3        ygst_buf_append_str(out, " engineStages");
387 3        ygst_buf_append(out, members.data, members.len);
388 3        ygst_buf_cstr(out);
389 3        rc = out->oom ? -1 : 1;
390     }
391 4    ygst_buf_free(&members);
392 4    return rc;
393 }