STKouyuEngine C core line coverage

Total 98.91% (2441 of 2468 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.c13614097.14%
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 457static uint32_t le32(const unsigned char *p) {
26 457    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
27 }
28 
29 348static uint16_t le16(const unsigned char *p) {
30 348    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 125int ygst_is_riff_wave(const unsigned char *p, size_t n) {
51 125    return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0;
52 }
53 
54 123int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) {
55 123    size_t pos = 12;
56 123    int have_fmt = 0;
57 123    memset(info, 0, sizeof *info);
58 123    if (!ygst_is_riff_wave(p, n)) return -1;
59 341    while (pos + 8 <= n) {
60 341        uint32_t size = le32(p + pos + 4);
61 341        const unsigned char *id = p + pos;
62 341        size_t body = pos + 8;
63 341        if (memcmp(id, "fmt ", 4) == 0) {
64 117            if (size < 16 || body + 16 > n) return -1;
65 116            info->format = le16(p + body);
66 116            info->channels = le16(p + body + 2);
67 116            info->sample_rate = (int)le32(p + body + 4);
68 116            info->bits = le16(p + body + 14);
69 116            have_fmt = 1;
70 224        } else if (memcmp(id, "data", 4) == 0) {
71 117            if (!have_fmt) return -1;
72 116            info->data_offset = body;
73             /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */
74 116            info->data_len = (size == 0 || size > n - body) ? n - body : size;
75 116            return 0;
76         }
77 223        if (size > n - body) return -1;
78 222        pos = body + size + (size & 1u);
79     }
80 0    return -1;
81 }
82 
83 81long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) {
84     double per_second;
85 81    if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1;
86 79    per_second = (double)sample_rate * channels * (bits / 8.0);
87 79    if (per_second <= 0) return -1;
88 79    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 69static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) {
93 69    double per_second = (double)sample_rate * channels * (bits / 8.0);
94 69    if (per_second <= 0) return 0;
95 68    return (double)bytes * 1000.0 > (double)ms * per_second;
96 }
97 
98 28int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) {
99     long ms;
100 28    if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY;
101 27    if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE;
102 26    ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits);
103 26    if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
104 24    if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE;
105 22    return 0;
106 }
107 
108 54int ygst_audio_check_upload(const unsigned char *data, size_t len) {
109     ygst_wav_info info;
110 54    if (!data || len == 0) return YGST_ERRID_AUDIO_EMPTY;
111 52    if (len > YGST_MAX_AUDIO_BYTES) return YGST_ERRID_AUDIO_TOO_LARGE;
112 51    if (ygst_wav_parse(data, len, &info) == 0) {
113         long ms;
114 49        if (info.data_len == 0) return YGST_ERRID_AUDIO_EMPTY;
115 47        if (info.format == 1 || info.format == 3 || info.format == 0xFFFE) {
116 47            ms = ygst_pcm_duration_ms(info.data_len, info.sample_rate, info.channels, info.bits);
117 47            if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT;
118 45            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 46    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 5388void ygst_buf_init(ygst_buf *b) {
14 5388    b->data = NULL;
15 5388    b->len = 0;
16 5388    b->cap = 0;
17 5388    b->oom = 0;
18 5388}
19 
20 2694void ygst_buf_free(ygst_buf *b) {
21 2694    free(b->data);
22 2694    ygst_buf_init(b);
23 2694}
24 
25 4071void ygst_buf_reset(ygst_buf *b) {
26 4071    b->len = 0;
27 4071    b->oom = 0;
28 4071    if (b->data) b->data[0] = 0;
29 4071}
30 
31 30380int ygst_buf_reserve(ygst_buf *b, size_t extra) {
32     size_t need;
33     size_t cap;
34     unsigned char *p;
35 30380    if (b->oom) return -1;
36 30380    if (extra > (size_t)-1 - b->len - 1) {
37 0        b->oom = 1;
38 0        return -1;
39     }
40 30380    need = b->len + extra + 1; /* room for the terminating NUL */
41 30380    if (need <= b->cap) return 0;
42 3171    cap = b->cap ? b->cap : 64;
43 6546    while (cap < need) {
44 3375        if (cap > ((size_t)-1) / 2) {
45 0            cap = need;
46 0            break;
47         }
48 3375        cap *= 2;
49     }
50 3171    p = (unsigned char *)realloc(b->data, cap);
51 3171    if (!p) {
52 0        b->oom = 1;
53 0        return -1;
54     }
55 3171    b->data = p;
56 3171    b->cap = cap;
57 3171    return 0;
58 }
59 
60 30249int ygst_buf_append(ygst_buf *b, const void *p, size_t n) {
61 30249    if (n == 0) return b->oom ? -1 : 0;
62 30235    if (ygst_buf_reserve(b, n) != 0) return -1;
63 30235    memcpy(b->data + b->len, p, n);
64 30235    b->len += n;
65 30235    b->data[b->len] = 0;
66 30235    return 0;
67 }
68 
69 6188int ygst_buf_append_str(ygst_buf *b, const char *s) {
70 6188    return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
71 }
72 
73 14885int ygst_buf_append_char(ygst_buf *b, char c) {
74 14885    return ygst_buf_append(b, &c, 1);
75 }
76 
77 1133int ygst_buf_append_int(ygst_buf *b, long long v) {
78     char tmp[32];
79 1133    int n = snprintf(tmp, sizeof tmp, "%lld", v);
80 1133    if (n < 0) return -1;
81 1133    return ygst_buf_append(b, tmp, (size_t)n);
82 }
83 
84 9935const char *ygst_buf_cstr(ygst_buf *b) {
85 9935    if (!b->data) {
86 145        if (ygst_buf_reserve(b, 0) != 0) return "";
87 145        b->data[0] = 0;
88     }
89 9935    return (const char *)b->data;
90 }
91 
92 1118char *ygst_strdup(const char *s) {
93     size_t n;
94     char *p;
95 1118    if (!s) return NULL;
96 1117    n = strlen(s);
97 1117    p = (char *)malloc(n + 1);
98 1117    if (!p) return NULL;
99 1117    memcpy(p, s, n + 1);
100 1117    return p;
101 }
102 
103 1020static int lower_ascii(int c) {
104 1020    return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
105 }
106 
107 247int ygst_strcaseeq(const char *a, const char *b) {
108 247    if (!a || !b) return 0;
109 633    while (*a && *b) {
110 510        if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0;
111 387        a++;
112 387        b++;
113     }
114 123    return *a == 0 && *b == 0;
115 }
116 
117 13int ygst_has_suffix_ci(const char *s, const char *suffix) {
118     size_t n;
119     size_t m;
120 13    if (!s || !suffix) return 0;
121 12    n = strlen(s);
122 12    m = strlen(suffix);
123 12    if (m > n) return 0;
124 11    return ygst_strcaseeq(s + (n - m), suffix);
125 }
126 
127 586static int is_space(unsigned char c) {
128 586    return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
129 }
130 
131 169size_t ygst_trim_copy(const char *s, char *out, size_t cap) {
132     const char *e;
133     size_t n;
134 169    if (!out || cap == 0) return 0;
135 169    out[0] = 0;
136 169    if (!s) return 0;
137 174    while (*s && is_space((unsigned char)*s)) s++;
138 167    e = s + strlen(s);
139 172    while (e > s && is_space((unsigned char)e[-1])) e--;
140 167    n = (size_t)(e - s);
141 167    if (n >= cap) n = cap - 1;
142 167    memcpy(out, s, n);
143 167    out[n] = 0;
144 167    return n;
145 }
146 
147 255int ygst_is_blank(const char *s) {
148 255    if (!s) return 1;
149 258    while (*s) {
150 244        if (!is_space((unsigned char)*s)) return 0;
151 12        s++;
152     }
153 14    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 16107static 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 273819    for (i = 0; i < 16; i++) {
29 257712        w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) |
30 257712               (uint32_t)p[i * 4 + 3];
31     }
32 789243    for (i = 16; i < 64; i++) {
33 773136        uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3);
34 773136        uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10);
35 773136        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
36     }
37 16107    a = c->h[0];
38 16107    b = c->h[1];
39 16107    cc = c->h[2];
40 16107    d = c->h[3];
41 16107    e = c->h[4];
42 16107    f = c->h[5];
43 16107    g = c->h[6];
44 16107    h = c->h[7];
45 1046955    for (i = 0; i < 64; i++) {
46 1030848        uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25);
47 1030848        uint32_t ch = (e & f) ^ ((~e) & g);
48 1030848        uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22);
49 1030848        uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
50 1030848        t1 = h + S1 + ch + K[i] + w[i];
51 1030848        t2 = S0 + maj;
52 1030848        h = g;
53 1030848        g = f;
54 1030848        f = e;
55 1030848        e = d + t1;
56 1030848        d = cc;
57 1030848        cc = b;
58 1030848        b = a;
59 1030848        a = t1 + t2;
60     }
61 16107    c->h[0] += a;
62 16107    c->h[1] += b;
63 16107    c->h[2] += cc;
64 16107    c->h[3] += d;
65 16107    c->h[4] += e;
66 16107    c->h[5] += f;
67 16107    c->h[6] += g;
68 16107    c->h[7] += h;
69 16107}
70 
71 203void ygst_sha256_init(ygst_sha256_ctx *c) {
72 203    c->h[0] = 0x6a09e667u;
73 203    c->h[1] = 0xbb67ae85u;
74 203    c->h[2] = 0x3c6ef372u;
75 203    c->h[3] = 0xa54ff53au;
76 203    c->h[4] = 0x510e527fu;
77 203    c->h[5] = 0x9b05688cu;
78 203    c->h[6] = 0x1f83d9abu;
79 203    c->h[7] = 0x5be0cd19u;
80 203    c->total = 0;
81 203    c->used = 0;
82 203}
83 
84 1391void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) {
85 1391    const unsigned char *p = (const unsigned char *)data;
86 1391    c->total += (uint64_t)n;
87 1391    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 16264    while (n >= 64) {
99 14885        sha256_block(c, p);
100 14885        p += 64;
101 14885        n -= 64;
102     }
103 1379    if (n) {
104 1193        memcpy(c->block, p, n);
105 1193        c->used = n;
106     }
107 }
108 
109 203void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) {
110 203    uint64_t bits = c->total * 8u;
111     int i;
112 203    c->block[c->used++] = 0x80;
113 203    if (c->used > 56) {
114 23        memset(c->block + c->used, 0, 64 - c->used);
115 23        sha256_block(c, c->block);
116 23        c->used = 0;
117     }
118 203    memset(c->block + c->used, 0, 56 - c->used);
119 1827    for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i));
120 203    sha256_block(c, c->block);
121 1827    for (i = 0; i < 8; i++) {
122 1624        out[i * 4] = (unsigned char)(c->h[i] >> 24);
123 1624        out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16);
124 1624        out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8);
125 1624        out[i * 4 + 3] = (unsigned char)c->h[i];
126     }
127 203}
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 82void 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 82    memset(k, 0, sizeof k);
143 82    if (key_len > 64) {
144 1        ygst_sha256(key, key_len, k);
145 81    } else if (key_len) {
146 81        memcpy(k, key, key_len);
147     }
148 5330    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36);
149 82    ygst_sha256_init(&c);
150 82    ygst_sha256_update(&c, pad, 64);
151 82    ygst_sha256_update(&c, msg, msg_len);
152 82    ygst_sha256_final(&c, inner);
153 5330    for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c);
154 82    ygst_sha256_init(&c);
155 82    ygst_sha256_update(&c, pad, 64);
156 82    ygst_sha256_update(&c, inner, 32);
157 82    ygst_sha256_final(&c, out);
158 82}
159 
160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
161 
162 111size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) {
163 111    size_t need = ((n + 2) / 3) * 4;
164     size_t i;
165 111    size_t o = 0;
166 111    if (!out || cap < need + 1) return 0;
167 1019    for (i = 0; i + 2 < n; i += 3) {
168 910        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2];
169 910        out[o++] = B64[(v >> 18) & 63];
170 910        out[o++] = B64[(v >> 12) & 63];
171 910        out[o++] = B64[(v >> 6) & 63];
172 910        out[o++] = B64[v & 63];
173     }
174 109    if (n - i == 1) {
175 25        uint32_t v = (uint32_t)in[i] << 16;
176 25        out[o++] = B64[(v >> 18) & 63];
177 25        out[o++] = B64[(v >> 12) & 63];
178 25        out[o++] = '=';
179 25        out[o++] = '=';
180 84    } else if (n - i == 2) {
181 81        uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
182 81        out[o++] = B64[(v >> 18) & 63];
183 81        out[o++] = B64[(v >> 12) & 63];
184 81        out[o++] = B64[(v >> 6) & 63];
185 81        out[o++] = '=';
186     }
187 109    out[o] = 0;
188 109    return o;
189 }
190 
191 200void ygst_hex_lower(const unsigned char *in, size_t n, char *out) {
192     static const char H[] = "0123456789abcdef";
193     size_t i;
194 2840    for (i = 0; i < n; i++) {
195 2640        out[i * 2] = H[in[i] >> 4];
196 2640        out[i * 2 + 1] = H[in[i] & 15];
197     }
198 200    out[n * 2] = 0;
199 200}

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 108static 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 108    z += 719468;
18 108    era = (z >= 0 ? z : z - 146096) / 146097;
19 108    doe = (unsigned)(z - era * 146097);
20 108    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
21 108    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
22 108    mp = (5 * doy + 2) / 153;
23 108    *d = doy - (153 * mp + 2) / 5 + 1;
24 108    *m = mp < 10 ? mp + 3 : mp - 9;
25 108    *y = (long long)yoe + era * 400 + (*m <= 2);
26 108}
27 
28 108int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) {
29 108    long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL;
30 108    long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL);
31 108    long long rem = ms - days * 86400000LL;
32     long long y;
33     unsigned m, d, hh, mi, ss, mss;
34 108    civil_from_days(days, &y, &m, &d);
35 108    if (y < 0) y = 0;
36 108    if (y > 9999) y = 9999;
37 108    hh = (unsigned)(rem / 3600000LL) % 24u;
38 108    mi = (unsigned)(rem / 60000LL) % 60u;
39 108    ss = (unsigned)(rem / 1000LL) % 60u;
40 108    mss = (unsigned)rem % 1000u;
41 108    snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss,
42              mss);
43 108    return 0;
44 }
45 
46 66int ygst_align_result(ygst_span result, ygst_buf *out) {
47     ygst_span sentences, sent, details, d, scores, v, so, sp;
48 66    size_t spos = 0;
49 66    size_t copied = 0; /* bytes of result already copied to out */
50 66    int added = 0;
51 66    if (!result.p) return -1;
52 118    if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 ||
53 53        ygst_json_type(sentences) != 'a') {
54 13        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 1074static void key(ygst_buf *out, const char *k, int first) {
90 1074    if (!first) ygst_buf_append_char(out, ',');
91 1074    ygst_json_escape(out, k);
92 1074    ygst_buf_append_char(out, ':');
93 1074}
94 
95 56int 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 56    if (!result_raw.p || result_raw.n == 0) return -1;
99 55    ygst_buf_append_char(out, '{');
100 55    key(out, "tokenId", 1);
101 55    ygst_json_escape(out, token_id);
102 55    if (record_id_json && record_id_json[0]) {
103 54        key(out, "recordId", 0);
104 54        ygst_buf_append_str(out, record_id_json);
105     }
106 55    key(out, "applicationId", 0);
107 55    ygst_json_escape(out, app_key);
108 55    key(out, "userId", 0);
109 55    ygst_json_escape(out, user_id);
110 55    key(out, "refText", 0);
111 55    ygst_json_escape(out, ref_text);
112 55    key(out, "eof", 0);
113 55    ygst_buf_append_char(out, '1');
114 55    key(out, "dtLastResponse", 0);
115 55    ygst_json_escape(out, dt_last_response);
116 55    key(out, "result", 0);
117 55    if (ygst_align_result(result_raw, out) < 0) return -1;
118 55    if (params_json && params_json[0]) {
119 3        key(out, "params", 0);
120 3        ygst_buf_append_str(out, params_json);
121     }
122 55    if (audio_url && audio_url[0]) {
123 3        key(out, "audioUrl", 0);
124 3        ygst_json_escape(out, audio_url);
125     }
126 55    ygst_buf_append_char(out, '}');
127 55    ygst_buf_cstr(out);
128 55    return out->oom ? -1 : 0;
129 }
130 
131 21int ygst_envelope_error(ygst_buf *out, const char *token_id, int err_id, const char *message,
132                         const char *app_key) {
133 21    ygst_buf_append_char(out, '{');
134 21    key(out, "tokenId", 1);
135 21    ygst_json_escape(out, token_id);
136 21    key(out, "errId", 0);
137 21    ygst_buf_append_int(out, err_id);
138 21    key(out, "error", 0);
139 21    ygst_json_escape(out, message);
140 21    key(out, "eof", 0);
141 21    ygst_buf_append_char(out, '1');
142 21    key(out, "applicationId", 0);
143 21    ygst_json_escape(out, app_key);
144 21    ygst_buf_append_char(out, '}');
145 21    ygst_buf_cstr(out);
146 21    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 45int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) {
318     size_t n;
319     int w;
320 45    if (!base || !core_type || !out) return -1;
321 44    n = strlen(base);
322 45    while (n > 0 && base[n - 1] == '/') n--;
323 44    w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type);
324 44    return (w < 0 || (size_t)w >= cap) ? -1 : 0;
325 }
326 
327 66int ygst_token_id(const unsigned char rnd[16], char out[33]) {
328 66    ygst_hex_lower(rnd, 16, out);
329 66    return 0;
330 }
331 
332 67int ygst_idempotency_key_ok(const char *k) {
333 67    size_t n = 0;
334 67    if (!k) return 0;
335 2331    for (; k[n]; n++) {
336 2270        unsigned char c = (unsigned char)k[n];
337 2270        if (c < 0x21 || c > 0x7E) return 0;
338 2266        if (n >= 200) return 0;
339     }
340 61    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 565void ygst_fields_init(ygst_fields *f) {
14 565    f->items = NULL;
15 565    f->count = 0;
16 565    f->cap = 0;
17 565    f->oom = 0;
18 565}
19 
20 278void ygst_fields_free(ygst_fields *f) {
21     size_t i;
22 830    for (i = 0; i < f->count; i++) {
23 552        free(f->items[i].name);
24 552        free(f->items[i].value);
25     }
26 278    free(f->items);
27 278    ygst_fields_init(f);
28 278}
29 
30 575static long find_index(const ygst_fields *f, const char *name) {
31     size_t i;
32 2753    for (i = 0; i < f->count; i++) {
33 2195        if (strcmp(f->items[i].name, name) == 0) return (long)i;
34     }
35 558    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 565int ygst_fields_set(ygst_fields *f, const char *name, const char *value) {
49     long i;
50     char *v;
51 565    if (!name || f->oom) return -1;
52 564    if (!value) {
53 1        ygst_fields_remove(f, name);
54 1        return 0;
55     }
56 563    i = find_index(f, name);
57 563    v = ygst_strdup(value);
58 563    if (!v) {
59 0        f->oom = 1;
60 0        return -1;
61     }
62 563    if (i >= 0) {
63 10        free(f->items[i].value);
64 10        f->items[i].value = v;
65 10        return 0;
66     }
67 553    if (f->count == f->cap) {
68 200        size_t cap = f->cap ? f->cap * 2 : 16;
69 200        ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field));
70 200        if (!p) {
71 0            free(v);
72 0            f->oom = 1;
73 0            return -1;
74         }
75 200        f->items = p;
76 200        f->cap = cap;
77     }
78 553    f->items[f->count].name = ygst_strdup(name);
79 553    if (!f->items[f->count].name) {
80 0        free(v);
81 0        f->oom = 1;
82 0        return -1;
83     }
84 553    f->items[f->count].value = v;
85 553    f->count++;
86 553    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 45int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) {
95     size_t i;
96 187    for (i = 0; i < src->count; i++) {
97 142        if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1;
98     }
99 45    return 0;
100 }
101 
102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */
103 816static uint32_t next_cp(const unsigned char **s) {
104 816    const unsigned char *p = *s;
105 816    uint32_t c = p[0];
106 816    int n = 0;
107     int i;
108 816    if (c < 0x80) {
109 806        *s = p + 1;
110 806        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 816static int utf16_units(uint32_t cp, uint32_t u[2]) {
138 816    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 812    u[0] = cp;
145 812    return 1;
146 }
147 
148 333int ygst_utf16_compare(const char *a, const char *b) {
149 333    const unsigned char *pa = (const unsigned char *)a;
150 333    const unsigned char *pb = (const unsigned char *)b;
151     uint32_t ua[2], ub[2];
152 333    int na = 0, nb = 0, ia = 0, ib = 0;
153 77    for (;;) {
154         uint32_t ca, cb;
155 410        if (ia == na) {
156 410            if (!*pa) {
157 2                na = 0;
158             } else {
159 408                na = utf16_units(next_cp(&pa), ua);
160             }
161 410            ia = 0;
162         }
163 410        if (ib == nb) {
164 410            if (!*pb) {
165 2                nb = 0;
166             } else {
167 408                nb = utf16_units(next_cp(&pb), ub);
168             }
169 410            ib = 0;
170         }
171 410        if (na == 0 || nb == 0) {
172 3            if (na == 0 && nb == 0) return 0;
173 2            return na == 0 ? -1 : 1;
174         }
175 407        ca = ua[ia++];
176 407        cb = ub[ib++];
177 407        if (ca != cb) return ca < cb ? -1 : 1;
178     }
179 }
180 
181 325static int cmp_field_ptr(const void *x, const void *y) {
182 325    const ygst_field *a = *(const ygst_field *const *)x;
183 325    const ygst_field *b = *(const ygst_field *const *)y;
184 325    return ygst_utf16_compare(a->name, b->name);
185 }
186 
187 88int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) {
188     const ygst_field **v;
189 88    size_t n = 0;
190     size_t i;
191 88    int first = 1;
192 88    if (f->oom) return -1;
193 88    v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v));
194 88    if (!v) return -1;
195 397    for (i = 0; i < f->count; i++) {
196 309        if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i];
197     }
198 88    qsort(v, n, sizeof(*v), cmp_field_ptr);
199 375    for (i = 0; i < n; i++) {
200 287        if (!first) ygst_buf_append_char(out, '&');
201 287        first = 0;
202 287        ygst_buf_append_str(out, v[i]->name);
203 287        ygst_buf_append_char(out, '=');
204 287        ygst_buf_append_str(out, v[i]->value);
205     }
206 88    free(v);
207 88    ygst_buf_cstr(out);
208 88    return out->oom ? -1 : 0;
209 }
210 
211 80int 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 80    out[0] = 0;
216 80    if (!secret) return -1;
217 79    ygst_buf_init(&payload);
218 79    rc = ygst_sign_payload(f, &payload);
219 79    if (rc == 0) {
220 79        ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"",
221                          payload.len, mac);
222 79        if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1;
223     }
224 79    ygst_buf_free(&payload);
225 79    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 5672336static const char *ws(const char *p, const char *e) {
16 11653379    while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
17 5672336    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 1557622static const char *skip_string(const char *p, const char *e) {
28 1557622    if (p >= e || *p != '"') return NULL;
29 1557619    p++;
30 11254187    while (p < e) {
31 11254185        unsigned char c = (unsigned char)*p;
32 11254185        if (c == '"') return p + 1;
33 9696571        if (c < 0x20) return NULL;
34 9696570        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 9695778        p++;
63     }
64 2    return NULL;
65 }
66 
67 1487732static int is_digit(char c) {
68 1487732    return c >= '0' && c <= '9';
69 }
70 
71 454782static const char *skip_number(const char *p, const char *e) {
72 454782    if (p < e && *p == '-') p++;
73 454782    if (p >= e) return NULL;
74 454781    if (*p == '0') {
75 162750        p++;
76 292031    } else if (*p >= '1' && *p <= '9') {
77 955842        while (p < e && is_digit(*p)) p++;
78     } else {
79 0        return NULL;
80     }
81 454781    if (p < e && *p == '.') {
82 24962        p++;
83 24962        if (p >= e || !is_digit(*p)) return NULL;
84 60153        while (p < e && is_digit(*p)) p++;
85     }
86 454780    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 454779    return p;
93 }
94 
95 241086static const char *skip_lit(const char *p, const char *e, const char *lit) {
96 241086    size_t n = strlen(lit);
97 241086    if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL;
98 241082    return p + n;
99 }
100 
101 1399869static const char *skip_value(const char *p, const char *e, int depth) {
102 1399869    if (depth > YGST_JSON_MAX_DEPTH) return NULL;
103 1399868    p = ws(p, e);
104 1399868    if (p >= e) return NULL;
105 1399864    switch (*p) {
106 258206    case '{':
107 258206        p = ws(p + 1, e);
108 258206        if (p < e && *p == '}') return p + 1;
109         for (;;) {
110 1096496            p = skip_string(ws(p, e), e);
111 1096496            if (!p) return NULL;
112 1096493            p = ws(p, e);
113 1096493            if (p >= e || *p != ':') return NULL;
114 1096492            p = skip_value(p + 1, e, depth + 1);
115 1096492            if (!p) return NULL;
116 1096490            p = ws(p, e);
117 1096490            if (p >= e) return NULL;
118 1096490            if (*p == ',') {
119 838345                p++;
120 838345                continue;
121             }
122 258145            if (*p == '}') return p + 1;
123 1            return NULL;
124         }
125 91406    case '[':
126 91406        p = ws(p + 1, e);
127 91406        if (p < e && *p == ']') return p + 1;
128         for (;;) {
129 172311            p = skip_value(p, e, depth + 1);
130 172311            if (!p) return NULL;
131 172180            p = ws(p, e);
132 172180            if (p >= e) return NULL;
133 172180            if (*p == ',') {
134 106227                p++;
135 106227                continue;
136             }
137 65953            if (*p == ']') return p + 1;
138 1            return NULL;
139         }
140 356270    case '"':
141 356270        return skip_string(p, e);
142 112859    case 't':
143 112859        return skip_lit(p, e, "true");
144 128202    case 'f':
145 128202        return skip_lit(p, e, "false");
146 25    case 'n':
147 25        return skip_lit(p, e, "null");
148 452896    default:
149 452896        if (*p == '-' || is_digit(*p)) return skip_number(p, e);
150 6        return NULL;
151     }
152 }
153 
154 687int 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 687    if (!s) return -1;
159 686    e = s + n;
160 686    start = ws(s, e);
161 686    end = skip_value(start, e, 0);
162 686    if (!end) return -1;
163 658    if (ws(end, e) != e) return -1;
164 656    if (root) {
165 656        root->p = start;
166 656        root->n = (size_t)(end - start);
167     }
168 656    return 0;
169 }
170 
171 230511int ygst_json_type(ygst_span v) {
172 230511    if (!v.p || v.n == 0) return 0;
173 230510    switch (v.p[0]) {
174 151890    case '{':
175 151890        return 'o';
176 55260    case '[':
177 55260        return 'a';
178 6889    case '"':
179 6889        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 3096    default:
187 3096        return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0;
188     }
189 }
190 
191 123270int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) {
192 123270    const char *e = obj.p + obj.n;
193     const char *p;
194     const char *k;
195     const char *v;
196 123270    if (ygst_json_type(obj) != 'o') return -1;
197 123269    p = obj.p + (*pos ? *pos : 1);
198 123269    p = ws(p, e);
199 123269    if (p >= e) return -1;
200 123269    if (*p == '}') return 0;
201 100582    if (*pos) {
202 77902        if (*p != ',') return -1;
203 77902        p = ws(p + 1, e);
204     }
205 100582    k = p;
206 100582    p = skip_string(p, e);
207 100582    if (!p) return -1;
208 100582    if (key) {
209 100582        key->p = k;
210 100582        key->n = (size_t)(p - k);
211     }
212 100582    p = ws(p, e);
213 100582    if (p >= e || *p != ':') return -1;
214 100581    v = ws(p + 1, e);
215 100581    p = skip_value(v, e, 1);
216 100581    if (!p) return -1;
217 100581    if (value) {
218 100581        value->p = v;
219 100581        value->n = (size_t)(p - v);
220     }
221 100581    *pos = (size_t)(p - obj.p);
222 100581    return 1;
223 }
224 
225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */
226 98732static int key_equals(ygst_span tok, const char *key) {
227 98732    size_t klen = strlen(key);
228 98732    if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) {
229 98727        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 22417int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) {
241 22417    size_t pos = 0;
242     ygst_span k, v;
243 22417    int found = 0;
244     int rc;
245 22417    if (ygst_json_type(obj) != 'o' || !key) return -1;
246 121148    while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) {
247 98732        if (key_equals(k, key)) {
248 20988            found = 1;
249 20988            if (out) *out = v;
250         }
251     }
252 22416    if (rc < 0) return -1;
253 22415    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 4275int ygst_json_string(ygst_span v, ygst_buf *out) {
308     const char *p;
309     const char *e;
310 4275    if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1;
311 4273    p = v.p + 1;
312 4273    e = v.p + v.n - 1;
313 4356    while (p < e) {
314 4280        const char *run = p;
315 31446        while (p < e && *p != '\\') p++;
316 4280        if (p > run) ygst_buf_append(out, run, (size_t)(p - run));
317 4280        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 4273    ygst_buf_cstr(out);
368 4273    return out->oom ? -1 : 0;
369 }
370 
371 1894static int number_text(ygst_span v, char *tmp, size_t cap) {
372 1894    if (ygst_json_type(v) != 'n' || v.n >= cap) return -1;
373 1892    if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1;
374 1892    memcpy(tmp, v.p, v.n);
375 1892    tmp[v.n] = 0;
376 1892    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 1873int ygst_json_int(ygst_span v, long long *out) {
397     char tmp[64];
398     size_t i;
399 1873    int neg = 0;
400 1873    long long acc = 0;
401 1873    if (number_text(v, tmp, sizeof tmp) != 0) return -1;
402 1872    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 1868    i = 0;
410 1868    if (tmp[0] == '-') {
411 1        neg = 1;
412 1        i = 1;
413     }
414 6490    for (; tmp[i]; i++) {
415 4623        int dgt = tmp[i] - '0';
416 4623        if (acc > (9223372036854775807LL - dgt) / 10) return -1;
417 4622        acc = acc * 10 + dgt;
418     }
419 1867    *out = neg ? -acc : acc;
420 1867    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 2095int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) {
437     static const char H[] = "0123456789abcdef";
438     size_t i;
439 2095    size_t run = 0;
440 2095    ygst_buf_append_char(out, '"');
441 50724    for (i = 0; i < n; i++) {
442 48629        unsigned char c = (unsigned char)s[i];
443 48629        const char *rep = NULL;
444         char u[7];
445 48629        if (c == '"') {
446 17            rep = "\\\"";
447 48612        } else if (c == '\\') {
448 2            rep = "\\\\";
449 48610        } else if (c == '\n') {
450 3            rep = "\\n";
451 48607        } else if (c == '\r') {
452 1            rep = "\\r";
453 48606        } else if (c == '\t') {
454 1            rep = "\\t";
455 48605        } else if (c == '\b') {
456 1            rep = "\\b";
457 48604        } else if (c == '\f') {
458 1            rep = "\\f";
459 48603        } 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 48629        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 2095    if (n > run) ygst_buf_append(out, s + run, n - run);
476 2095    ygst_buf_append_char(out, '"');
477 2095    return out->oom ? -1 : 0;
478 }
479 
480 2095int ygst_json_escape(ygst_buf *out, const char *s) {
481 2095    if (!s) s = "";
482 2095    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 43int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) {
15     char hex[33];
16 43    ygst_hex_lower(rnd, 16, hex);
17 43    snprintf(out, 64, "----YuguSTCompat%s", hex);
18 43    return 0;
19 }
20 
21 44int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) {
22     int n;
23 44    if (!boundary || !out) return -1;
24 44    n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary);
25 44    return (n < 0 || (size_t)n >= cap) ? -1 : 0;
26 }
27 
28 140static int contains(const unsigned char *hay, size_t n, const char *needle) {
29 140    size_t m = strlen(needle);
30     size_t i;
31 140    if (m == 0 || n < m) return 0;
32 3045246    for (i = 0; i + m <= n; i++) {
33 3045198        if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1;
34     }
35 48    return 0;
36 }
37 
38 50int 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 50    if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1;
43 48    if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1;
44 142    for (i = 0; i < fields->count; i++) {
45 97        const char *v = fields->items[i].value ? fields->items[i].value : "";
46 97        if (!ygst_field_name_ok(fields->items[i].name)) return -1;
47 96        if (contains((const unsigned char *)v, strlen(v), boundary)) return -2;
48     }
49 45    if (file && contains(file, file_len, boundary)) return -2;
50 139    for (i = 0; i < fields->count; i++) {
51 95        ygst_buf_append_str(out, "--");
52 95        ygst_buf_append_str(out, boundary);
53 95        ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
54 95        ygst_buf_append_str(out, fields->items[i].name);
55 95        ygst_buf_append_str(out, "\"\r\n\r\n");
56 95        ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : "");
57 95        ygst_buf_append_str(out, "\r\n");
58     }
59 44    ygst_buf_append_str(out, "--");
60 44    ygst_buf_append_str(out, boundary);
61 44    ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\"");
62 44    ygst_buf_append_str(out, file_field);
63 44    ygst_buf_append_str(out, "\"; filename=\"");
64 44    ygst_buf_append_str(out, filename);
65 44    ygst_buf_append_str(out, "\"\r\nContent-Type: ");
66 44    ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream");
67 44    ygst_buf_append_str(out, "\r\n\r\n");
68 44    if (file_len) ygst_buf_append(out, file, file_len);
69 44    ygst_buf_append_str(out, "\r\n--");
70 44    ygst_buf_append_str(out, boundary);
71 44    ygst_buf_append_str(out, "--\r\n");
72 44    return out->oom ? -1 : 0;
73 }
74 
75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */
76 65const 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 65    if (ygst_is_blank(audio_type)) return "wav";
82 63    ygst_trim_copy(audio_type, t, sizeof t);
83 63    if (t[0] == '.') memmove(t, t + 1, strlen(t));
84 120    for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) {
85 117        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 53const char *ygst_audio_content_type(const char *ext) {
93 53    if (!ext) return "application/octet-stream";
94 52    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.14%

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). open.eval.cn since 2.1.2: Shengtong's
43  * Chinese open question, passed as coreTypeNS because KYTestType has no member for it. */
44 static const char *const SUPPORTED[] = {"word.eval",    "word.eval.pro", "sent.eval",    "sent.eval.pro",
45                                         "para.eval",    "word.eval.cn",  "sent.eval.cn", "para.eval.cn",
46                                         "alpha.eval",   "pinyin",        "open.eval.cn"};
47 
48 /* Shengtong coreTypes that are answered with errId 60003 without network I/O. */
49 static const char *const UNSUPPORTED[] = {"choice.rec",   "open.eval",    "asr.rec",      "asr.eval",
50                                           "align.eval",   "word.eval.fr", "sent.eval.fr", "para.eval.fr",
51                                           "word.eval.jp", "sent.eval.jp", "para.eval.jp", "word.eval.kr",
52                                           "sent.eval.kr", "para.eval.kr"};
53 
54 54const char *ygst_coretype_for_enum(unsigned long kytesttype) {
55 54    if (kytesttype >= sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]) return NULL;
56 51    return KY_TEST_TYPES[kytesttype];
57 }
58 
59 25size_t ygst_coretype_enum_count(void) {
60 25    return sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0];
61 }
62 
63 122ygst_ct_status ygst_coretype_check(const char *core_type) {
64     size_t i;
65 122    if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN;
66 973    for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) {
67 938        if (strcmp(core_type, SUPPORTED[i]) == 0) return YGST_CT_SUPPORTED;
68     }
69 279    for (i = 0; i < sizeof UNSUPPORTED / sizeof UNSUPPORTED[0]; i++) {
70 275        if (strcmp(core_type, UNSUPPORTED[i]) == 0) return YGST_CT_UNSUPPORTED;
71     }
72 4    return YGST_CT_UNKNOWN;
73 }
74 
75 75ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) {
76     const char *mapped;
77 75    if (!out || cap == 0) return YGST_CT_UNKNOWN;
78 74    out[0] = 0;
79 74    if (!ygst_is_blank(core_type_ns)) {
80 68        ygst_trim_copy(core_type_ns, out, cap);
81 68        return ygst_coretype_check(out);
82     }
83 6    mapped = ygst_coretype_for_enum(kytesttype);
84 6    if (!mapped) return YGST_CT_UNSUPPORTED;
85 4    if (strlen(mapped) >= cap) return YGST_CT_UNKNOWN;
86 3    strcpy(out, mapped);
87 3    return ygst_coretype_check(out);
88 }
89 
90 11int ygst_coretype_is_chinese(const char *core_type) {
91 11    return core_type && (ygst_has_suffix_ci(core_type, ".cn") || strcmp(core_type, "pinyin") == 0);
92 }
93 
94 8int ygst_coretype_is_open_question(const char *core_type) {
95 8    return core_type && strcmp(core_type, "open.eval.cn") == 0;
96 }
97 
98 200int ygst_coretype_is_paragraph(const char *core_type) {
99 200    return core_type && strncmp(core_type, "para.", 5) == 0;
100 }
101 
102 100int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) {
103 100    if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1");
104 93    return 0;
105 }
106 
107 75int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text,
108                           const char *ref_pinyin) {
109 75    if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED;
110 70    if (!ygst_is_blank(ref_text)) return 0;
111     /* the refText of an open question is the question: a reference pinyin cannot replace it */
112 9    if (!ygst_is_blank(ref_pinyin) && ygst_coretype_is_chinese(core_type) && !ygst_coretype_is_open_question(core_type))
113 3        return 0;
114 6    return YGST_ERRID_REFTEXT_EMPTY;
115 }
116 
117 /* ------------------------------------------------------------------ number text */
118 
119 69static void fix_decimal_point(char *s) {
120 431    for (; *s; s++) {
121 362        if (*s == ',') *s = '.';
122     }
123 69}
124 
125 3static void trim_fraction_zeros(char *s) {
126 3    char *dot = strchr(s, '.');
127     char *end;
128 3    if (!dot) return;
129 3    end = s + strlen(s);
130 3    while (end > dot + 1 && end[-1] == '0') end--;
131 3    if (end == dot + 1) end = dot;
132 3    *end = 0;
133 }
134 
135 28int ygst_fmt_double(double v, char *out, size_t cap) {
136     char tmp[64];
137     int p;
138 28    if (!out || cap == 0) return -1;
139 27    out[0] = 0;
140 27    if (v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308) return -1; /* NaN, inf */
141 25    if (v == 0) {
142 2        tmp[0] = '0';
143 2        tmp[1] = 0;
144 23    } else if (fabs(v) < 9.0e15 && v == floor(v)) {
145 11        snprintf(tmp, sizeof tmp, "%.0f", v);
146 12    } else if (fabs(v) >= 9.0e15) {
147 1        snprintf(tmp, sizeof tmp, "%.0f", v);
148     } else {
149 41        for (p = 1; p <= 17; p++) {
150 41            char *end = NULL;
151             double back;
152 41            snprintf(tmp, sizeof tmp, "%.*g", p, v);
153 41            fix_decimal_point(tmp);
154 41            back = strtod(tmp, &end);
155 41            if (end && *end) {
156                 /* decimal comma locale: compare through the locale's own separator */
157                 char alt[64];
158                 char *dot;
159 0                strcpy(alt, tmp);
160 0                dot = strchr(alt, '.');
161 0                if (dot) *dot = ',';
162 0                back = strtod(alt, NULL);
163             }
164 41            if (back == v) break;
165         }
166 11        if (strchr(tmp, 'e') || strchr(tmp, 'E')) {
167             /* plain decimal: enough fraction digits to keep the significant ones */
168 3            int exp10 = (int)floor(log10(fabs(v)));
169 3            int digits = (p > 17 ? 17 : p) - 1 - exp10;
170 3            if (digits < 0) digits = 0;
171 3            if (digits > 40) digits = 40;
172 3            snprintf(tmp, sizeof tmp, "%.*f", digits, v);
173 3            fix_decimal_point(tmp);
174 3            trim_fraction_zeros(tmp);
175         }
176     }
177 25    fix_decimal_point(tmp);
178 25    if (strcmp(tmp, "-0") == 0) strcpy(tmp, "0");
179 25    if (strlen(tmp) >= cap) return -1;
180 24    strcpy(out, tmp);
181 24    return 0;
182 }
183 
184 206int ygst_field_name_ok(const char *name) {
185 206    if (!name || !name[0]) return 0;
186 1976    for (; *name; name++) {
187 1777        if (*name == '"' || *name == '\r' || *name == '\n') return 0;
188     }
189 199    return 1;
190 }
191 
192 /* ------------------------------------------------------------------ mapping */
193 
194 93void ygst_test_params_init(ygst_test_params *p) {
195 93    memset(p, 0, sizeof *p);
196 93}
197 
198 983static int set_str(ygst_fields *f, const char *name, const char *v) {
199 983    if (!v || !v[0]) return 0;
200 79    return ygst_fields_set(f, name, v);
201 }
202 
203 686static int set_flag(ygst_fields *f, const char *name, int on) {
204 686    return on ? ygst_fields_set(f, name, "1") : 0;
205 }
206 
207 491static int set_num(ygst_fields *f, const char *name, double v) {
208     char tmp[64];
209 491    if (v == 0 || ygst_fmt_double(v, tmp, sizeof tmp) != 0) return 0;
210 12    return ygst_fields_set(f, name, tmp);
211 }
212 
213 3static int set_int(ygst_fields *f, const char *name, long v) {
214     char tmp[32];
215 3    snprintf(tmp, sizeof tmp, "%ld", v);
216 3    return ygst_fields_set(f, name, tmp);
217 }
218 
219 98int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) {
220     static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"};
221 98    int rc = 0;
222 98    rc |= set_str(out, "refText", p->ref_text);
223 98    rc |= set_str(out, "refPinyin", p->ref_pinyin);
224 98    if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group);
225 98    rc |= set_num(out, "scale", p->scale);
226 98    rc |= set_num(out, "precision", p->precision);
227 98    rc |= set_num(out, "slack", p->slack);
228 98    rc |= set_flag(out, "paragraph_need_word_score",
229 98                   p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type));
230 98    rc |= set_flag(out, "phoneme_output", p->phoneme_output);
231 98    rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url);
232 98    if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]);
233 98    rc |= set_str(out, "dict_dialect", p->dict_dialect);
234 98    rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json);
235 98    rc |= set_str(out, "customized_pron", p->customized_pron_json);
236 98    if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis);
237 98    rc |= set_flag(out, "output_rawtext", p->output_rawtext);
238 98    rc |= set_flag(out, "punctuate", p->punctuate);
239 98    rc |= set_num(out, "itn", p->itn);
240 98    rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable);
241     /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */
242 98    rc |= set_flag(out, "vad_detction", p->vad_detection);
243 98    rc |= set_str(out, "keywords", p->keywords);
244 98    rc |= set_str(out, "keypoints", p->keypoints_json);
245 98    rc |= set_num(out, "keypoints_weight", p->keypoints_weight);
246 98    rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json);
247 98    rc |= set_str(out, "negativeReftext", p->negative_reftext);
248 98    if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home");
249 98    if (p->q_type != 0) rc |= set_int(out, "qType", p->q_type);
250     /* customParams in the order of the Android RecordSetting newParams: a name that is already
251      * mapped keeps its position and takes the new value; request comes last */
252 98    if (p->custom_params) {
253         size_t i;
254 70        for (i = 0; i < p->custom_params->count; i++) {
255 10            const ygst_field *cp = &p->custom_params->items[i];
256 10            if (!ygst_field_name_ok(cp->name) || !cp->value || !cp->value[0]) continue;
257 7            rc |= ygst_fields_set(out, cp->name, cp->value);
258         }
259     }
260 98    rc |= set_str(out, "request", p->request_json);
261     /* customParams cannot switch the paragraph word details off */
262 98    rc |= ygst_fields_apply_coretype_rules(out, p->core_type);
263 98    return (rc != 0 || out->oom) ? -1 : 0;
264 }

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 268int ygst_url_encode(ygst_buf *out, const char *s) {
21     static const char H[] = "0123456789ABCDEF";
22 268    const unsigned char *p = (const unsigned char *)(s ? s : "");
23 4421    for (; *p; p++) {
24 4153        unsigned char c = *p;
25 4153        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ||
26 78            c == '_' || c == '~') {
27 4076            ygst_buf_append_char(out, (char)c);
28         } else {
29             char e[3];
30 77            e[0] = '%';
31 77            e[1] = H[c >> 4];
32 77            e[2] = H[c & 15];
33 77            ygst_buf_append(out, e, 3);
34         }
35     }
36 268    ygst_buf_cstr(out);
37 268    return out->oom ? -1 : 0;
38 }
39 
40 71static int starts_ci(const char *s, const char *prefix) {
41 407    while (*prefix) {
42 374        char a = *s;
43 374        if (a >= 'A' && a <= 'Z') a = (char)(a + 32);
44 374        if (a != *prefix) return 0;
45 336        s++;
46 336        prefix++;
47     }
48 33    return 1;
49 }
50 
51 37int 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 37    if (!out || cap == 0) return -1;
57 36    out[0] = 0;
58 36    if (!rest_base) return -1;
59 35    if (starts_ci(rest_base, "https://")) {
60 6        scheme = "wss://";
61 6        rest = rest_base + 8;
62 29    } else if (starts_ci(rest_base, "http://")) {
63 25        scheme = "ws://";
64 25        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 33    n = strlen(rest);
75 37    while (n > 0 && rest[n - 1] == '/') n--;
76 33    if (n == 0 || rest[0] == '/') return -1;
77 31    w = snprintf(out, cap, "%s%.*s", scheme, (int)n, rest);
78 31    if (w < 0 || (size_t)w >= cap) {
79 1        out[0] = 0;
80 1        return -1;
81     }
82 30    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 32int 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 32    int rc = -1;
100 32    if (!out || !core_type || !core_type[0] || ygst_is_blank(app_key) || !secret || !secret[0] || !nonce ||
101 28        !nonce[0] || !ygst_idempotency_key_ok(idempotency_key)) {
102 6        return -1;
103     }
104 26    if (ygst_ws_base_url(rest_base, base, sizeof base) != 0) return -1;
105 25    snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec);
106 25    ygst_fields_init(&q);
107 25    ygst_fields_set(&q, "appKey", app_key);
108 25    ygst_fields_set(&q, "timestamp", ts);
109 25    ygst_fields_set(&q, "nonce", nonce);
110 25    ygst_fields_set(&q, "idempotencyKey", idempotency_key);
111 25    if (extra) {
112 34        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 25    if (q.oom || ygst_sign(&q, secret, sig) != 0) goto done;
119 25    ygst_buf_append_str(out, base);
120 25    ygst_buf_append_char(out, '/');
121 25    ygst_url_encode(out, core_type);
122 132    for (i = 0; i < q.count; i++) {
123 107        ygst_buf_append_char(out, i == 0 ? '?' : '&');
124 107        ygst_url_encode(out, q.items[i].name);
125 107        ygst_buf_append_char(out, '=');
126 107        ygst_url_encode(out, q.items[i].value);
127     }
128 25    ygst_buf_append_str(out, "&signature=");
129 25    ygst_url_encode(out, sig);
130 25    ygst_buf_cstr(out);
131 25    rc = out->oom ? -1 : 0;
132 25done:
133 25    ygst_fields_free(&q);
134 25    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 71static int reserved_frame_name(const char *n, int flags) {
141 70    return strcmp(n, "cmd") == 0 || strcmp(n, "end") == 0 || strcmp(n, "data") == 0 ||
142 206           strcmp(n, "realtime_feedback") == 0 || strcmp(n, "idempotencyKey") == 0 ||
143 65           ((flags & YGST_WSP_TIMING) && strcmp(n, "timing") == 0);
144 }
145 
146 33int ygst_ws_param_frame(const ygst_fields *fields, const char *idempotency_key, int flags, ygst_buf *out) {
147     size_t i;
148 33    int skipped = 0;
149 33    if (!fields || !out || fields->oom || !ygst_idempotency_key_ok(idempotency_key)) return -1;
150 31    ygst_buf_append_char(out, '{');
151 102    for (i = 0; i < fields->count; i++) {
152 71        const ygst_field *f = &fields->items[i];
153 71        if (!f->name || reserved_frame_name(f->name, flags)) {
154 7            skipped++;
155 7            continue;
156         }
157 64        ygst_json_escape(out, f->name);
158 64        ygst_buf_append_char(out, ':');
159 64        ygst_json_escape(out, f->value ? f->value : "");
160 64        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 31    if (flags & YGST_WSP_REALTIME_FEEDBACK) ygst_buf_append_str(out, "\"realtime_feedback\":true,");
164 31    if (flags & YGST_WSP_TIMING) ygst_buf_append_str(out, "\"timing\":1,");
165 31    ygst_buf_append_str(out, "\"idempotencyKey\":");
166 31    ygst_json_escape(out, idempotency_key);
167 31    ygst_buf_append_char(out, '}');
168 31    ygst_buf_cstr(out);
169 31    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 1136static int int_member(ygst_span obj, const char *key, long long *out) {
176     ygst_span v;
177 1136    if (ygst_json_member(obj, key, &v) != 1) return 0;
178 1070    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 1064    return ygst_json_int(v, out) == 0;
194 }
195 
196 8592static int bool_member(ygst_span obj, const char *key) {
197     ygst_span v;
198 8592    int b = 0;
199 8592    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 142static void event_name(ygst_span ev, char *out, size_t cap) {
210     ygst_buf b;
211 142    out[0] = 0;
212 142    if (ygst_json_type(ev) != 's') return;
213 141    ygst_buf_init(&b);
214 141    if (ygst_json_string(ev, &b) == 0 && b.len < cap) memcpy(out, ygst_buf_cstr(&b), b.len + 1);
215 141    ygst_buf_free(&b);
216 }
217 
218 216static void parse_progress(ygst_ws_frame *f) {
219     ygst_span r, list, item;
220     long long v;
221 216    size_t pos = 0;
222 216    int read = 0, matched = 0, total = 0;
223 216    if (ygst_json_member(f->root, "result", &r) != 1 || ygst_json_type(r) != 'o') return;
224 215    f->result = r;
225 215    if (int_member(r, "bytes", &v) && v >= 0) f->bytes = v;
226 215    f->read_units = int_member(r, "readUnits", &v) ? count_value(v) : -1;
227 215    f->matched_units = int_member(r, "matchedUnits", &v) ? count_value(v) : -1;
228 215    f->total_units = int_member(r, "totalUnits", &v) ? count_value(v) : -1;
229     /* every unit with its state */
230 215    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 215    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 30static void parse_final(ygst_ws_frame *f) {
252     ygst_span r;
253 30    if (ygst_json_member(f->root, "result", &r) == 1) f->result = r;
254 30    f->replayed = bool_member(f->root, "replayed");
255 30}
256 
257 392int ygst_ws_frame_parse(const char *text, size_t n, ygst_ws_frame *f) {
258     ygst_span root, v;
259 392    if (!f) return -1;
260 391    memset(f, 0, sizeof *f);
261 391    f->kind = YGST_WSF_INVALID;
262 391    f->bytes = -1;
263 391    f->read_units = -1;
264 391    f->matched_units = -1;
265 391    f->total_units = -1;
266 391    if (!text || ygst_json_parse(text, n, &root) != 0 || ygst_json_type(root) != 'o') return -1;
267 387    f->root = root;
268 387    if (ygst_json_member(root, "event", &v) == 1) {
269         char ev[16];
270 142        event_name(v, ev, sizeof ev);
271 142        if (strcmp(ev, "connected") == 0) {
272 59            f->kind = YGST_WSF_CONNECTED;
273 83        } else if (strcmp(ev, "started") == 0) {
274 59            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 142        return 0;
288     }
289 245    if (ygst_json_member(root, "eof", &v) == 1) {
290 242        long long eof = -1;
291 242        if (int_member(root, "eof", &eof) && eof == 0) {
292 216            f->kind = YGST_WSF_PROGRESS;
293 216            parse_progress(f);
294         } else {
295 26            f->kind = YGST_WSF_FINAL;
296 26            parse_final(f);
297         }
298 242        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 48void ygst_words_dedupe_init(ygst_words_dedupe *d) {
351 48    memset(d, 0, sizeof *d);
352 48}
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 46void ygst_rt_init(ygst_rt *rt) {
369 46    memset(rt, 0, sizeof *rt);
370 46    ygst_retry_policy_default(&rt->policy);
371 46    rt->connect_timeout_ms = 20000;
372 46    rt->attempt_timeout_ms = 60000;
373 46    rt->ping_interval_ms = 15000;
374 46    rt->pong_timeout_ms = 30000;
375 46    rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
376 46    rt->state = YGST_RT_IDLE;
377 46    rt->frame.kind = YGST_WSF_INVALID;
378 46    ygst_words_dedupe_init(&rt->dedupe);
379 46    ygst_classification_init(&rt->cls);
380 46    ygst_buf_init(&rt->failure);
381 46}
382 
383 46void ygst_rt_free(ygst_rt *rt) {
384 46    ygst_classification_free(&rt->cls);
385 46    ygst_buf_free(&rt->failure);
386 46}
387 
388 1452static ygst_rt_actions no_actions(void) {
389     ygst_rt_actions a;
390 1452    memset(&a, 0, sizeof a);
391 1452    return a;
392 }
393 
394 946static int on_connection(ygst_rt_state s) {
395 946    return s == YGST_RT_CONNECTING || s == YGST_RT_STARTING || s == YGST_RT_STREAMING || s == YGST_RT_ENDING;
396 }
397 
398 190static void clear_timers(ygst_rt *rt) {
399 190    rt->deadline_ms = 0;
400 190    rt->next_ping_ms = 0;
401 190    rt->pong_deadline_ms = 0;
402 190}
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 60static void open_connection(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
411 60    a->connect = 1;
412 60    rt->state = YGST_RT_CONNECTING;
413 60    rt->connections++;
414 60    rt->sent = 0;
415 60    clear_timers(rt);
416 60    rt->deadline_ms = now + rt->connect_timeout_ms;
417 60}
418 
419 274static void send_pending_audio(ygst_rt *rt, ygst_rt_actions *a) {
420 274    if (rt->total > rt->sent) {
421 247        a->audio_from = rt->sent;
422 247        a->audio_to = rt->total;
423 247        rt->sent = rt->total;
424     }
425 274}
426 
427 32static void start_streaming(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
428 32    rt->state = YGST_RT_STREAMING;
429 32    clear_timers(rt);
430 32    if (rt->ping_interval_ms > 0) rt->next_ping_ms = now + rt->ping_interval_ms;
431 32    send_pending_audio(rt, a);
432 32}
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 43static void send_end(ygst_rt *rt, ygst_rt_actions *a, int64_t now) {
437 43    send_pending_audio(rt, a);
438 43    a->send_end = 1;
439 43    if (!rt->end_sent) {
440 31        rt->end_sent = 1;
441 31        ygst_call_init(&rt->call, &rt->policy, rt->seed, now);
442     }
443 43    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 43    clear_timers(rt);
447 43    rt->deadline_ms = now + rt->attempt_timeout_ms;
448 43}
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 46ygst_rt_actions ygst_rt_start(ygst_rt *rt, int64_t now_ms) {
500 46    ygst_rt_actions a = no_actions();
501 46    if (rt->state != YGST_RT_IDLE) return a;
502 45    if (rt->max_frame_bytes == 0) rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES;
503 45    open_connection(rt, &a, now_ms);
504 45    return a;
505 }
506 
507 356ygst_rt_actions ygst_rt_on_audio(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
508 356    ygst_rt_actions a = no_actions();
509     (void)now_ms;
510 356    if (rt->end_requested) return a; /* the round audio is complete */
511 355    if (total_bytes > rt->total) rt->total = total_bytes;
512 355    if (rt->state == YGST_RT_STREAMING) send_pending_audio(rt, &a);
513 355    return a;
514 }
515 
516 40ygst_rt_actions ygst_rt_on_stop(ygst_rt *rt, size_t total_bytes, int64_t now_ms) {
517 40    ygst_rt_actions a = no_actions();
518 40    if (rt->end_requested || rt->state == YGST_RT_DONE) return a;
519 38    if (total_bytes > rt->total) rt->total = total_bytes;
520 38    rt->end_requested = 1;
521 38    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 24    case YGST_RT_STREAMING:
528 24        send_end(rt, &a, now_ms);
529 24        break;
530 8    default:
531 8        break; /* CONNECTING, STARTING: end goes out right after "started" */
532     }
533 38    return a;
534 }
535 
536 263ygst_rt_actions ygst_rt_on_text(ygst_rt *rt, const char *text, size_t n, int64_t now_ms) {
537 263    ygst_rt_actions a = no_actions();
538 263    if (!on_connection(rt->state)) return a;
539 259    if (ygst_ws_frame_parse(text, n, &rt->frame) != 0) return a;
540 258    switch (rt->frame.kind) {
541 53    case YGST_WSF_CONNECTED:
542 53        if (rt->state == YGST_RT_CONNECTING) {
543 52            a.send_params = 1;
544 52            rt->state = YGST_RT_STARTING;
545         }
546 53        break;
547 52    case YGST_WSF_STARTED:
548 52        if (rt->state == YGST_RT_STARTING) {
549 51            if (rt->end_sent || rt->end_requested) {
550 19                send_end(rt, &a, now_ms);
551             } else {
552 32                start_streaming(rt, &a, now_ms);
553             }
554         }
555 52        break;
556 123    case YGST_WSF_PROGRESS:
557         /* delivered only with new words; empty words and bytes-only frames are not */
558 123        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 123        break;
568 19    case YGST_WSF_FINAL:
569 19        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 18            memset(&o, 0, sizeof o);
576 18            o.http_status = 200;
577 18            o.body = text;
578 18            o.body_len = n;
579 18            o.retry_after_ms = -1;
580 18            o.replayed = rt->frame.replayed;
581 18            ygst_classify(&o, &rt->cls);
582         }
583 18        if (rt->cls.success) {
584 17            (void)ygst_call_next(&rt->call, &rt->cls, -1, now_ms);
585 17            rt->state = YGST_RT_DONE;
586 17            clear_timers(rt);
587 17            a.close = 1;
588 17            a.final = 1;
589         } else {
590 1            attempt_failed(rt, &a, now_ms);
591         }
592 18        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 258    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 724ygst_rt_actions ygst_rt_on_tick(ygst_rt *rt, int64_t now_ms) {
625 724    ygst_rt_actions a = no_actions();
626     char msg[96];
627 724    if (rt->state == YGST_RT_BACKOFF) {
628 73        if (now_ms >= rt->reconnect_at_ms) open_connection(rt, &a, now_ms);
629 73        return a;
630     }
631 651    if (!on_connection(rt->state)) return a;
632 486    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 482    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 480    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 480    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 35const char *ygst_compat_message(int err_id) {
39     size_t i;
40 165    for (i = 0; i < ygst_table_compat_count; i++) {
41 161        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 89void ygst_rng_seed(ygst_rng *r, uint64_t seed) {
57 89    r->s = seed;
58 89}
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 116void ygst_retry_policy_default(ygst_retry_policy *p) {
74 116    memset(p, 0, sizeof *p);
75 116    p->max_retries = 2;
76 116    p->initial_delay_ms = 200;
77 116    p->multiplier = 2.0;
78 116    p->max_delay_ms = 4000;
79 116    p->jitter = 0.3;
80 116    p->respect_retry_after = 1;
81 116    p->max_retry_after_ms = 30000;
82 116    p->total_timeout_ms = 300000;
83 116    p->auto_retry = 0;
84 116    p->max_auto_retries = 2;
85 116    p->err_ids[0] = YGST_ERRID_RETRYABLE;
86 116    p->n_err_ids = 1;
87 116}
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 213void ygst_classification_init(ygst_classification *c) {
214 213    memset(c, 0, sizeof *c);
215 213    ygst_buf_init(&c->record_id);
216 213    ygst_buf_init(&c->audio_url);
217 213    ygst_buf_init(&c->message);
218 213}
219 
220 213void ygst_classification_free(ygst_classification *c) {
221 213    ygst_buf_free(&c->record_id);
222 213    ygst_buf_free(&c->audio_url);
223 213    ygst_buf_free(&c->message);
224 213}
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 233int 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 233    int parsed = 0;
287 233    long long code = 0;
288 233    c->success = 0;
289 233    c->http_status = o->http_status;
290 233    c->local_code = o->local_code;
291 233    c->biz_code = 0;
292 233    c->has_audio_url = 0;
293 233    c->has_record_id = 0;
294 233    c->record_id_raw.p = NULL;
295 233    c->record_id_raw.n = 0;
296 233    c->root.p = NULL;
297 233    c->root.n = 0;
298 233    c->result.p = NULL;
299 233    c->result.n = 0;
300 233    ygst_buf_reset(&c->record_id);
301 233    ygst_buf_reset(&c->audio_url);
302 233    ygst_buf_reset(&c->message);
303 
304 233    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 190    if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o';
316 
317 190    if (o->http_status >= 200 && o->http_status < 300) {
318         ygst_span result, v;
319 76        if (parsed && ygst_json_member(root, "result", &result) == 1) {
320             ygst_span inner;
321 73            c->success = 1;
322 73            c->root = root;
323 73            c->result = result;
324 73            if (ygst_json_member(root, "recordId", &v) == 1) {
325 70                c->has_record_id = 1;
326 70                c->record_id_raw = v;
327 70                if (ygst_json_type(v) == 's') {
328 69                    ygst_json_string(v, &c->record_id);
329                 } else {
330 1                    ygst_buf_append(&c->record_id, v.p, v.n);
331                 }
332             }
333 73            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 70            } 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 73            if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0;
340 73            ygst_buf_cstr(&c->record_id);
341 73            ygst_buf_cstr(&c->audio_url);
342 73            c->retryable = 0;
343 73            c->err_id = 0;
344 73            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 85void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) {
436 85    memset(c, 0, sizeof *c);
437 85    if (p) {
438 84        c->policy = *p;
439     } else {
440 1        ygst_retry_policy_default(&c->policy);
441     }
442 85    ygst_rng_seed(&c->rng, seed);
443 85    c->start_ms = now_ms;
444 85}
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 136ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) {
455     ygst_action a;
456 136    int64_t deadline = c->start_ms + c->policy.total_timeout_ms;
457 136    a.kind = YGST_ACT_FAIL;
458 136    a.delay_ms = 0;
459 136    a.err_id = cls->err_id;
460 136    a.auto_retry = 0;
461 136    c->attempt++;
462 136    c->total_attempts++;
463 136    if (cls->success) {
464 54        a.kind = YGST_ACT_SUCCESS;
465 54        a.err_id = 0;
466 54        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 77void ygst_timing_init(ygst_timing *t) {
16 77    if (!t) return;
17 76    memset(t, 0, sizeof *t);
18 76    t->mode = YGST_TM_NONE;
19 76    t->upload_ms = -1;
20 76    t->wait_ms = -1;
21 76    t->download_ms = -1;
22 76    t->audio_bytes = -1;
23 76    t->upload_bytes = -1;
24 76    t->queued_at_stop = -1;
25 76    t->wire_bytes = -1;
26 76    t->body_bytes = -1;
27 76    t->server_total_ms = -1;
28 76    t->server_engine_ms = -1;
29 76    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 56int ygst_timing_member(ygst_span root, ygst_timing *t, ygst_span *timing) {
149     ygst_span v;
150 56    if (timing) {
151 55        timing->p = NULL;
152 55        timing->n = 0;
153     }
154 56    if (!root.p || ygst_json_type(root) != 'o') return -1;
155 54    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 303static long long between(int64_t from, int64_t to) {
188 303    if (!from || !to || to < from) return -1;
189 277    return (long long)(to - from);
190 }
191 
192 419static void put_ms(ygst_buf *b, const char *key, long long ms) {
193 419    ygst_buf_append_char(b, ' ');
194 419    ygst_buf_append_str(b, key);
195 419    ygst_buf_append_char(b, '=');
196 419    if (ms < 0) {
197 30        ygst_buf_append_char(b, '-');
198 30        return;
199     }
200 389    ygst_buf_append_int(b, ms);
201 389    ygst_buf_append_str(b, "ms");
202 }
203 
204 189static void put_bytes(ygst_buf *b, const char *key, long long n) {
205 189    ygst_buf_append_char(b, ' ');
206 189    ygst_buf_append_str(b, key);
207 189    ygst_buf_append_char(b, '=');
208 189    if (n < 0) {
209 11        ygst_buf_append_char(b, '-');
210 11        return;
211     }
212 178    ygst_buf_append_int(b, n);
213 178    ygst_buf_append_char(b, 'B');
214 }
215 
216 63static const char *mode_name(ygst_tm_mode m) {
217 63    switch (m) {
218 40    case YGST_TM_REST:
219 40        return "rest";
220 17    case YGST_TM_WS:
221 17        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 63static void put_audio(ygst_buf *b, const ygst_timing *t) {
230     char s[48];
231 124    if (t->audio_bytes >= 0 && t->audio_bytes_per_sec > 0) {
232 61        long long bps = t->audio_bytes_per_sec;
233 61        long long hundredths = (t->audio_bytes * 100 + bps / 2) / bps;
234 61        snprintf(s, sizeof s, " audio=%lld.%02llds", hundredths / 100, hundredths % 100);
235 61        ygst_buf_append_str(b, s);
236     } else {
237 2        ygst_buf_append_str(b, " audio=-");
238     }
239 63}
240 
241 124static void put_token(ygst_buf *b, const char *token_id) {
242 124    ygst_buf_append_str(b, "timing tokenId=");
243 124    ygst_buf_append_str(b, token_id ? token_id : "");
244 124}
245 
246 65int 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 65    if (!t || !out) return -1;
251 63    ws = t->mode == YGST_TM_WS;
252 63    if (ws) {
253 17        int64_t first = t->ws_final_first_ms;
254 17        done = t->ws_final_ms;
255 17        upload = between(t->stop_ms, t->ws_end_ms);
256 17        wait = between(t->ws_end_ms, first ? first : done);
257 17        download = between(first, done);
258     } else {
259         /* the split of the last attempt from the transport, placed after its start */
260 46        int64_t body_sent = t->rest_start_ms && t->upload_ms >= 0 ? t->rest_start_ms + t->upload_ms : 0;
261 46        done = t->rest_done_ms;
262 46        upload = between(t->stop_ms, body_sent);
263 46        wait = t->wait_ms;
264 46        download = t->download_ms;
265     }
266 63    put_token(out, token_id);
267 63    ygst_buf_append_str(out, " mode=");
268 63    ygst_buf_append_str(out, mode_name(t->mode));
269 63    put_audio(out, t);
270 63    put_ms(out, "afterStop", between(t->stop_ms, t->delivered_ms));
271 63    put_ms(out, "upload", upload);
272 63    put_ms(out, "wait", wait);
273 63    put_ms(out, "download", download);
274 63    put_ms(out, "parse", between(done, t->parsed_ms));
275 63    put_ms(out, "deliver", between(t->parsed_ms, t->delivered_ms));
276 63    if (ws) {
277 17        put_ms(out, "handshake", between(t->ws_connect_ms, t->ws_handshake_ms));
278 17        put_bytes(out, "queued", t->queued_at_stop);
279 17        put_bytes(out, "final", t->body_bytes);
280 17        put_bytes(out, "wire", t->wire_bytes);
281 17        if (t->deflate) ygst_buf_append_str(out, " deflate");
282 17        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 46        ygst_buf_append_str(out, " attempts=");
288 46        ygst_buf_append_int(out, t->attempts);
289 46        put_bytes(out, "sent", t->upload_bytes);
290 46        put_bytes(out, "body", t->body_bytes);
291 46        put_bytes(out, "wire", t->wire_bytes);
292 46        if (t->encoding[0]) {
293 1            ygst_buf_append_char(out, ' ');
294 1            ygst_buf_append_str(out, t->encoding);
295         }
296     }
297 63    if (t->server_total_ms >= 0) put_ms(out, "server.total", t->server_total_ms);
298 63    if (t->server_engine_ms >= 0) put_ms(out, "server.engine", t->server_engine_ms);
299 63    if (t->server_platform_ms >= 0) put_ms(out, "server.platform", t->server_platform_ms);
300 63    if (t->replayed) ygst_buf_append_str(out, " replayed");
301 63    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 63    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 63    ygst_buf_cstr(out);
311 63    return out->oom ? -1 : 0;
312 }
313 
314 59int 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 59    int64_t origin = 0;
320     size_t i;
321 59    if (!t || !out) return -1;
322 58    at[0] = t->start_ms;
323 58    at[1] = t->ws_connect_ms;
324 58    at[2] = t->ws_handshake_ms;
325 58    at[3] = t->ws_params_ms;
326 58    at[4] = t->ws_first_audio_ms;
327 58    at[5] = t->stop_ms;
328 58    at[6] = t->ws_end_ms;
329 58    at[7] = t->ws_final_first_ms;
330 58    at[8] = t->ws_final_ms;
331 58    at[9] = t->rest_start_ms;
332 58    at[10] = t->rest_done_ms;
333 58    at[11] = t->parsed_ms;
334 58    at[12] = t->delivered_ms;
335 812    for (i = 0; i < sizeof names / sizeof names[0]; i++) {
336 754        if (at[i] && (!origin || at[i] < origin)) origin = at[i];
337     }
338 58    put_token(out, token_id);
339 58    ygst_buf_append_str(out, " phases");
340 812    for (i = 0; i < sizeof names / sizeof names[0]; i++) {
341 754        if (!at[i]) continue;
342 415        ygst_buf_append_char(out, ' ');
343 415        ygst_buf_append_str(out, names[i]);
344 415        ygst_buf_append_str(out, "=+");
345 415        ygst_buf_append_int(out, (long long)(at[i] - origin));
346 415        ygst_buf_append_str(out, "ms");
347     }
348 58    ygst_buf_cstr(out);
349 58    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 60int 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 60    if (!out) return -1;
367 59    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 }