Total 98.90% (2438 of 2465 lines)
| file | covered | lines | percent |
|---|---|---|---|
| Sources/STKouyuEngine/core/ygst_audio.c | 167 | 168 | 99.40% |
| Sources/STKouyuEngine/core/ygst_buf.c | 91 | 97 | 93.81% |
| Sources/STKouyuEngine/core/ygst_crypto.c | 140 | 140 | 100.00% |
| Sources/STKouyuEngine/core/ygst_envelope.c | 251 | 251 | 100.00% |
| Sources/STKouyuEngine/core/ygst_errors_table.c | 40 | 40 | 100.00% |
| Sources/STKouyuEngine/core/ygst_fields.c | 141 | 149 | 94.63% |
| Sources/STKouyuEngine/core/ygst_json.c | 340 | 348 | 97.70% |
| Sources/STKouyuEngine/core/ygst_multipart.c | 69 | 69 | 100.00% |
| Sources/STKouyuEngine/core/ygst_params.c | 133 | 137 | 97.08% |
| Sources/STKouyuEngine/core/ygst_realtime.c | 463 | 463 | 100.00% |
| Sources/STKouyuEngine/core/ygst_retry.c | 327 | 327 | 100.00% |
| Sources/STKouyuEngine/core/ygst_timing.c | 276 | 276 | 100.00% |
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * WAV header writer and reader, local audio checks, a frame energy VAD with an adaptive noise 6 * floor, the 0..100 sound intensity scale and the countdown values of the tick callback. 7 */ 8 #include "ygst_core.h" 9 10 #include <math.h> 11 #include <string.h> 12 13 80static void put_le32(unsigned char *p, uint32_t v) { 14 80 p[0] = (unsigned char)v; 15 80 p[1] = (unsigned char)(v >> 8); 16 80 p[2] = (unsigned char)(v >> 16); 17 80 p[3] = (unsigned char)(v >> 24); 18 80} 19 20 64static void put_le16(unsigned char *p, uint16_t v) { 21 64 p[0] = (unsigned char)v; 22 64 p[1] = (unsigned char)(v >> 8); 23 64} 24 25 445static uint32_t le32(const unsigned char *p) { 26 445 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); 27 } 28 29 339static uint16_t le16(const unsigned char *p) { 30 339 return (uint16_t)(p[0] | (p[1] << 8)); 31 } 32 33 16void ygst_wav_header(unsigned char out[44], uint32_t data_len, int sample_rate, int channels, int bits) { 34 16 uint32_t block = (uint32_t)(channels * (bits / 8)); 35 16 memcpy(out, "RIFF", 4); 36 16 put_le32(out + 4, data_len > 0xFFFFFFFFu - 36u ? 0xFFFFFFFFu : data_len + 36u); 37 16 memcpy(out + 8, "WAVE", 4); 38 16 memcpy(out + 12, "fmt ", 4); 39 16 put_le32(out + 16, 16); 40 16 put_le16(out + 20, 1); 41 16 put_le16(out + 22, (uint16_t)channels); 42 16 put_le32(out + 24, (uint32_t)sample_rate); 43 16 put_le32(out + 28, (uint32_t)sample_rate * block); 44 16 put_le16(out + 32, (uint16_t)block); 45 16 put_le16(out + 34, (uint16_t)bits); 46 16 memcpy(out + 36, "data", 4); 47 16 put_le32(out + 40, data_len); 48 16} 49 50 122int ygst_is_riff_wave(const unsigned char *p, size_t n) { 51 122 return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0; 52 } 53 54 120int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) { 55 120 size_t pos = 12; 56 120 int have_fmt = 0; 57 120 memset(info, 0, sizeof *info); 58 120 if (!ygst_is_riff_wave(p, n)) return -1; 59 332 while (pos + 8 <= n) { 60 332 uint32_t size = le32(p + pos + 4); 61 332 const unsigned char *id = p + pos; 62 332 size_t body = pos + 8; 63 332 if (memcmp(id, "fmt ", 4) == 0) { 64 114 if (size < 16 || body + 16 > n) return -1; 65 113 info->format = le16(p + body); 66 113 info->channels = le16(p + body + 2); 67 113 info->sample_rate = (int)le32(p + body + 4); 68 113 info->bits = le16(p + body + 14); 69 113 have_fmt = 1; 70 218 } else if (memcmp(id, "data", 4) == 0) { 71 114 if (!have_fmt) return -1; 72 113 info->data_offset = body; 73 /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */ 74 113 info->data_len = (size == 0 || size > n - body) ? n - body : size; 75 113 return 0; 76 } 77 217 if (size > n - body) return -1; 78 216 pos = body + size + (size & 1u); 79 } 80 0 return -1; 81 } 82 83 79long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) { 84 double per_second; 85 79 if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1; 86 77 per_second = (double)sample_rate * channels * (bits / 8.0); 87 77 if (per_second <= 0) return -1; 88 77 return (long)((double)bytes * 1000.0 / per_second); 89 } 90 91 /* Exact duration test without rounding: bytes * 1000 compared with ms * bytes per second. */ 92 67static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) { 93 67 double per_second = (double)sample_rate * channels * (bits / 8.0); 94 67 if (per_second <= 0) return 0; 95 66 return (double)bytes * 1000.0 > (double)ms * per_second; 96 } 97 98 27int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) { 99 long ms; 100 27 if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY; 101 26 if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE; 102 25 ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits); 103 25 if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT; 104 23 if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE; 105 21 return 0; 106 } 107 108 53int ygst_audio_check_upload(const unsigned char *data, size_t len) { 109 ygst_wav_info info; 110 53 if (!data || len == 0) return YGST_ERRID_AUDIO_EMPTY; 111 51 if (len > YGST_MAX_AUDIO_BYTES) return YGST_ERRID_AUDIO_TOO_LARGE; 112 50 if (ygst_wav_parse(data, len, &info) == 0) { 113 long ms; 114 48 if (info.data_len == 0) return YGST_ERRID_AUDIO_EMPTY; 115 46 if (info.format == 1 || info.format == 3 || info.format == 0xFFFE) { 116 46 ms = ygst_pcm_duration_ms(info.data_len, info.sample_rate, info.channels, info.bits); 117 46 if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT; 118 44 if (pcm_longer_than(info.data_len, info.sample_rate, info.channels, info.bits, YGST_MAX_AUDIO_MS)) 119 1 return YGST_ERRID_AUDIO_TOO_LARGE; 120 } 121 } 122 45 return 0; 123 } 124 125 /* ------------------------------------------------------------------ VAD */ 126 127 #define VAD_ABS_DB (-42.0) /* a frame quieter than this is never speech */ 128 #define VAD_OVER_NOISE_DB 10.0 /* speech must also exceed the noise floor by this much */ 129 #define VAD_START_FRAMES 3 /* 30 ms of speech starts an utterance */ 130 #define VAD_NOISE_MIN_DB (-90.0) 131 #define VAD_NOISE_MAX_DB (-35.0) 132 133 10void ygst_vad_init(ygst_vad *v, int sample_rate, double seek_10ms, double ref_length_10ms) { 134 10 memset(v, 0, sizeof *v); 135 10 v->sample_rate = sample_rate > 0 ? sample_rate : 16000; 136 10 v->frame_samples = v->sample_rate / 100; 137 10 if (v->frame_samples <= 0) v->frame_samples = 1; 138 10 v->seek_ms = (int)((seek_10ms > 0 ? seek_10ms : 60) * 10.0); 139 10 v->ref_length_ms = ref_length_10ms > 0 ? (int)(ref_length_10ms * 10.0) : 0; 140 10 v->noise_db = -60.0; 141 10} 142 143 1341int ygst_sound_intensity_db(double db) { 144 1341 double x = (db + 60.0) * 100.0 / 60.0; 145 1341 if (x < 0) x = 0; 146 1341 if (x > 100) x = 100; 147 1341 return (int)(x + 0.5); 148 } 149 150 1338static double rms_db(double sumsq, int n) { 151 double rms; 152 1338 if (n <= 0) return -120.0; 153 1337 rms = sqrt(sumsq / n); 154 1337 if (rms < 1.0) return -120.0; 155 651 return 20.0 * log10(rms / 32768.0); 156 } 157 158 3int ygst_sound_intensity(const int16_t *pcm, size_t samples) { 159 3 double sumsq = 0; 160 size_t i; 161 3203 for (i = 0; i < samples; i++) sumsq += (double)pcm[i] * pcm[i]; 162 3 return ygst_sound_intensity_db(rms_db(sumsq, (int)samples)); 163 } 164 165 1335static void vad_frame(ygst_vad *v, double db) { 166 1335 double threshold = v->noise_db + VAD_OVER_NOISE_DB; 167 int speech; 168 1335 if (threshold < VAD_ABS_DB) threshold = VAD_ABS_DB; 169 1335 speech = db > threshold; 170 1335 v->intensity = ygst_sound_intensity_db(db); 171 1335 if (!speech) { 172 927 v->noise_db = v->noise_db * 0.95 + db * 0.05; 173 927 if (v->noise_db < VAD_NOISE_MIN_DB) v->noise_db = VAD_NOISE_MIN_DB; 174 927 if (v->noise_db > VAD_NOISE_MAX_DB) v->noise_db = VAD_NOISE_MAX_DB; 175 } 176 1335 v->speech_run = speech ? v->speech_run + 1 : 0; 177 1335 if (v->status != 1) { 178 644 if (v->speech_run >= VAD_START_FRAMES) { 179 7 v->status = 1; 180 7 v->speaking_ms = 0; 181 7 v->silence_ms = 0; 182 } 183 644 return; 184 } 185 691 v->speaking_ms += 10; 186 691 v->silence_ms = speech ? 0 : v->silence_ms + 10; 187 691 if (v->silence_ms >= v->seek_ms && v->speaking_ms >= v->ref_length_ms) { 188 4 v->status = 2; 189 4 v->ended_edge = 1; 190 } 191 } 192 193 757int ygst_vad_process(ygst_vad *v, const int16_t *pcm, size_t samples) { 194 size_t i; 195 757 v->ended_edge = 0; 196 214357 for (i = 0; i < samples; i++) { 197 213600 v->acc_sumsq += (double)pcm[i] * pcm[i]; 198 213600 v->acc_n++; 199 213600 if (v->acc_n >= v->frame_samples) { 200 1335 vad_frame(v, rms_db(v->acc_sumsq, v->acc_n)); 201 1335 v->acc_sumsq = 0; 202 1335 v->acc_n = 0; 203 } 204 } 205 757 return v->status; 206 } 207 208 131int ygst_vad_process_bytes(ygst_vad *v, const unsigned char *bytes, size_t n) { 209 int16_t chunk[256]; 210 131 size_t used = 0; 211 131 int ended = 0; 212 131 size_t i = 0; 213 131 if (v->has_carry && n > 0) { 214 20 chunk[used++] = (int16_t)(uint16_t)(v->carry | (bytes[0] << 8)); 215 20 v->has_carry = 0; 216 20 i = 1; 217 } 218 150831 for (; i + 1 < n; i += 2) { 219 150700 chunk[used++] = (int16_t)(uint16_t)(bytes[i] | (bytes[i + 1] << 8)); 220 150700 if (used == sizeof chunk / sizeof chunk[0]) { 221 542 ygst_vad_process(v, chunk, used); 222 542 ended |= v->ended_edge; 223 542 used = 0; 224 } 225 } 226 131 if (i < n) { 227 20 v->carry = bytes[i]; 228 20 v->has_carry = 1; 229 } 230 131 if (used) { 231 130 ygst_vad_process(v, chunk, used); 232 130 ended |= v->ended_edge; 233 } 234 131 v->ended_edge = ended; 235 131 return v->status; 236 } 237 238 4void ygst_tick_compute(double duration_ms, double elapsed_ms, double *millis_left, double *percent_left) { 239 double left; 240 4 if (duration_ms <= 0) { 241 1 *millis_left = 0; 242 1 *percent_left = 0; 243 1 return; 244 } 245 3 left = duration_ms - (elapsed_ms > 0 ? elapsed_ms : 0); 246 3 if (left < 0) left = 0; 247 3 *millis_left = left; 248 3 *percent_left = left * 100.0 / duration_ms; 249 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Growable byte buffer and small string helpers. 6 */ 7 #include "ygst_core.h" 8 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 13 5276void ygst_buf_init(ygst_buf *b) { 14 5276 b->data = NULL; 15 5276 b->len = 0; 16 5276 b->cap = 0; 17 5276 b->oom = 0; 18 5276} 19 20 2638void ygst_buf_free(ygst_buf *b) { 21 2638 free(b->data); 22 2638 ygst_buf_init(b); 23 2638} 24 25 4049void ygst_buf_reset(ygst_buf *b) { 26 4049 b->len = 0; 27 4049 b->oom = 0; 28 4049 if (b->data) b->data[0] = 0; 29 4049} 30 31 29696int ygst_buf_reserve(ygst_buf *b, size_t extra) { 32 size_t need; 33 size_t cap; 34 unsigned char *p; 35 29696 if (b->oom) return -1; 36 29696 if (extra > (size_t)-1 - b->len - 1) { 37 0 b->oom = 1; 38 0 return -1; 39 } 40 29696 need = b->len + extra + 1; /* room for the terminating NUL */ 41 29696 if (need <= b->cap) return 0; 42 3094 cap = b->cap ? b->cap : 64; 43 6397 while (cap < need) { 44 3303 if (cap > ((size_t)-1) / 2) { 45 0 cap = need; 46 0 break; 47 } 48 3303 cap *= 2; 49 } 50 3094 p = (unsigned char *)realloc(b->data, cap); 51 3094 if (!p) { 52 0 b->oom = 1; 53 0 return -1; 54 } 55 3094 b->data = p; 56 3094 b->cap = cap; 57 3094 return 0; 58 } 59 60 29569int ygst_buf_append(ygst_buf *b, const void *p, size_t n) { 61 29569 if (n == 0) return b->oom ? -1 : 0; 62 29556 if (ygst_buf_reserve(b, n) != 0) return -1; 63 29556 memcpy(b->data + b->len, p, n); 64 29556 b->len += n; 65 29556 b->data[b->len] = 0; 66 29556 return 0; 67 } 68 69 6013int ygst_buf_append_str(ygst_buf *b, const char *s) { 70 6013 return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0); 71 } 72 73 14485int ygst_buf_append_char(ygst_buf *b, char c) { 74 14485 return ygst_buf_append(b, &c, 1); 75 } 76 77 1095int ygst_buf_append_int(ygst_buf *b, long long v) { 78 char tmp[32]; 79 1095 int n = snprintf(tmp, sizeof tmp, "%lld", v); 80 1095 if (n < 0) return -1; 81 1095 return ygst_buf_append(b, tmp, (size_t)n); 82 } 83 84 9872const char *ygst_buf_cstr(ygst_buf *b) { 85 9872 if (!b->data) { 86 140 if (ygst_buf_reserve(b, 0) != 0) return ""; 87 140 b->data[0] = 0; 88 } 89 9872 return (const char *)b->data; 90 } 91 92 1092char *ygst_strdup(const char *s) { 93 size_t n; 94 char *p; 95 1092 if (!s) return NULL; 96 1091 n = strlen(s); 97 1091 p = (char *)malloc(n + 1); 98 1091 if (!p) return NULL; 99 1091 memcpy(p, s, n + 1); 100 1091 return p; 101 } 102 103 990static int lower_ascii(int c) { 104 990 return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; 105 } 106 107 242int ygst_strcaseeq(const char *a, const char *b) { 108 242 if (!a || !b) return 0; 109 613 while (*a && *b) { 110 495 if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0; 111 372 a++; 112 372 b++; 113 } 114 118 return *a == 0 && *b == 0; 115 } 116 117 11int ygst_has_suffix_ci(const char *s, const char *suffix) { 118 size_t n; 119 size_t m; 120 11 if (!s || !suffix) return 0; 121 10 n = strlen(s); 122 10 m = strlen(suffix); 123 10 if (m > n) return 0; 124 9 return ygst_strcaseeq(s + (n - m), suffix); 125 } 126 127 564static int is_space(unsigned char c) { 128 564 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; 129 } 130 131 164size_t ygst_trim_copy(const char *s, char *out, size_t cap) { 132 const char *e; 133 size_t n; 134 164 if (!out || cap == 0) return 0; 135 164 out[0] = 0; 136 164 if (!s) return 0; 137 169 while (*s && is_space((unsigned char)*s)) s++; 138 162 e = s + strlen(s); 139 167 while (e > s && is_space((unsigned char)e[-1])) e--; 140 162 n = (size_t)(e - s); 141 162 if (n >= cap) n = cap - 1; 142 162 memcpy(out, s, n); 143 162 out[n] = 0; 144 162 return n; 145 } 146 147 242int ygst_is_blank(const char *s) { 148 242 if (!s) return 1; 149 244 while (*s) { 150 232 if (!is_space((unsigned char)*s)) return 0; 151 11 s++; 152 } 153 12 return 1; 154 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 2104), Base64 (RFC 4648). Written from the 6 * specifications so that the core has no dependency on CommonCrypto or OpenSSL. 7 */ 8 #include "ygst_core.h" 9 10 #include <string.h> 11 12 static const uint32_t K[64] = { 13 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 14 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, 15 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 16 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 17 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 18 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, 19 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, 20 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u}; 21 22 #define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) 23 24 16097static void sha256_block(ygst_sha256_ctx *c, const unsigned char *p) { 25 uint32_t w[64]; 26 uint32_t a, b, cc, d, e, f, g, h, t1, t2; 27 int i; 28 273649 for (i = 0; i < 16; i++) { 29 257552 w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) | 30 257552 (uint32_t)p[i * 4 + 3]; 31 } 32 788753 for (i = 16; i < 64; i++) { 33 772656 uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3); 34 772656 uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10); 35 772656 w[i] = w[i - 16] + s0 + w[i - 7] + s1; 36 } 37 16097 a = c->h[0]; 38 16097 b = c->h[1]; 39 16097 cc = c->h[2]; 40 16097 d = c->h[3]; 41 16097 e = c->h[4]; 42 16097 f = c->h[5]; 43 16097 g = c->h[6]; 44 16097 h = c->h[7]; 45 1046305 for (i = 0; i < 64; i++) { 46 1030208 uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25); 47 1030208 uint32_t ch = (e & f) ^ ((~e) & g); 48 1030208 uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22); 49 1030208 uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc); 50 1030208 t1 = h + S1 + ch + K[i] + w[i]; 51 1030208 t2 = S0 + maj; 52 1030208 h = g; 53 1030208 g = f; 54 1030208 f = e; 55 1030208 e = d + t1; 56 1030208 d = cc; 57 1030208 cc = b; 58 1030208 b = a; 59 1030208 a = t1 + t2; 60 } 61 16097 c->h[0] += a; 62 16097 c->h[1] += b; 63 16097 c->h[2] += cc; 64 16097 c->h[3] += d; 65 16097 c->h[4] += e; 66 16097 c->h[5] += f; 67 16097 c->h[6] += g; 68 16097 c->h[7] += h; 69 16097} 70 71 199void ygst_sha256_init(ygst_sha256_ctx *c) { 72 199 c->h[0] = 0x6a09e667u; 73 199 c->h[1] = 0xbb67ae85u; 74 199 c->h[2] = 0x3c6ef372u; 75 199 c->h[3] = 0xa54ff53au; 76 199 c->h[4] = 0x510e527fu; 77 199 c->h[5] = 0x9b05688cu; 78 199 c->h[6] = 0x1f83d9abu; 79 199 c->h[7] = 0x5be0cd19u; 80 199 c->total = 0; 81 199 c->used = 0; 82 199} 83 84 1383void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) { 85 1383 const unsigned char *p = (const unsigned char *)data; 86 1383 c->total += (uint64_t)n; 87 1383 if (c->used) { 88 1008 size_t take = 64 - c->used; 89 1008 if (take > n) take = n; 90 1008 memcpy(c->block + c->used, p, take); 91 1008 c->used += take; 92 1008 p += take; 93 1008 n -= take; 94 1008 if (c->used < 64) return; 95 996 sha256_block(c, c->block); 96 996 c->used = 0; 97 } 98 16251 while (n >= 64) { 99 14880 sha256_block(c, p); 100 14880 p += 64; 101 14880 n -= 64; 102 } 103 1371 if (n) { 104 1189 memcpy(c->block, p, n); 105 1189 c->used = n; 106 } 107 } 108 109 199void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) { 110 199 uint64_t bits = c->total * 8u; 111 int i; 112 199 c->block[c->used++] = 0x80; 113 199 if (c->used > 56) { 114 22 memset(c->block + c->used, 0, 64 - c->used); 115 22 sha256_block(c, c->block); 116 22 c->used = 0; 117 } 118 199 memset(c->block + c->used, 0, 56 - c->used); 119 1791 for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i)); 120 199 sha256_block(c, c->block); 121 1791 for (i = 0; i < 8; i++) { 122 1592 out[i * 4] = (unsigned char)(c->h[i] >> 24); 123 1592 out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16); 124 1592 out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8); 125 1592 out[i * 4 + 3] = (unsigned char)c->h[i]; 126 } 127 199} 128 129 5void ygst_sha256(const void *data, size_t n, unsigned char out[32]) { 130 ygst_sha256_ctx c; 131 5 ygst_sha256_init(&c); 132 5 ygst_sha256_update(&c, data, n); 133 5 ygst_sha256_final(&c, out); 134 5} 135 136 80void ygst_hmac_sha256(const void *key, size_t key_len, const void *msg, size_t msg_len, unsigned char out[32]) { 137 unsigned char k[64]; 138 unsigned char pad[64]; 139 unsigned char inner[32]; 140 ygst_sha256_ctx c; 141 int i; 142 80 memset(k, 0, sizeof k); 143 80 if (key_len > 64) { 144 1 ygst_sha256(key, key_len, k); 145 79 } else if (key_len) { 146 79 memcpy(k, key, key_len); 147 } 148 5200 for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36); 149 80 ygst_sha256_init(&c); 150 80 ygst_sha256_update(&c, pad, 64); 151 80 ygst_sha256_update(&c, msg, msg_len); 152 80 ygst_sha256_final(&c, inner); 153 5200 for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c); 154 80 ygst_sha256_init(&c); 155 80 ygst_sha256_update(&c, pad, 64); 156 80 ygst_sha256_update(&c, inner, 32); 157 80 ygst_sha256_final(&c, out); 158 80} 159 160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 161 162 108size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) { 163 108 size_t need = ((n + 2) / 3) * 4; 164 size_t i; 165 108 size_t o = 0; 166 108 if (!out || cap < need + 1) return 0; 167 991 for (i = 0; i + 2 < n; i += 3) { 168 885 uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2]; 169 885 out[o++] = B64[(v >> 18) & 63]; 170 885 out[o++] = B64[(v >> 12) & 63]; 171 885 out[o++] = B64[(v >> 6) & 63]; 172 885 out[o++] = B64[v & 63]; 173 } 174 106 if (n - i == 1) { 175 24 uint32_t v = (uint32_t)in[i] << 16; 176 24 out[o++] = B64[(v >> 18) & 63]; 177 24 out[o++] = B64[(v >> 12) & 63]; 178 24 out[o++] = '='; 179 24 out[o++] = '='; 180 82 } else if (n - i == 2) { 181 79 uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8); 182 79 out[o++] = B64[(v >> 18) & 63]; 183 79 out[o++] = B64[(v >> 12) & 63]; 184 79 out[o++] = B64[(v >> 6) & 63]; 185 79 out[o++] = '='; 186 } 187 106 out[o] = 0; 188 106 return o; 189 } 190 191 194void ygst_hex_lower(const unsigned char *in, size_t n, char *out) { 192 static const char H[] = "0123456789abcdef"; 193 size_t i; 194 2754 for (i = 0; i < n; i++) { 195 2560 out[i * 2] = H[in[i] >> 4]; 196 2560 out[i * 2 + 1] = H[in[i] & 15]; 197 } 198 194 out[n * 2] = 0; 199 194}
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Shengtong style result, error and reading progress JSON (DESIGN 6.2, 14.3), the dtLastResponse 6 * clock text, the request params echo, base URL resolution and small identifiers. 7 */ 8 #include "ygst_core.h" 9 10 #include <stdio.h> 11 #include <string.h> 12 13 /* civil_from_days (H. Hinnant) */ 14 106static void civil_from_days(long long z, long long *y, unsigned *m, unsigned *d) { 15 long long era; 16 unsigned doe, yoe, doy, mp; 17 106 z += 719468; 18 106 era = (z >= 0 ? z : z - 146096) / 146097; 19 106 doe = (unsigned)(z - era * 146097); 20 106 yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; 21 106 doy = doe - (365 * yoe + yoe / 4 - yoe / 100); 22 106 mp = (5 * doy + 2) / 153; 23 106 *d = doy - (153 * mp + 2) / 5 + 1; 24 106 *m = mp < 10 ? mp + 3 : mp - 9; 25 106 *y = (long long)yoe + era * 400 + (*m <= 2); 26 106} 27 28 106int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) { 29 106 long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL; 30 106 long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL); 31 106 long long rem = ms - days * 86400000LL; 32 long long y; 33 unsigned m, d, hh, mi, ss, mss; 34 106 civil_from_days(days, &y, &m, &d); 35 106 if (y < 0) y = 0; 36 106 if (y > 9999) y = 9999; 37 106 hh = (unsigned)(rem / 3600000LL) % 24u; 38 106 mi = (unsigned)(rem / 60000LL) % 60u; 39 106 ss = (unsigned)(rem / 1000LL) % 60u; 40 106 mss = (unsigned)rem % 1000u; 41 106 snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss, 42 mss); 43 106 return 0; 44 } 45 46 64int ygst_align_result(ygst_span result, ygst_buf *out) { 47 ygst_span sentences, sent, details, d, scores, v, so, sp; 48 64 size_t spos = 0; 49 64 size_t copied = 0; /* bytes of result already copied to out */ 50 64 int added = 0; 51 64 if (!result.p) return -1; 52 116 if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 || 53 53 ygst_json_type(sentences) != 'a') { 54 11 return ygst_buf_append(out, result.p, result.n) == 0 ? 0 : -1; 55 } 56 109 while (ygst_json_array_next(sentences, &spos, &sent) == 1) { 57 57 size_t dpos = 0; 58 109 if (ygst_json_type(sent) != 'o' || ygst_json_member(sent, "details", &details) != 1 || 59 52 ygst_json_type(details) != 'a') { 60 6 continue; 61 } 62 343 while (ygst_json_array_next(details, &dpos, &d) == 1) { 63 int add_overall, add_pron; 64 size_t close; 65 292 if (ygst_json_type(d) != 'o' || ygst_json_member(d, "overall", &v) == 1) continue; 66 290 if (ygst_json_member(d, "scores", &scores) != 1 || ygst_json_type(scores) != 'o') continue; 67 288 add_overall = ygst_json_member(scores, "overall", &so) == 1; 68 288 add_pron = ygst_json_member(d, "pronunciation", &v) != 1 && ygst_json_member(scores, "pronunciation", &sp) == 1; 69 288 if (!add_overall && !add_pron) continue; 70 287 close = (size_t)(d.p + d.n - 1 - result.p); /* the closing brace of the item */ 71 287 ygst_buf_append(out, result.p + copied, close - copied); 72 287 if (add_overall) { 73 287 ygst_buf_append_str(out, ",\"overall\":"); 74 287 ygst_buf_append(out, so.p, so.n); 75 287 added++; 76 } 77 287 if (add_pron) { 78 284 ygst_buf_append_str(out, ",\"pronunciation\":"); 79 284 ygst_buf_append(out, sp.p, sp.n); 80 284 added++; 81 } 82 287 copied = close; 83 } 84 } 85 52 ygst_buf_append(out, result.p + copied, result.n - copied); 86 52 return out->oom ? -1 : added; 87 } 88 89 1053static void key(ygst_buf *out, const char *k, int first) { 90 1053 if (!first) ygst_buf_append_char(out, ','); 91 1053 ygst_json_escape(out, k); 92 1053 ygst_buf_append_char(out, ':'); 93 1053} 94 95 54int ygst_envelope_result(ygst_buf *out, const char *token_id, const char *record_id_json, const char *app_key, 96 const char *user_id, const char *ref_text, const char *dt_last_response, 97 ygst_span result_raw, const char *params_json, const char *audio_url) { 98 54 if (!result_raw.p || result_raw.n == 0) return -1; 99 53 ygst_buf_append_char(out, '{'); 100 53 key(out, "tokenId", 1); 101 53 ygst_json_escape(out, token_id); 102 53 if (record_id_json && record_id_json[0]) { 103 52 key(out, "recordId", 0); 104 52 ygst_buf_append_str(out, record_id_json); 105 } 106 53 key(out, "applicationId", 0); 107 53 ygst_json_escape(out, app_key); 108 53 key(out, "userId", 0); 109 53 ygst_json_escape(out, user_id); 110 53 key(out, "refText", 0); 111 53 ygst_json_escape(out, ref_text); 112 53 key(out, "eof", 0); 113 53 ygst_buf_append_char(out, '1'); 114 53 key(out, "dtLastResponse", 0); 115 53 ygst_json_escape(out, dt_last_response); 116 53 key(out, "result", 0); 117 53 if (ygst_align_result(result_raw, out) < 0) return -1; 118 53 if (params_json && params_json[0]) { 119 3 key(out, "params", 0); 120 3 ygst_buf_append_str(out, params_json); 121 } 122 53 if (audio_url && audio_url[0]) { 123 3 key(out, "audioUrl", 0); 124 3 ygst_json_escape(out, audio_url); 125 } 126 53 ygst_buf_append_char(out, '}'); 127 53 ygst_buf_cstr(out); 128 53 return out->oom ? -1 : 0; 129 } 130 131 20int ygst_envelope_error(ygst_buf *out, const char *token_id, int err_id, const char *message, 132 const char *app_key) { 133 20 ygst_buf_append_char(out, '{'); 134 20 key(out, "tokenId", 1); 135 20 ygst_json_escape(out, token_id); 136 20 key(out, "errId", 0); 137 20 ygst_buf_append_int(out, err_id); 138 20 key(out, "error", 0); 139 20 ygst_json_escape(out, message); 140 20 key(out, "eof", 0); 141 20 ygst_buf_append_char(out, '1'); 142 20 key(out, "applicationId", 0); 143 20 ygst_json_escape(out, app_key); 144 20 ygst_buf_append_char(out, '}'); 145 20 ygst_buf_cstr(out); 146 20 return out->oom ? -1 : 0; 147 } 148 149 /* The progress result as the platform sent it, without the first skip items of "words". */ 150 60static int append_progress_result(ygst_buf *out, ygst_span result, int skip) { 151 ygst_span words, item; 152 60 size_t pos = 0; 153 60 int i = 0; 154 60 int first = 1; 155 62 if (skip <= 0 || ygst_json_type(result) != 'o' || ygst_json_member(result, "words", &words) != 1 || 156 2 ygst_json_type(words) != 'a') { 157 58 return ygst_buf_append(out, result.p, result.n); 158 } 159 2 ygst_buf_append(out, result.p, (size_t)(words.p - result.p)); 160 2 ygst_buf_append_char(out, '['); 161 8 while (ygst_json_array_next(words, &pos, &item) == 1) { 162 6 if (i++ < skip) continue; 163 1 if (!first) ygst_buf_append_char(out, ','); 164 1 first = 0; 165 1 ygst_buf_append(out, item.p, item.n); 166 } 167 2 ygst_buf_append_char(out, ']'); 168 2 ygst_buf_append(out, words.p + words.n, (size_t)(result.p + result.n - (words.p + words.n))); 169 2 return out->oom ? -1 : 0; 170 } 171 172 62int ygst_envelope_progress(ygst_buf *out, const char *token_id, const char *app_key, const char *user_id, 173 const char *ref_text, const char *dt_last_response, ygst_span result_raw, 174 int skip_words) { 175 62 if (!result_raw.p || result_raw.n == 0) return -1; 176 60 ygst_buf_append_char(out, '{'); 177 60 key(out, "tokenId", 1); 178 60 ygst_json_escape(out, token_id); 179 /* Shengtong's intermediate results carry recordId; the platform names the record only in the final */ 180 60 key(out, "recordId", 0); 181 60 ygst_buf_append_str(out, "\"\""); 182 60 key(out, "applicationId", 0); 183 60 ygst_json_escape(out, app_key); 184 60 key(out, "userId", 0); 185 60 ygst_json_escape(out, user_id); 186 60 key(out, "refText", 0); 187 60 ygst_json_escape(out, ref_text); 188 60 key(out, "eof", 0); 189 60 ygst_buf_append_char(out, '0'); 190 60 key(out, "dtLastResponse", 0); 191 60 ygst_json_escape(out, dt_last_response); 192 60 key(out, "result", 0); 193 60 if (append_progress_result(out, result_raw, skip_words) != 0) return -1; 194 60 ygst_buf_append_char(out, '}'); 195 60 ygst_buf_cstr(out); 196 60 return out->oom ? -1 : 0; 197 } 198 199 4int ygst_params_json(ygst_buf *out, const char *app_key, const char *user_id, int64_t timestamp_sec, 200 const char *audio_type, int sample_rate, int channel, int sample_bytes, 201 const char *core_type, const char *token_id, const ygst_fields *request_fields) { 202 size_t i; 203 char ts[32]; 204 4 snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec); 205 4 ygst_buf_append_str(out, "{\"app\":{"); 206 4 key(out, "applicationId", 1); 207 4 ygst_json_escape(out, app_key); 208 4 key(out, "userId", 0); 209 4 ygst_json_escape(out, user_id); 210 4 key(out, "timestamp", 0); 211 4 ygst_json_escape(out, ts); 212 4 ygst_buf_append_str(out, "},\"audio\":{"); 213 4 key(out, "audioType", 1); 214 4 ygst_json_escape(out, audio_type); 215 4 key(out, "sampleRate", 0); 216 4 ygst_buf_append_int(out, sample_rate); 217 4 key(out, "channel", 0); 218 4 ygst_buf_append_int(out, channel); 219 4 key(out, "sampleBytes", 0); 220 4 ygst_buf_append_int(out, sample_bytes); 221 4 ygst_buf_append_str(out, "},\"request\":{"); 222 4 key(out, "coreType", 1); 223 4 ygst_json_escape(out, core_type); 224 4 key(out, "tokenId", 0); 225 4 ygst_json_escape(out, token_id); 226 4 if (request_fields) { 227 12 for (i = 0; i < request_fields->count; i++) { 228 9 const char *n = request_fields->items[i].name; 229 9 if (strcmp(n, "coreType") == 0 || strcmp(n, "tokenId") == 0) continue; 230 8 key(out, n, 0); 231 8 ygst_json_escape(out, request_fields->items[i].value); 232 } 233 } 234 4 ygst_buf_append_str(out, "}}"); 235 4 ygst_buf_cstr(out); 236 4 return out->oom ? -1 : 0; 237 } 238 239 /* ------------------------------------------------------------------ base URL */ 240 241 46static int prefix_ci(const char *s, const char *p) { 242 157 while (*p) { 243 144 char a = *s, b = *p; 244 144 if (a >= 'A' && a <= 'Z') a = (char)(a + 32); 245 144 if (a != b) return 0; 246 111 s++; 247 111 p++; 248 } 249 13 return 1; 250 } 251 252 13static int host_is_shengtong(const char *host, size_t n) { 253 static const char D[] = "stkouyu.com"; 254 13 size_t m = sizeof D - 1; 255 char h[256]; 256 size_t i; 257 13 if (n == 0 || n >= sizeof h) return 0; 258 195 for (i = 0; i < n; i++) { 259 182 char c = host[i]; 260 182 h[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; 261 } 262 13 h[n] = 0; 263 13 if (n == m) return strcmp(h, D) == 0; 264 9 return n > m && strcmp(h + n - m, D) == 0 && h[n - m - 1] == '.'; 265 } 266 267 18ygst_base_kind ygst_resolve_base_url(const char *server, char *out, size_t cap) { 268 char s[1024]; 269 const char *scheme; 270 const char *rest; 271 const char *host_start; 272 const char *host_end; 273 const char *end; 274 size_t n; 275 int written; 276 18 if (!out || cap == 0) return YGST_BASE_DEFAULT_EMPTY; 277 18 snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL); 278 18 n = ygst_trim_copy(server, s, sizeof s); 279 18 if (n == 0) return YGST_BASE_DEFAULT_EMPTY; 280 16 if (prefix_ci(s, "https://")) { 281 2 scheme = "https"; 282 2 rest = s + 8; 283 14 } else if (prefix_ci(s, "http://")) { 284 5 scheme = "http"; 285 5 rest = s + 7; 286 9 } else if (prefix_ci(s, "wss://")) { 287 2 scheme = "https"; 288 2 rest = s + 6; 289 7 } else if (prefix_ci(s, "ws://")) { 290 4 scheme = "http"; 291 4 rest = s + 5; 292 } else { 293 3 return host_is_shengtong(s, strcspn(s, "/:?#")) ? YGST_BASE_DEFAULT_SHENGTONG : YGST_BASE_ERR_SCHEME; 294 } 295 13 host_start = rest; 296 13 if (*host_start == '[') { 297 2 host_end = strchr(host_start, ']'); 298 2 if (!host_end) return YGST_BASE_ERR_HOST; 299 1 host_end++; 300 } else { 301 11 host_end = host_start; 302 159 while (*host_end && *host_end != '/' && *host_end != ':' && *host_end != '?' && *host_end != '#') host_end++; 303 } 304 12 if (host_end == host_start) return YGST_BASE_ERR_HOST; 305 10 if (host_is_shengtong(host_start, (size_t)(host_end - host_start))) return YGST_BASE_DEFAULT_SHENGTONG; 306 7 end = rest; 307 145 while (*end && *end != '?' && *end != '#') end++; 308 9 while (end > host_end && end[-1] == '/') end--; 309 7 written = snprintf(out, cap, "%s://%.*s", scheme, (int)(end - rest), rest); 310 7 if (written < 0 || (size_t)written >= cap) { 311 1 snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL); 312 1 return YGST_BASE_ERR_HOST; 313 } 314 6 return YGST_BASE_CUSTOM; 315 } 316 317 44int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) { 318 size_t n; 319 int w; 320 44 if (!base || !core_type || !out) return -1; 321 43 n = strlen(base); 322 44 while (n > 0 && base[n - 1] == '/') n--; 323 43 w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type); 324 43 return (w < 0 || (size_t)w >= cap) ? -1 : 0; 325 } 326 327 63int ygst_token_id(const unsigned char rnd[16], char out[33]) { 328 63 ygst_hex_lower(rnd, 16, out); 329 63 return 0; 330 } 331 332 65int ygst_idempotency_key_ok(const char *k) { 333 65 size_t n = 0; 334 65 if (!k) return 0; 335 2265 for (; k[n]; n++) { 336 2206 unsigned char c = (unsigned char)k[n]; 337 2206 if (c < 0x21 || c > 0x7E) return 0; 338 2202 if (n >= 200) return 0; 339 } 340 59 return n >= 1; 341 } 342 343 3void ygst_mask_app_key(const char *app_key, char out[16]) { 344 3 size_t n = app_key ? strlen(app_key) : 0; 345 3 if (n > 4) n = 4; 346 3 memcpy(out, app_key ? app_key : "", n); 347 3 memcpy(out + n, "***", 4); 348 3}
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 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Ordered form field list and the platform request signature (CONTRACT.md 0.2): 6 * drop empty values, sort by key, join key=value with &, Base64(HMAC_SHA256(payload, secret)). 7 */ 8 #include "ygst_core.h" 9 10 #include <stdlib.h> 11 #include <string.h> 12 13 550void ygst_fields_init(ygst_fields *f) { 14 550 f->items = NULL; 15 550 f->count = 0; 16 550 f->cap = 0; 17 550 f->oom = 0; 18 550} 19 20 271void ygst_fields_free(ygst_fields *f) { 21 size_t i; 22 810 for (i = 0; i < f->count; i++) { 23 539 free(f->items[i].name); 24 539 free(f->items[i].value); 25 } 26 271 free(f->items); 27 271 ygst_fields_init(f); 28 271} 29 30 562static long find_index(const ygst_fields *f, const char *name) { 31 size_t i; 32 2727 for (i = 0; i < f->count; i++) { 33 2182 if (strcmp(f->items[i].name, name) == 0) return (long)i; 34 } 35 545 return -1; 36 } 37 38 2int ygst_fields_remove(ygst_fields *f, const char *name) { 39 2 long i = name ? find_index(f, name) : -1; 40 2 if (i < 0) return 0; 41 1 free(f->items[i].name); 42 1 free(f->items[i].value); 43 1 memmove(&f->items[i], &f->items[i + 1], (f->count - (size_t)i - 1) * sizeof(ygst_field)); 44 1 f->count--; 45 1 return 1; 46 } 47 48 552int ygst_fields_set(ygst_fields *f, const char *name, const char *value) { 49 long i; 50 char *v; 51 552 if (!name || f->oom) return -1; 52 551 if (!value) { 53 1 ygst_fields_remove(f, name); 54 1 return 0; 55 } 56 550 i = find_index(f, name); 57 550 v = ygst_strdup(value); 58 550 if (!v) { 59 0 f->oom = 1; 60 0 return -1; 61 } 62 550 if (i >= 0) { 63 10 free(f->items[i].value); 64 10 f->items[i].value = v; 65 10 return 0; 66 } 67 540 if (f->count == f->cap) { 68 195 size_t cap = f->cap ? f->cap * 2 : 16; 69 195 ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field)); 70 195 if (!p) { 71 0 free(v); 72 0 f->oom = 1; 73 0 return -1; 74 } 75 195 f->items = p; 76 195 f->cap = cap; 77 } 78 540 f->items[f->count].name = ygst_strdup(name); 79 540 if (!f->items[f->count].name) { 80 0 free(v); 81 0 f->oom = 1; 82 0 return -1; 83 } 84 540 f->items[f->count].value = v; 85 540 f->count++; 86 540 return 0; 87 } 88 89 10const char *ygst_fields_get(const ygst_fields *f, const char *name) { 90 10 long i = name ? find_index(f, name) : -1; 91 10 return i < 0 ? NULL : f->items[i].value; 92 } 93 94 44int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) { 95 size_t i; 96 183 for (i = 0; i < src->count; i++) { 97 139 if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1; 98 } 99 44 return 0; 100 } 101 102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */ 103 800static uint32_t next_cp(const unsigned char **s) { 104 800 const unsigned char *p = *s; 105 800 uint32_t c = p[0]; 106 800 int n = 0; 107 int i; 108 800 if (c < 0x80) { 109 790 *s = p + 1; 110 790 return c; 111 } 112 10 if ((c & 0xE0) == 0xC0) { 113 3 n = 1; 114 3 c &= 0x1F; 115 7 } else if ((c & 0xF0) == 0xE0) { 116 2 n = 2; 117 2 c &= 0x0F; 118 5 } else if ((c & 0xF8) == 0xF0) { 119 4 n = 3; 120 4 c &= 0x07; 121 } else { 122 1 *s = p + 1; 123 1 return 0xFFFD; 124 } 125 27 for (i = 1; i <= n; i++) { 126 19 if ((p[i] & 0xC0) != 0x80) { 127 1 *s = p + 1; 128 1 return 0xFFFD; 129 } 130 18 c = (c << 6) | (p[i] & 0x3F); 131 } 132 8 *s = p + n + 1; 133 8 return c; 134 } 135 136 /* Emits the UTF-16 code units of cp into u, returns how many (1 or 2). */ 137 800static int utf16_units(uint32_t cp, uint32_t u[2]) { 138 800 if (cp >= 0x10000 && cp <= 0x10FFFF) { 139 4 cp -= 0x10000; 140 4 u[0] = 0xD800 + (cp >> 10); 141 4 u[1] = 0xDC00 + (cp & 0x3FF); 142 4 return 2; 143 } 144 796 u[0] = cp; 145 796 return 1; 146 } 147 148 325int ygst_utf16_compare(const char *a, const char *b) { 149 325 const unsigned char *pa = (const unsigned char *)a; 150 325 const unsigned char *pb = (const unsigned char *)b; 151 uint32_t ua[2], ub[2]; 152 325 int na = 0, nb = 0, ia = 0, ib = 0; 153 77 for (;;) { 154 uint32_t ca, cb; 155 402 if (ia == na) { 156 402 if (!*pa) { 157 2 na = 0; 158 } else { 159 400 na = utf16_units(next_cp(&pa), ua); 160 } 161 402 ia = 0; 162 } 163 402 if (ib == nb) { 164 402 if (!*pb) { 165 2 nb = 0; 166 } else { 167 400 nb = utf16_units(next_cp(&pb), ub); 168 } 169 402 ib = 0; 170 } 171 402 if (na == 0 || nb == 0) { 172 3 if (na == 0 && nb == 0) return 0; 173 2 return na == 0 ? -1 : 1; 174 } 175 399 ca = ua[ia++]; 176 399 cb = ub[ib++]; 177 399 if (ca != cb) return ca < cb ? -1 : 1; 178 } 179 } 180 181 317static int cmp_field_ptr(const void *x, const void *y) { 182 317 const ygst_field *a = *(const ygst_field *const *)x; 183 317 const ygst_field *b = *(const ygst_field *const *)y; 184 317 return ygst_utf16_compare(a->name, b->name); 185 } 186 187 86int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) { 188 const ygst_field **v; 189 86 size_t n = 0; 190 size_t i; 191 86 int first = 1; 192 86 if (f->oom) return -1; 193 86 v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v)); 194 86 if (!v) return -1; 195 388 for (i = 0; i < f->count; i++) { 196 302 if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i]; 197 } 198 86 qsort(v, n, sizeof(*v), cmp_field_ptr); 199 366 for (i = 0; i < n; i++) { 200 280 if (!first) ygst_buf_append_char(out, '&'); 201 280 first = 0; 202 280 ygst_buf_append_str(out, v[i]->name); 203 280 ygst_buf_append_char(out, '='); 204 280 ygst_buf_append_str(out, v[i]->value); 205 } 206 86 free(v); 207 86 ygst_buf_cstr(out); 208 86 return out->oom ? -1 : 0; 209 } 210 211 78int ygst_sign(const ygst_fields *f, const char *secret, char out[45]) { 212 ygst_buf payload; 213 unsigned char mac[32]; 214 int rc; 215 78 out[0] = 0; 216 78 if (!secret) return -1; 217 77 ygst_buf_init(&payload); 218 77 rc = ygst_sign_payload(f, &payload); 219 77 if (rc == 0) { 220 77 ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"", 221 payload.len, mac); 222 77 if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1; 223 } 224 77 ygst_buf_free(&payload); 225 77 return rc; 226 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Minimal validating JSON scanner (RFC 8259). It never builds a tree: values are spans into the 6 * original text, so the platform result can be passed to the caller byte for byte. 7 */ 8 #include "ygst_core.h" 9 10 #include <stdlib.h> 11 #include <string.h> 12 13 #define YGST_JSON_MAX_DEPTH 128 14 15 5664988static const char *ws(const char *p, const char *e) { 16 11646031 while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; 17 5664988 return p; 18 } 19 20 156static int hexval(char c) { 21 156 if (c >= '0' && c <= '9') return c - '0'; 22 27 if (c >= 'a' && c <= 'f') return c - 'a' + 10; 23 0 if (c >= 'A' && c <= 'F') return c - 'A' + 10; 24 0 return -1; 25 } 26 27 1555671static const char *skip_string(const char *p, const char *e) { 28 1555671 if (p >= e || *p != '"') return NULL; 29 1555668 p++; 30 11230030 while (p < e) { 31 11230028 unsigned char c = (unsigned char)*p; 32 11230028 if (c == '"') return p + 1; 33 9674365 if (c < 0x20) return NULL; 34 9674364 if (c == '\\') { 35 792 p++; 36 792 if (p >= e) return NULL; 37 792 switch (*p) { 38 767 case '"': 39 case '\\': 40 case '/': 41 case 'b': 42 case 'f': 43 case 'n': 44 case 'r': 45 case 't': 46 767 p++; 47 767 break; 48 24 case 'u': { 49 int i; 50 24 if (e - p < 5) return NULL; 51 115 for (i = 1; i <= 4; i++) { 52 92 if (hexval(p[i]) < 0) return NULL; 53 } 54 23 p += 5; 55 23 break; 56 } 57 1 default: 58 1 return NULL; 59 } 60 790 continue; 61 } 62 9673572 p++; 63 } 64 2 return NULL; 65 } 66 67 1483640static int is_digit(char c) { 68 1483640 return c >= '0' && c <= '9'; 69 } 70 71 453879static const char *skip_number(const char *p, const char *e) { 72 453879 if (p < e && *p == '-') p++; 73 453879 if (p >= e) return NULL; 74 453878 if (*p == '0') { 75 162568 p++; 76 291310 } else if (*p >= '1' && *p <= '9') { 77 953260 while (p < e && is_digit(*p)) p++; 78 } else { 79 0 return NULL; 80 } 81 453878 if (p < e && *p == '.') { 82 24802 p++; 83 24802 if (p >= e || !is_digit(*p)) return NULL; 84 59713 while (p < e && is_digit(*p)) p++; 85 } 86 453877 if (p < e && (*p == 'e' || *p == 'E')) { 87 8 p++; 88 8 if (p < e && (*p == '+' || *p == '-')) p++; 89 8 if (p >= e || !is_digit(*p)) return NULL; 90 19 while (p < e && is_digit(*p)) p++; 91 } 92 453876 return p; 93 } 94 95 240886static const char *skip_lit(const char *p, const char *e, const char *lit) { 96 240886 size_t n = strlen(lit); 97 240886 if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL; 98 240882 return p + n; 99 } 100 101 1398134static const char *skip_value(const char *p, const char *e, int depth) { 102 1398134 if (depth > YGST_JSON_MAX_DEPTH) return NULL; 103 1398133 p = ws(p, e); 104 1398133 if (p >= e) return NULL; 105 1398129 switch (*p) { 106 257923 case '{': 107 257923 p = ws(p + 1, e); 108 257923 if (p < e && *p == '}') return p + 1; 109 for (;;) { 110 1095090 p = skip_string(ws(p, e), e); 111 1095090 if (!p) return NULL; 112 1095087 p = ws(p, e); 113 1095087 if (p >= e || *p != ':') return NULL; 114 1095086 p = skip_value(p + 1, e, depth + 1); 115 1095086 if (!p) return NULL; 116 1095084 p = ws(p, e); 117 1095084 if (p >= e) return NULL; 118 1095084 if (*p == ',') { 119 837222 p++; 120 837222 continue; 121 } 122 257862 if (*p == '}') return p + 1; 123 1 return NULL; 124 } 125 91346 case '[': 126 91346 p = ws(p + 1, e); 127 91346 if (p < e && *p == ']') return p + 1; 128 for (;;) { 129 172231 p = skip_value(p, e, depth + 1); 130 172231 if (!p) return NULL; 131 172100 p = ws(p, e); 132 172100 if (p >= e) return NULL; 133 172100 if (*p == ',') { 134 106187 p++; 135 106187 continue; 136 } 137 65913 if (*p == ']') return p + 1; 138 1 return NULL; 139 } 140 355966 case '"': 141 355966 return skip_string(p, e); 142 112839 case 't': 143 112839 return skip_lit(p, e, "true"); 144 128022 case 'f': 145 128022 return skip_lit(p, e, "false"); 146 25 case 'n': 147 25 return skip_lit(p, e, "null"); 148 452008 default: 149 452008 if (*p == '-' || is_digit(*p)) return skip_number(p, e); 150 6 return NULL; 151 } 152 } 153 154 675int ygst_json_parse(const char *s, size_t n, ygst_span *root) { 155 const char *e; 156 const char *start; 157 const char *end; 158 675 if (!s) return -1; 159 674 e = s + n; 160 674 start = ws(s, e); 161 674 end = skip_value(start, e, 0); 162 674 if (!end) return -1; 163 646 if (ws(end, e) != e) return -1; 164 644 if (root) { 165 644 root->p = start; 166 644 root->n = (size_t)(end - start); 167 } 168 644 return 0; 169 } 170 171 230033int ygst_json_type(ygst_span v) { 172 230033 if (!v.p || v.n == 0) return 0; 173 230032 switch (v.p[0]) { 174 151450 case '{': 175 151450 return 'o'; 176 55260 case '[': 177 55260 return 'a'; 178 6881 case '"': 179 6881 return 's'; 180 6720 case 't': 181 6720 return 't'; 182 6649 case 'f': 183 6649 return 'f'; 184 6 case 'n': 185 6 return 'z'; 186 3066 default: 187 3066 return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0; 188 } 189 } 190 191 122944int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) { 192 122944 const char *e = obj.p + obj.n; 193 const char *p; 194 const char *k; 195 const char *v; 196 122944 if (ygst_json_type(obj) != 'o') return -1; 197 122943 p = obj.p + (*pos ? *pos : 1); 198 122943 p = ws(p, e); 199 122943 if (p >= e) return -1; 200 122943 if (*p == '}') return 0; 201 100345 if (*pos) { 202 77754 if (*p != ',') return -1; 203 77754 p = ws(p + 1, e); 204 } 205 100345 k = p; 206 100345 p = skip_string(p, e); 207 100345 if (!p) return -1; 208 100345 if (key) { 209 100345 key->p = k; 210 100345 key->n = (size_t)(p - k); 211 } 212 100345 p = ws(p, e); 213 100345 if (p >= e || *p != ':') return -1; 214 100344 v = ws(p + 1, e); 215 100344 p = skip_value(v, e, 1); 216 100344 if (!p) return -1; 217 100344 if (value) { 218 100344 value->p = v; 219 100344 value->n = (size_t)(p - v); 220 } 221 100344 *pos = (size_t)(p - obj.p); 222 100344 return 1; 223 } 224 225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */ 226 98495static int key_equals(ygst_span tok, const char *key) { 227 98495 size_t klen = strlen(key); 228 98495 if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) { 229 98490 return tok.n - 2 == klen && memcmp(tok.p + 1, key, klen) == 0; 230 } else { 231 ygst_buf b; 232 int eq; 233 5 ygst_buf_init(&b); 234 5 eq = ygst_json_string(tok, &b) == 0 && b.len == klen && memcmp(ygst_buf_cstr(&b), key, klen) == 0; 235 5 ygst_buf_free(&b); 236 5 return eq; 237 } 238 } 239 240 22328int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) { 241 22328 size_t pos = 0; 242 ygst_span k, v; 243 22328 int found = 0; 244 int rc; 245 22328 if (ygst_json_type(obj) != 'o' || !key) return -1; 246 120822 while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) { 247 98495 if (key_equals(k, key)) { 248 20951 found = 1; 249 20951 if (out) *out = v; 250 } 251 } 252 22327 if (rc < 0) return -1; 253 22326 return found; 254 } 255 256 30318int ygst_json_array_next(ygst_span arr, size_t *pos, ygst_span *out) { 257 30318 const char *e = arr.p + arr.n; 258 const char *p; 259 const char *v; 260 30318 if (ygst_json_type(arr) != 'a') return -1; 261 30317 p = arr.p + (*pos ? *pos : 1); 262 30317 p = ws(p, e); 263 30317 if (p >= e) return -1; 264 30317 if (*p == ']') return 0; 265 29800 if (*pos) { 266 27203 if (*p != ',') return -1; 267 27202 p = ws(p + 1, e); 268 } 269 29799 v = p; 270 29799 p = skip_value(v, e, 1); 271 29799 if (!p) return -1; 272 29799 if (out) { 273 29799 out->p = v; 274 29799 out->n = (size_t)(p - v); 275 } 276 29799 *pos = (size_t)(p - arr.p); 277 29799 return 1; 278 } 279 280 14static void put_utf8(ygst_buf *out, uint32_t cp) { 281 char b[4]; 282 14 if (cp < 0x80) { 283 6 b[0] = (char)cp; 284 6 ygst_buf_append(out, b, 1); 285 8 } else if (cp < 0x800) { 286 1 b[0] = (char)(0xC0 | (cp >> 6)); 287 1 b[1] = (char)(0x80 | (cp & 0x3F)); 288 1 ygst_buf_append(out, b, 2); 289 7 } else if (cp < 0x10000) { 290 6 b[0] = (char)(0xE0 | (cp >> 12)); 291 6 b[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); 292 6 b[2] = (char)(0x80 | (cp & 0x3F)); 293 6 ygst_buf_append(out, b, 3); 294 } else { 295 1 b[0] = (char)(0xF0 | (cp >> 18)); 296 1 b[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); 297 1 b[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); 298 1 b[3] = (char)(0x80 | (cp & 0x3F)); 299 1 ygst_buf_append(out, b, 4); 300 } 301 14} 302 303 16static uint32_t hex4(const char *p) { 304 16 return (uint32_t)((hexval(p[0]) << 12) | (hexval(p[1]) << 8) | (hexval(p[2]) << 4) | hexval(p[3])); 305 } 306 307 4271int ygst_json_string(ygst_span v, ygst_buf *out) { 308 const char *p; 309 const char *e; 310 4271 if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1; 311 4269 p = v.p + 1; 312 4269 e = v.p + v.n - 1; 313 4352 while (p < e) { 314 4276 const char *run = p; 315 31392 while (p < e && *p != '\\') p++; 316 4276 if (p > run) ygst_buf_append(out, run, (size_t)(p - run)); 317 4276 if (p >= e) break; 318 83 p++; /* backslash */ 319 83 switch (*p) { 320 1 case 'b': 321 1 ygst_buf_append_char(out, '\b'); 322 1 p++; 323 1 break; 324 1 case 'f': 325 1 ygst_buf_append_char(out, '\f'); 326 1 p++; 327 1 break; 328 1 case 'n': 329 1 ygst_buf_append_char(out, '\n'); 330 1 p++; 331 1 break; 332 1 case 'r': 333 1 ygst_buf_append_char(out, '\r'); 334 1 p++; 335 1 break; 336 1 case 't': 337 1 ygst_buf_append_char(out, '\t'); 338 1 p++; 339 1 break; 340 14 case 'u': { 341 14 uint32_t cp = hex4(p + 1); 342 14 p += 5; 343 14 if (cp >= 0xD800 && cp <= 0xDBFF) { 344 5 if (e - p >= 6 && p[0] == '\\' && p[1] == 'u') { 345 2 uint32_t lo = hex4(p + 2); 346 2 if (lo >= 0xDC00 && lo <= 0xDFFF) { 347 1 cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); 348 1 p += 6; 349 } else { 350 1 cp = 0xFFFD; 351 } 352 } else { 353 1 cp = 0xFFFD; 354 } 355 11 } else if (cp >= 0xDC00 && cp <= 0xDFFF) { 356 1 cp = 0xFFFD; 357 } 358 14 put_utf8(out, cp); 359 14 break; 360 } 361 64 default: /* " \ / */ 362 64 ygst_buf_append_char(out, *p); 363 64 p++; 364 64 break; 365 } 366 } 367 4269 ygst_buf_cstr(out); 368 4269 return out->oom ? -1 : 0; 369 } 370 371 1879static int number_text(ygst_span v, char *tmp, size_t cap) { 372 1879 if (ygst_json_type(v) != 'n' || v.n >= cap) return -1; 373 1877 if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1; 374 1877 memcpy(tmp, v.p, v.n); 375 1877 tmp[v.n] = 0; 376 1877 return 0; 377 } 378 379 21int ygst_json_number(ygst_span v, double *out) { 380 char tmp[64]; 381 21 char *end = NULL; 382 21 if (number_text(v, tmp, sizeof tmp) != 0) return -1; 383 20 *out = strtod(tmp, &end); 384 20 if (end && *end) { 385 /* A locale with a decimal comma: retry with the separator it expects. */ 386 0 char *dot = strchr(tmp, '.'); 387 0 if (dot) { 388 0 *dot = ','; 389 0 *out = strtod(tmp, &end); 390 } 391 0 if (end && *end) return -1; 392 } 393 20 return 0; 394 } 395 396 1858int ygst_json_int(ygst_span v, long long *out) { 397 char tmp[64]; 398 size_t i; 399 1858 int neg = 0; 400 1858 long long acc = 0; 401 1858 if (number_text(v, tmp, sizeof tmp) != 0) return -1; 402 1857 if (strpbrk(tmp, ".eE")) { 403 double d; 404 4 if (ygst_json_number(v, &d) != 0) return -1; 405 4 if (d != d || d > 9.2e18 || d < -9.2e18 || d != (double)(long long)d) return -1; 406 2 *out = (long long)d; 407 2 return 0; 408 } 409 1853 i = 0; 410 1853 if (tmp[0] == '-') { 411 1 neg = 1; 412 1 i = 1; 413 } 414 6430 for (; tmp[i]; i++) { 415 4578 int dgt = tmp[i] - '0'; 416 4578 if (acc > (9223372036854775807LL - dgt) / 10) return -1; 417 4577 acc = acc * 10 + dgt; 418 } 419 1852 *out = neg ? -acc : acc; 420 1852 return 0; 421 } 422 423 13369int ygst_json_bool(ygst_span v, int *out) { 424 13369 int t = ygst_json_type(v); 425 13369 if (t == 't' && v.n == 4) { 426 6719 *out = 1; 427 6719 return 0; 428 } 429 6650 if (t == 'f' && v.n == 5) { 430 6649 *out = 0; 431 6649 return 0; 432 } 433 1 return -1; 434 } 435 436 2049int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) { 437 static const char H[] = "0123456789abcdef"; 438 size_t i; 439 2049 size_t run = 0; 440 2049 ygst_buf_append_char(out, '"'); 441 49453 for (i = 0; i < n; i++) { 442 47404 unsigned char c = (unsigned char)s[i]; 443 47404 const char *rep = NULL; 444 char u[7]; 445 47404 if (c == '"') { 446 17 rep = "\\\""; 447 47387 } else if (c == '\\') { 448 2 rep = "\\\\"; 449 47385 } else if (c == '\n') { 450 3 rep = "\\n"; 451 47382 } else if (c == '\r') { 452 1 rep = "\\r"; 453 47381 } else if (c == '\t') { 454 1 rep = "\\t"; 455 47380 } else if (c == '\b') { 456 1 rep = "\\b"; 457 47379 } else if (c == '\f') { 458 1 rep = "\\f"; 459 47378 } else if (c < 0x20) { 460 2 u[0] = '\\'; 461 2 u[1] = 'u'; 462 2 u[2] = '0'; 463 2 u[3] = '0'; 464 2 u[4] = H[c >> 4]; 465 2 u[5] = H[c & 15]; 466 2 u[6] = 0; 467 2 rep = u; 468 } 469 47404 if (rep) { 470 28 if (i > run) ygst_buf_append(out, s + run, i - run); 471 28 ygst_buf_append_str(out, rep); 472 28 run = i + 1; 473 } 474 } 475 2049 if (n > run) ygst_buf_append(out, s + run, n - run); 476 2049 ygst_buf_append_char(out, '"'); 477 2049 return out->oom ? -1 : 0; 478 } 479 480 2049int ygst_json_escape(ygst_buf *out, const char *s) { 481 2049 if (!s) s = ""; 482 2049 return ygst_json_escape_n(out, s, strlen(s)); 483 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * multipart/form-data assembly (RFC 7578) for POST /{coreType}. Text fields carry no filename, 6 * so the platform treats them as form parameters and they are exactly the signed set; the audio 7 * is a file part and is not signed. 8 */ 9 #include "ygst_core.h" 10 11 #include <stdio.h> 12 #include <string.h> 13 14 42int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) { 15 char hex[33]; 16 42 ygst_hex_lower(rnd, 16, hex); 17 42 snprintf(out, 64, "----YuguSTCompat%s", hex); 18 42 return 0; 19 } 20 21 43int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) { 22 int n; 23 43 if (!boundary || !out) return -1; 24 43 n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary); 25 43 return (n < 0 || (size_t)n >= cap) ? -1 : 0; 26 } 27 28 136static int contains(const unsigned char *hay, size_t n, const char *needle) { 29 136 size_t m = strlen(needle); 30 size_t i; 31 136 if (m == 0 || n < m) return 0; 32 2983774 for (i = 0; i + m <= n; i++) { 33 2983727 if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1; 34 } 35 47 return 0; 36 } 37 38 49int ygst_multipart_build(const ygst_fields *fields, const char *boundary, const char *file_field, 39 const char *filename, const char *file_content_type, const unsigned char *file, 40 size_t file_len, ygst_buf *out) { 41 size_t i; 42 49 if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1; 43 47 if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1; 44 138 for (i = 0; i < fields->count; i++) { 45 94 const char *v = fields->items[i].value ? fields->items[i].value : ""; 46 94 if (!ygst_field_name_ok(fields->items[i].name)) return -1; 47 93 if (contains((const unsigned char *)v, strlen(v), boundary)) return -2; 48 } 49 44 if (file && contains(file, file_len, boundary)) return -2; 50 135 for (i = 0; i < fields->count; i++) { 51 92 ygst_buf_append_str(out, "--"); 52 92 ygst_buf_append_str(out, boundary); 53 92 ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\""); 54 92 ygst_buf_append_str(out, fields->items[i].name); 55 92 ygst_buf_append_str(out, "\"\r\n\r\n"); 56 92 ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : ""); 57 92 ygst_buf_append_str(out, "\r\n"); 58 } 59 43 ygst_buf_append_str(out, "--"); 60 43 ygst_buf_append_str(out, boundary); 61 43 ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\""); 62 43 ygst_buf_append_str(out, file_field); 63 43 ygst_buf_append_str(out, "\"; filename=\""); 64 43 ygst_buf_append_str(out, filename); 65 43 ygst_buf_append_str(out, "\"\r\nContent-Type: "); 66 43 ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream"); 67 43 ygst_buf_append_str(out, "\r\n\r\n"); 68 43 if (file_len) ygst_buf_append(out, file, file_len); 69 43 ygst_buf_append_str(out, "\r\n--"); 70 43 ygst_buf_append_str(out, boundary); 71 43 ygst_buf_append_str(out, "--\r\n"); 72 43 return out->oom ? -1 : 0; 73 } 74 75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */ 76 63const char *ygst_audio_ext(const char *audio_type) { 77 static const char *const KNOWN[] = {"wav", "mp3", "ogg", "amr", "flv", "opus", "aac", 78 "spx", "speex", "silk", "slk", "m4a", "pcm"}; 79 char t[16]; 80 size_t i; 81 63 if (ygst_is_blank(audio_type)) return "wav"; 82 61 ygst_trim_copy(audio_type, t, sizeof t); 83 61 if (t[0] == '.') memmove(t, t + 1, strlen(t)); 84 118 for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) { 85 115 if (ygst_strcaseeq(t, KNOWN[i])) return strcmp(KNOWN[i], "pcm") == 0 ? "wav" : KNOWN[i]; 86 } 87 3 if (ygst_strcaseeq(t, "wave")) return "wav"; 88 2 if (ygst_strcaseeq(t, "mpeg")) return "mp3"; 89 1 return "bin"; 90 } 91 92 52const char *ygst_audio_content_type(const char *ext) { 93 52 if (!ext) return "application/octet-stream"; 94 51 if (ygst_strcaseeq(ext, "wav")) return "audio/wav"; 95 9 if (ygst_strcaseeq(ext, "mp3")) return "audio/mpeg"; 96 8 if (ygst_strcaseeq(ext, "ogg") || ygst_strcaseeq(ext, "spx") || ygst_strcaseeq(ext, "speex")) return "audio/ogg"; 97 6 if (ygst_strcaseeq(ext, "opus")) return "audio/opus"; 98 5 if (ygst_strcaseeq(ext, "amr")) return "audio/amr"; 99 4 if (ygst_strcaseeq(ext, "aac")) return "audio/aac"; 100 3 if (ygst_strcaseeq(ext, "m4a")) return "audio/mp4"; 101 2 if (ygst_strcaseeq(ext, "flv")) return "video/x-flv"; 102 1 return "application/octet-stream"; 103 } 104 105 4int ygst_audio_type_is_pcm(const char *audio_type) { 106 char t[16]; 107 4 if (ygst_is_blank(audio_type)) return 1; 108 3 ygst_trim_copy(audio_type, t, sizeof t); 109 6 return ygst_strcaseeq(t, "wav") || ygst_strcaseeq(t, "pcm") || ygst_strcaseeq(t, "wave") || 110 6 ygst_strcaseeq(t, ".wav") || ygst_strcaseeq(t, ".pcm"); 111 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * coreType resolution and the mapping from KYTestConfig properties to the form fields of 6 * POST /{coreType}. The table mirrors DESIGN 6.1 so that iOS and Android send the same fields. 7 */ 8 #include "ygst_core.h" 9 10 #include <math.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 15 /* KYTestType in declaration order (KYTestConfig.h). NULL: Shengtong publishes no string. */ 16 static const char *const KY_TEST_TYPES[] = { 17 "word.eval", /* KYTestType_Word */ 18 "sent.eval", /* KYTestType_Sentence */ 19 "para.eval", /* KYTestType_Paragraph */ 20 "open.eval", /* KYTestType_Open */ 21 "choice.rec", /* KYTestType_Choice */ 22 "asr.rec", /* KYTestType_Asr */ 23 "align.eval", /* KYTestType_Align */ 24 "word.eval.pro", /* KYTestType_Word_Pro */ 25 "sent.eval.pro", /* KYTestType_Sentence_Pro */ 26 "word.eval.cn", /* KYTestType_Word_Cn */ 27 "sent.eval.cn", /* KYTestType_Sentence_Cn */ 28 "para.eval.cn", /* KYTestType_Paragraph_Cn */ 29 "asr.eval", /* KYTestType_AsrEval */ 30 "word.eval.fr", /* KYTestType_Word_fr */ 31 "sent.eval.fr", /* KYTestType_Sentence_fr */ 32 "para.eval.fr", /* KYTestType_Paragraph_fr */ 33 "word.eval.kr", /* KYTestType_Word_kr */ 34 "sent.eval.kr", /* KYTestType_Sentence_kr */ 35 "para.eval.kr", /* KYTestType_Paragraph_kr */ 36 "word.eval.jp", /* KYTestType_Word_jp */ 37 "sent.eval.jp", /* KYTestType_Sentence_jp */ 38 "para.eval.jp", /* KYTestType_Paragraph_jp */ 39 NULL /* KYTestType_Wordspell */ 40 }; 41 42 /* coreTypes the platform evaluates on POST /{coreType} (DESIGN 6.1). */ 43 static const char *const SUPPORTED[] = {"word.eval", "word.eval.pro", "sent.eval", "sent.eval.pro", 44 "para.eval", "word.eval.cn", "sent.eval.cn", "para.eval.cn", 45 "alpha.eval", "pinyin"}; 46 47 /* Shengtong coreTypes that are answered with errId 60003 without network I/O. */ 48 static const char *const UNSUPPORTED[] = {"choice.rec", "open.eval", "asr.rec", "asr.eval", 49 "align.eval", "word.eval.fr", "sent.eval.fr", "para.eval.fr", 50 "word.eval.jp", "sent.eval.jp", "para.eval.jp", "word.eval.kr", 51 "sent.eval.kr", "para.eval.kr"}; 52 53 54const char *ygst_coretype_for_enum(unsigned long kytesttype) { 54 54 if (kytesttype >= sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]) return NULL; 55 51 return KY_TEST_TYPES[kytesttype]; 56 } 57 58 25size_t ygst_coretype_enum_count(void) { 59 25 return sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]; 60 } 61 62 118ygst_ct_status ygst_coretype_check(const char *core_type) { 63 size_t i; 64 118 if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN; 65 894 for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) { 66 859 if (strcmp(core_type, SUPPORTED[i]) == 0) return YGST_CT_SUPPORTED; 67 } 68 279 for (i = 0; i < sizeof UNSUPPORTED / sizeof UNSUPPORTED[0]; i++) { 69 275 if (strcmp(core_type, UNSUPPORTED[i]) == 0) return YGST_CT_UNSUPPORTED; 70 } 71 4 return YGST_CT_UNKNOWN; 72 } 73 74 72ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) { 75 const char *mapped; 76 72 if (!out || cap == 0) return YGST_CT_UNKNOWN; 77 71 out[0] = 0; 78 71 if (!ygst_is_blank(core_type_ns)) { 79 65 ygst_trim_copy(core_type_ns, out, cap); 80 65 return ygst_coretype_check(out); 81 } 82 6 mapped = ygst_coretype_for_enum(kytesttype); 83 6 if (!mapped) return YGST_CT_UNSUPPORTED; 84 4 if (strlen(mapped) >= cap) return YGST_CT_UNKNOWN; 85 3 strcpy(out, mapped); 86 3 return ygst_coretype_check(out); 87 } 88 89 9int ygst_coretype_is_chinese(const char *core_type) { 90 9 return core_type && (ygst_has_suffix_ci(core_type, ".cn") || strcmp(core_type, "pinyin") == 0); 91 } 92 93 196int ygst_coretype_is_paragraph(const char *core_type) { 94 196 return core_type && strncmp(core_type, "para.", 5) == 0; 95 } 96 97 98int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) { 98 98 if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1"); 99 91 return 0; 100 } 101 102 70int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text, 103 const char *ref_pinyin) { 104 70 if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED; 105 65 if (!ygst_is_blank(ref_text)) return 0; 106 7 if (!ygst_is_blank(ref_pinyin) && ygst_coretype_is_chinese(core_type)) return 0; 107 4 return YGST_ERRID_REFTEXT_EMPTY; 108 } 109 110 /* ------------------------------------------------------------------ number text */ 111 112 69static void fix_decimal_point(char *s) { 113 431 for (; *s; s++) { 114 362 if (*s == ',') *s = '.'; 115 } 116 69} 117 118 3static void trim_fraction_zeros(char *s) { 119 3 char *dot = strchr(s, '.'); 120 char *end; 121 3 if (!dot) return; 122 3 end = s + strlen(s); 123 3 while (end > dot + 1 && end[-1] == '0') end--; 124 3 if (end == dot + 1) end = dot; 125 3 *end = 0; 126 } 127 128 28int ygst_fmt_double(double v, char *out, size_t cap) { 129 char tmp[64]; 130 int p; 131 28 if (!out || cap == 0) return -1; 132 27 out[0] = 0; 133 27 if (v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308) return -1; /* NaN, inf */ 134 25 if (v == 0) { 135 2 tmp[0] = '0'; 136 2 tmp[1] = 0; 137 23 } else if (fabs(v) < 9.0e15 && v == floor(v)) { 138 11 snprintf(tmp, sizeof tmp, "%.0f", v); 139 12 } else if (fabs(v) >= 9.0e15) { 140 1 snprintf(tmp, sizeof tmp, "%.0f", v); 141 } else { 142 41 for (p = 1; p <= 17; p++) { 143 41 char *end = NULL; 144 double back; 145 41 snprintf(tmp, sizeof tmp, "%.*g", p, v); 146 41 fix_decimal_point(tmp); 147 41 back = strtod(tmp, &end); 148 41 if (end && *end) { 149 /* decimal comma locale: compare through the locale's own separator */ 150 char alt[64]; 151 char *dot; 152 0 strcpy(alt, tmp); 153 0 dot = strchr(alt, '.'); 154 0 if (dot) *dot = ','; 155 0 back = strtod(alt, NULL); 156 } 157 41 if (back == v) break; 158 } 159 11 if (strchr(tmp, 'e') || strchr(tmp, 'E')) { 160 /* plain decimal: enough fraction digits to keep the significant ones */ 161 3 int exp10 = (int)floor(log10(fabs(v))); 162 3 int digits = (p > 17 ? 17 : p) - 1 - exp10; 163 3 if (digits < 0) digits = 0; 164 3 if (digits > 40) digits = 40; 165 3 snprintf(tmp, sizeof tmp, "%.*f", digits, v); 166 3 fix_decimal_point(tmp); 167 3 trim_fraction_zeros(tmp); 168 } 169 } 170 25 fix_decimal_point(tmp); 171 25 if (strcmp(tmp, "-0") == 0) strcpy(tmp, "0"); 172 25 if (strlen(tmp) >= cap) return -1; 173 24 strcpy(out, tmp); 174 24 return 0; 175 } 176 177 200int ygst_field_name_ok(const char *name) { 178 200 if (!name || !name[0]) return 0; 179 1925 for (; *name; name++) { 180 1732 if (*name == '"' || *name == '\r' || *name == '\n') return 0; 181 } 182 193 return 1; 183 } 184 185 /* ------------------------------------------------------------------ mapping */ 186 187 91void ygst_test_params_init(ygst_test_params *p) { 188 91 memset(p, 0, sizeof *p); 189 91} 190 191 963static int set_str(ygst_fields *f, const char *name, const char *v) { 192 963 if (!v || !v[0]) return 0; 193 77 return ygst_fields_set(f, name, v); 194 } 195 196 672static int set_flag(ygst_fields *f, const char *name, int on) { 197 672 return on ? ygst_fields_set(f, name, "1") : 0; 198 } 199 200 481static int set_num(ygst_fields *f, const char *name, double v) { 201 char tmp[64]; 202 481 if (v == 0 || ygst_fmt_double(v, tmp, sizeof tmp) != 0) return 0; 203 12 return ygst_fields_set(f, name, tmp); 204 } 205 206 3static int set_int(ygst_fields *f, const char *name, long v) { 207 char tmp[32]; 208 3 snprintf(tmp, sizeof tmp, "%ld", v); 209 3 return ygst_fields_set(f, name, tmp); 210 } 211 212 96int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) { 213 static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"}; 214 96 int rc = 0; 215 96 rc |= set_str(out, "refText", p->ref_text); 216 96 rc |= set_str(out, "refPinyin", p->ref_pinyin); 217 96 if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group); 218 96 rc |= set_num(out, "scale", p->scale); 219 96 rc |= set_num(out, "precision", p->precision); 220 96 rc |= set_num(out, "slack", p->slack); 221 96 rc |= set_flag(out, "paragraph_need_word_score", 222 96 p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type)); 223 96 rc |= set_flag(out, "phoneme_output", p->phoneme_output); 224 96 rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url); 225 96 if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]); 226 96 rc |= set_str(out, "dict_dialect", p->dict_dialect); 227 96 rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json); 228 96 rc |= set_str(out, "customized_pron", p->customized_pron_json); 229 96 if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis); 230 96 rc |= set_flag(out, "output_rawtext", p->output_rawtext); 231 96 rc |= set_flag(out, "punctuate", p->punctuate); 232 96 rc |= set_num(out, "itn", p->itn); 233 96 rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable); 234 /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */ 235 96 rc |= set_flag(out, "vad_detction", p->vad_detection); 236 96 rc |= set_str(out, "keywords", p->keywords); 237 96 rc |= set_str(out, "keypoints", p->keypoints_json); 238 96 rc |= set_num(out, "keypoints_weight", p->keypoints_weight); 239 96 rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json); 240 96 rc |= set_str(out, "negativeReftext", p->negative_reftext); 241 96 if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home"); 242 96 if (p->q_type != 0) rc |= set_int(out, "qType", p->q_type); 243 /* customParams in the order of the Android RecordSetting newParams: a name that is already 244 * mapped keeps its position and takes the new value; request comes last */ 245 96 if (p->custom_params) { 246 size_t i; 247 67 for (i = 0; i < p->custom_params->count; i++) { 248 9 const ygst_field *cp = &p->custom_params->items[i]; 249 9 if (!ygst_field_name_ok(cp->name) || !cp->value || !cp->value[0]) continue; 250 6 rc |= ygst_fields_set(out, cp->name, cp->value); 251 } 252 } 253 96 rc |= set_str(out, "request", p->request_json); 254 /* customParams cannot switch the paragraph word details off */ 255 96 rc |= ygst_fields_apply_coretype_rules(out, p->core_type); 256 96 return (rc != 0 || out->oom) ? -1 : 0; 257 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Realtime feedback (2.1.0, DESIGN 14.3, CONTRACT 5.2 and 5.5): the compat WebSocket round of one 6 * evaluation. Handshake URL and its signature, parameter frame, classification of platform 7 * frames, the words dedupe of the reconnect rule and the round controller that decides 8 * connecting, streaming, ending, falling back to REST before end and reconnecting after end. 9 * 2.1.1: the same round streams every recording by default; the parameter frame asks for reading 10 * progress only with realtime_feedback. 11 */ 12 #include "ygst_core.h" 13 14 #include <stdio.h> 15 #include <stdlib.h> 16 #include <string.h> 17 18 /* ------------------------------------------------------------------ URL */ 19 20 258int ygst_url_encode(ygst_buf *out, const char *s) { 21 static const char H[] = "0123456789ABCDEF"; 22 258 const unsigned char *p = (const unsigned char *)(s ? s : ""); 23 4251 for (; *p; p++) { 24 3993 unsigned char c = *p; 25 3993 if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || 26 88 c == '_' || c == '~') { 27 3906 ygst_buf_append_char(out, (char)c); 28 } else { 29 char e[3]; 30 87 e[0] = '%'; 31 87 e[1] = H[c >> 4]; 32 87 e[2] = H[c & 15]; 33 87 ygst_buf_append(out, e, 3); 34 } 35 } 36 258 ygst_buf_cstr(out); 37 258 return out->oom ? -1 : 0; 38 } 39 40 69static int starts_ci(const char *s, const char *prefix) { 41 394 while (*prefix) { 42 362 char a = *s; 43 362 if (a >= 'A' && a <= 'Z') a = (char)(a + 32); 44 362 if (a != *prefix) return 0; 45 325 s++; 46 325 prefix++; 47 } 48 32 return 1; 49 } 50 51 36int ygst_ws_base_url(const char *rest_base, char *out, size_t cap) { 52 const char *scheme; 53 const char *rest; 54 size_t n; 55 int w; 56 36 if (!out || cap == 0) return -1; 57 35 out[0] = 0; 58 35 if (!rest_base) return -1; 59 34 if (starts_ci(rest_base, "https://")) { 60 6 scheme = "wss://"; 61 6 rest = rest_base + 8; 62 28 } else if (starts_ci(rest_base, "http://")) { 63 24 scheme = "ws://"; 64 24 rest = rest_base + 7; 65 4 } else if (starts_ci(rest_base, "wss://")) { 66 1 scheme = "wss://"; 67 1 rest = rest_base + 6; 68 3 } else if (starts_ci(rest_base, "ws://")) { 69 1 scheme = "ws://"; 70 1 rest = rest_base + 5; 71 } else { 72 2 return -1; 73 } 74 32 n = strlen(rest); 75 36 while (n > 0 && rest[n - 1] == '/') n--; 76 32 if (n == 0 || rest[0] == '/') return -1; 77 30 w = snprintf(out, cap, "%s%.*s", scheme, (int)n, rest); 78 30 if (w < 0 || (size_t)w >= cap) { 79 1 out[0] = 0; 80 1 return -1; 81 } 82 29 return 0; 83 } 84 85 /* Query parameters the handshake sets itself. */ 86 9static int own_query_name(const char *n) { 87 8 return strcmp(n, "appKey") == 0 || strcmp(n, "timestamp") == 0 || strcmp(n, "nonce") == 0 || 88 17 strcmp(n, "idempotencyKey") == 0 || strcmp(n, "signature") == 0; 89 } 90 91 31int ygst_ws_handshake_url(const char *rest_base, const char *core_type, const char *app_key, const char *secret, 92 int64_t timestamp_sec, const char *nonce, const char *idempotency_key, 93 const ygst_fields *extra, ygst_buf *out) { 94 char base[1024]; 95 char ts[32]; 96 char sig[45]; 97 ygst_fields q; 98 size_t i; 99 31 int rc = -1; 100 31 if (!out || !core_type || !core_type[0] || ygst_is_blank(app_key) || !secret || !secret[0] || !nonce || 101 27 !nonce[0] || !ygst_idempotency_key_ok(idempotency_key)) { 102 6 return -1; 103 } 104 25 if (ygst_ws_base_url(rest_base, base, sizeof base) != 0) return -1; 105 24 snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec); 106 24 ygst_fields_init(&q); 107 24 ygst_fields_set(&q, "appKey", app_key); 108 24 ygst_fields_set(&q, "timestamp", ts); 109 24 ygst_fields_set(&q, "nonce", nonce); 110 24 ygst_fields_set(&q, "idempotencyKey", idempotency_key); 111 24 if (extra) { 112 33 for (i = 0; i < extra->count; i++) { 113 10 const ygst_field *f = &extra->items[i]; 114 10 if (!f->name || !f->name[0] || !f->value || !f->value[0] || own_query_name(f->name)) continue; 115 7 ygst_fields_set(&q, f->name, f->value); 116 } 117 } 118 24 if (q.oom || ygst_sign(&q, secret, sig) != 0) goto done; 119 24 ygst_buf_append_str(out, base); 120 24 ygst_buf_append_char(out, '/'); 121 24 ygst_url_encode(out, core_type); 122 127 for (i = 0; i < q.count; i++) { 123 103 ygst_buf_append_char(out, i == 0 ? '?' : '&'); 124 103 ygst_url_encode(out, q.items[i].name); 125 103 ygst_buf_append_char(out, '='); 126 103 ygst_url_encode(out, q.items[i].value); 127 } 128 24 ygst_buf_append_str(out, "&signature="); 129 24 ygst_url_encode(out, sig); 130 24 ygst_buf_cstr(out); 131 24 rc = out->oom ? -1 : 0; 132 24done: 133 24 ygst_fields_free(&q); 134 24 return rc; 135 } 136 137 /* ------------------------------------------------------------------ parameter frame */ 138 139 /* The platform reads these names as commands (cmd, end, data) or the frame sets them itself. */ 140 69static int reserved_frame_name(const char *n, int flags) { 141 68 return strcmp(n, "cmd") == 0 || strcmp(n, "end") == 0 || strcmp(n, "data") == 0 || 142 200 strcmp(n, "realtime_feedback") == 0 || strcmp(n, "idempotencyKey") == 0 || 143 63 ((flags & YGST_WSP_TIMING) && strcmp(n, "timing") == 0); 144 } 145 146 32int ygst_ws_param_frame(const ygst_fields *fields, const char *idempotency_key, int flags, ygst_buf *out) { 147 size_t i; 148 32 int skipped = 0; 149 32 if (!fields || !out || fields->oom || !ygst_idempotency_key_ok(idempotency_key)) return -1; 150 30 ygst_buf_append_char(out, '{'); 151 99 for (i = 0; i < fields->count; i++) { 152 69 const ygst_field *f = &fields->items[i]; 153 69 if (!f->name || reserved_frame_name(f->name, flags)) { 154 7 skipped++; 155 7 continue; 156 } 157 62 ygst_json_escape(out, f->name); 158 62 ygst_buf_append_char(out, ':'); 159 62 ygst_json_escape(out, f->value ? f->value : ""); 160 62 ygst_buf_append_char(out, ','); 161 } 162 /* 2.1.1: progress frames only when the app asked for them, the timing object only for the log */ 163 30 if (flags & YGST_WSP_REALTIME_FEEDBACK) ygst_buf_append_str(out, "\"realtime_feedback\":true,"); 164 30 if (flags & YGST_WSP_TIMING) ygst_buf_append_str(out, "\"timing\":1,"); 165 30 ygst_buf_append_str(out, "\"idempotencyKey\":"); 166 30 ygst_json_escape(out, idempotency_key); 167 30 ygst_buf_append_char(out, '}'); 168 30 ygst_buf_cstr(out); 169 30 return out->oom ? -1 : skipped; 170 } 171 172 /* ------------------------------------------------------------------ frames */ 173 174 /* An integer member, also when a gateway quoted it. 1 found, 0 absent or not an integer. */ 175 1100static int int_member(ygst_span obj, const char *key, long long *out) { 176 ygst_span v; 177 1100 if (ygst_json_member(obj, key, &v) != 1) return 0; 178 1055 if (ygst_json_type(v) == 's') { 179 ygst_buf b; 180 6 int ok = 0; 181 6 ygst_buf_init(&b); 182 6 if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 20) { 183 6 char *end = NULL; 184 6 long long x = strtoll(ygst_buf_cstr(&b), &end, 10); 185 6 if (end && *end == 0) { 186 6 *out = x; 187 6 ok = 1; 188 } 189 } 190 6 ygst_buf_free(&b); 191 6 return ok; 192 } 193 1049 return ygst_json_int(v, out) == 0; 194 } 195 196 8591static int bool_member(ygst_span obj, const char *key) { 197 ygst_span v; 198 8591 int b = 0; 199 8591 if (ygst_json_member(obj, key, &v) != 1 || ygst_json_bool(v, &b) != 0) return 0; 200 8565 return b; 201 } 202 203 614static int count_value(long long v) { 204 614 if (v < 0) return -1; 205 614 return v > 2147483647LL ? 2147483647 : (int)v; 206 } 207 208 /* The event name when it is a short JSON string, else "". */ 209 140static void event_name(ygst_span ev, char *out, size_t cap) { 210 ygst_buf b; 211 140 out[0] = 0; 212 140 if (ygst_json_type(ev) != 's') return; 213 139 ygst_buf_init(&b); 214 139 if (ygst_json_string(ev, &b) == 0 && b.len < cap) memcpy(out, ygst_buf_cstr(&b), b.len + 1); 215 139 ygst_buf_free(&b); 216 } 217 218 209static void parse_progress(ygst_ws_frame *f) { 219 ygst_span r, list, item; 220 long long v; 221 209 size_t pos = 0; 222 209 int read = 0, matched = 0, total = 0; 223 209 if (ygst_json_member(f->root, "result", &r) != 1 || ygst_json_type(r) != 'o') return; 224 208 f->result = r; 225 208 if (int_member(r, "bytes", &v) && v >= 0) f->bytes = v; 226 208 f->read_units = int_member(r, "readUnits", &v) ? count_value(v) : -1; 227 208 f->matched_units = int_member(r, "matchedUnits", &v) ? count_value(v) : -1; 228 208 f->total_units = int_member(r, "totalUnits", &v) ? count_value(v) : -1; 229 /* every unit with its state */ 230 208 if (ygst_json_member(r, "progress", &list) == 1 && ygst_json_type(list) == 'a') { 231 197 f->has_progress = 1; 232 4479 while (ygst_json_array_next(list, &pos, &item) == 1) { 233 4282 total++; 234 4282 if (ygst_json_type(item) != 'o') continue; 235 4281 if (bool_member(item, "read")) read++; 236 4281 if (bool_member(item, "matched")) matched++; 237 } 238 197 if (f->read_units < 0) f->read_units = read; 239 197 if (f->matched_units < 0) f->matched_units = matched; 240 197 if (f->total_units < 0) f->total_units = total; 241 } 242 /* Shengtong items of the units read since the previous frame */ 243 208 if (ygst_json_member(r, "words", &list) == 1 && ygst_json_type(list) == 'a') { 244 175 f->has_words = 1; 245 175 f->words = list; 246 175 pos = 0; 247 506 while (ygst_json_array_next(list, &pos, &item) == 1) f->word_count++; 248 } 249 } 250 251 29static void parse_final(ygst_ws_frame *f) { 252 ygst_span r; 253 29 if (ygst_json_member(f->root, "result", &r) == 1) f->result = r; 254 29 f->replayed = bool_member(f->root, "replayed"); 255 29} 256 257 382int ygst_ws_frame_parse(const char *text, size_t n, ygst_ws_frame *f) { 258 ygst_span root, v; 259 382 if (!f) return -1; 260 381 memset(f, 0, sizeof *f); 261 381 f->kind = YGST_WSF_INVALID; 262 381 f->bytes = -1; 263 381 f->read_units = -1; 264 381 f->matched_units = -1; 265 381 f->total_units = -1; 266 381 if (!text || ygst_json_parse(text, n, &root) != 0 || ygst_json_type(root) != 'o') return -1; 267 377 f->root = root; 268 377 if (ygst_json_member(root, "event", &v) == 1) { 269 char ev[16]; 270 140 event_name(v, ev, sizeof ev); 271 140 if (strcmp(ev, "connected") == 0) { 272 58 f->kind = YGST_WSF_CONNECTED; 273 82 } else if (strcmp(ev, "started") == 0) { 274 58 f->kind = YGST_WSF_STARTED; 275 24 } else if (strcmp(ev, "pong") == 0) { 276 2 f->kind = YGST_WSF_PONG; 277 22 } else if (strcmp(ev, "error") == 0) { 278 18 long long code = 0; 279 18 f->kind = YGST_WSF_ERROR; 280 18 if (int_member(root, "code", &code) && code > 0) f->code = count_value(code); 281 4 } else if (strcmp(ev, "result") == 0) { 282 2 f->kind = YGST_WSF_FINAL; 283 2 parse_final(f); 284 } else { 285 2 f->kind = YGST_WSF_OTHER; 286 } 287 140 return 0; 288 } 289 237 if (ygst_json_member(root, "eof", &v) == 1) { 290 234 long long eof = -1; 291 234 if (int_member(root, "eof", &eof) && eof == 0) { 292 209 f->kind = YGST_WSF_PROGRESS; 293 209 parse_progress(f); 294 } else { 295 25 f->kind = YGST_WSF_FINAL; 296 25 parse_final(f); 297 } 298 234 return 0; 299 } 300 3 if (ygst_json_member(root, "result", NULL) == 1 || ygst_json_member(root, "recordId", NULL) == 1) { 301 2 f->kind = YGST_WSF_FINAL; 302 2 parse_final(f); 303 2 return 0; 304 } 305 1 f->kind = YGST_WSF_OTHER; 306 1 return 0; 307 } 308 309 16int ygst_ws_classify_error(ygst_span frame, ygst_classification *c) { 310 static const char *const KEYS[] = {"message", "msg", "error", NULL}; 311 const char *const *k; 312 16 long long code = 0; 313 16 c->success = 0; 314 16 c->http_status = 0; 315 16 c->local_code = 0; 316 16 c->biz_code = 0; 317 16 c->has_audio_url = 0; 318 16 c->has_record_id = 0; 319 16 c->record_id_raw.p = NULL; 320 16 c->record_id_raw.n = 0; 321 16 c->root.p = NULL; 322 16 c->root.n = 0; 323 16 c->result.p = NULL; 324 16 c->result.n = 0; 325 16 ygst_buf_reset(&c->record_id); 326 16 ygst_buf_reset(&c->audio_url); 327 16 ygst_buf_reset(&c->message); 328 16 if (ygst_json_type(frame) == 'o') { 329 16 if (int_member(frame, "code", &code) && code > 0) c->biz_code = count_value(code); 330 23 for (k = KEYS; *k; k++) { 331 ygst_span v; 332 21 if (ygst_json_member(frame, *k, &v) == 1 && ygst_json_type(v) == 's') { 333 14 ygst_json_string(v, &c->message); 334 14 break; 335 } 336 } 337 } 338 16 if (!c->message.len) ygst_buf_append_str(&c->message, "error frame without a message"); 339 16 ygst_buf_cstr(&c->message); 340 16 ygst_buf_cstr(&c->record_id); 341 16 ygst_buf_cstr(&c->audio_url); 342 /* without a business code the platform failed while evaluating: replay like a 5xx */ 343 16 c->retryable = c->biz_code ? ygst_is_retryable(0, c->biz_code, 0) : 1; 344 16 c->err_id = ygst_errid_for(0, c->biz_code, 0, c->retryable); 345 16 return c->message.oom ? -1 : 0; 346 } 347 348 /* ------------------------------------------------------------------ words dedupe */ 349 350 47void ygst_words_dedupe_init(ygst_words_dedupe *d) { 351 47 memset(d, 0, sizeof *d); 352 47} 353 354 103int ygst_words_dedupe_skip(ygst_words_dedupe *d, int read_units, int word_count) { 355 long start; 356 long skip; 357 103 if (word_count <= 0) return 0; 358 102 if (read_units < 0) read_units = d->delivered + word_count; /* position unknown: the next units */ 359 102 start = (long)read_units - word_count; 360 102 skip = d->delivered > start ? (long)d->delivered - start : 0; 361 102 if (skip >= word_count) return word_count; /* every item was delivered already */ 362 78 if (read_units > d->delivered) d->delivered = read_units; 363 78 return (int)skip; 364 } 365 366 /* ------------------------------------------------------------------ round controller */ 367 368 45void ygst_rt_init(ygst_rt *rt) { 369 45 memset(rt, 0, sizeof *rt); 370 45 ygst_retry_policy_default(&rt->policy); 371 45 rt->connect_timeout_ms = 20000; 372 45 rt->attempt_timeout_ms = 60000; 373 45 rt->ping_interval_ms = 15000; 374 45 rt->pong_timeout_ms = 30000; 375 45 rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES; 376 45 rt->state = YGST_RT_IDLE; 377 45 rt->frame.kind = YGST_WSF_INVALID; 378 45 ygst_words_dedupe_init(&rt->dedupe); 379 45 ygst_classification_init(&rt->cls); 380 45 ygst_buf_init(&rt->failure); 381 45} 382 383 45void ygst_rt_free(ygst_rt *rt) { 384 45 ygst_classification_free(&rt->cls); 385 45 ygst_buf_free(&rt->failure); 386 45} 387 388 1399static ygst_rt_actions no_actions(void) { 389 ygst_rt_actions a; 390 1399 memset(&a, 0, sizeof a); 391 1399 return a; 392 } 393 394 914static int on_connection(ygst_rt_state s) { 395 914 return s == YGST_RT_CONNECTING || s == YGST_RT_STARTING || s == YGST_RT_STREAMING || s == YGST_RT_ENDING; 396 } 397 398 186static void clear_timers(ygst_rt *rt) { 399 186 rt->deadline_ms = 0; 400 186 rt->next_ping_ms = 0; 401 186 rt->pong_deadline_ms = 0; 402 186} 403 404 36static void note_failure(ygst_rt *rt, const char *why) { 405 36 ygst_buf_reset(&rt->failure); 406 36 ygst_buf_append_str(&rt->failure, why ? why : ""); 407 36 ygst_buf_cstr(&rt->failure); 408 36} 409 410 59static void open_connection(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 411 59 a->connect = 1; 412 59 rt->state = YGST_RT_CONNECTING; 413 59 rt->connections++; 414 59 rt->sent = 0; 415 59 clear_timers(rt); 416 59 rt->deadline_ms = now + rt->connect_timeout_ms; 417 59} 418 419 249static void send_pending_audio(ygst_rt *rt, ygst_rt_actions *a) { 420 249 if (rt->total > rt->sent) { 421 223 a->audio_from = rt->sent; 422 223 a->audio_to = rt->total; 423 223 rt->sent = rt->total; 424 } 425 249} 426 427 31static void start_streaming(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 428 31 rt->state = YGST_RT_STREAMING; 429 31 clear_timers(rt); 430 31 if (rt->ping_interval_ms > 0) rt->next_ping_ms = now + rt->ping_interval_ms; 431 31 send_pending_audio(rt, a); 432 31} 433 434 /* All audio from the current offset, then end. The first end starts the attempts of the REST 435 * call controller; a reconnect only sends it again. */ 436 42static void send_end(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 437 42 send_pending_audio(rt, a); 438 42 a->send_end = 1; 439 42 if (!rt->end_sent) { 440 30 rt->end_sent = 1; 441 30 ygst_call_init(&rt->call, &rt->policy, rt->seed, now); 442 } 443 42 rt->state = YGST_RT_ENDING; 444 /* no heartbeat while the platform evaluates: the result timeout bounds the wait, as a REST 445 * attempt's read timeout does */ 446 42 clear_timers(rt); 447 42 rt->deadline_ms = now + rt->attempt_timeout_ms; 448 42} 449 450 /* Before end: the platform never evaluated this round, REST evaluates it with the same tokenId. */ 451 10static void fall_back(ygst_rt *rt, ygst_rt_actions *a, const char *why) { 452 10 a->close = 1; 453 10 rt->state = YGST_RT_FALLBACK; 454 10 clear_timers(rt); 455 10 note_failure(rt, why); 456 10 if (rt->end_requested) a->fallback = 1; 457 10} 458 459 /* After end: rt->cls holds the outcome of this attempt; the REST call controller decides. */ 460 26static void attempt_failed(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 461 26 ygst_action next = ygst_call_next(&rt->call, &rt->cls, -1, now); 462 26 a->close = 1; 463 26 clear_timers(rt); 464 26 note_failure(rt, ygst_buf_cstr(&rt->cls.message)); 465 26 if (next.kind == YGST_ACT_RETRY) { 466 16 rt->state = YGST_RT_BACKOFF; 467 16 rt->reconnect_at_ms = now + next.delay_ms; 468 16 a->retry = 1; 469 16 a->delay_ms = next.delay_ms; 470 16 return; 471 } 472 10 rt->state = YGST_RT_DONE; 473 10 a->fail = 1; 474 10 a->err_id = next.kind == YGST_ACT_FAIL ? next.err_id : YGST_LOCAL_PROTOCOL; 475 } 476 477 24static void connection_failed(ygst_rt *rt, ygst_rt_actions *a, int local_code, int http_status, const char *message, 478 int64_t now) { 479 ygst_outcome o; 480 24 if (!rt->end_sent) { 481 8 fall_back(rt, a, message); 482 8 return; 483 } 484 16 memset(&o, 0, sizeof o); 485 16 o.local_code = local_code; 486 16 o.http_status = local_code ? 0 : http_status; 487 16 o.retry_after_ms = -1; 488 16 o.local_message = message; 489 16 ygst_classify(&o, &rt->cls); 490 16 if (!local_code && http_status > 0 && message && message[0]) { 491 /* a rejected handshake has no body: say so instead of repeating the status */ 492 2 ygst_buf_reset(&rt->cls.message); 493 2 ygst_buf_append_str(&rt->cls.message, message); 494 2 ygst_buf_cstr(&rt->cls.message); 495 } 496 16 attempt_failed(rt, a, now); 497 } 498 499 45ygst_rt_actions ygst_rt_start(ygst_rt *rt, int64_t now_ms) { 500 45 ygst_rt_actions a = no_actions(); 501 45 if (rt->state != YGST_RT_IDLE) return a; 502 44 if (rt->max_frame_bytes == 0) rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES; 503 44 open_connection(rt, &a, now_ms); 504 44 return a; 505 } 506 507 333ygst_rt_actions ygst_rt_on_audio(ygst_rt *rt, size_t total_bytes, int64_t now_ms) { 508 333 ygst_rt_actions a = no_actions(); 509 (void)now_ms; 510 333 if (rt->end_requested) return a; /* the round audio is complete */ 511 332 if (total_bytes > rt->total) rt->total = total_bytes; 512 332 if (rt->state == YGST_RT_STREAMING) send_pending_audio(rt, &a); 513 332 return a; 514 } 515 516 39ygst_rt_actions ygst_rt_on_stop(ygst_rt *rt, size_t total_bytes, int64_t now_ms) { 517 39 ygst_rt_actions a = no_actions(); 518 39 if (rt->end_requested || rt->state == YGST_RT_DONE) return a; 519 37 if (total_bytes > rt->total) rt->total = total_bytes; 520 37 rt->end_requested = 1; 521 37 switch (rt->state) { 522 6 case YGST_RT_IDLE: 523 case YGST_RT_FALLBACK: 524 6 rt->state = YGST_RT_FALLBACK; 525 6 a.fallback = 1; 526 6 break; 527 23 case YGST_RT_STREAMING: 528 23 send_end(rt, &a, now_ms); 529 23 break; 530 8 default: 531 8 break; /* CONNECTING, STARTING: end goes out right after "started" */ 532 } 533 37 return a; 534 } 535 536 253ygst_rt_actions ygst_rt_on_text(ygst_rt *rt, const char *text, size_t n, int64_t now_ms) { 537 253 ygst_rt_actions a = no_actions(); 538 253 if (!on_connection(rt->state)) return a; 539 249 if (ygst_ws_frame_parse(text, n, &rt->frame) != 0) return a; 540 248 switch (rt->frame.kind) { 541 52 case YGST_WSF_CONNECTED: 542 52 if (rt->state == YGST_RT_CONNECTING) { 543 51 a.send_params = 1; 544 51 rt->state = YGST_RT_STARTING; 545 } 546 52 break; 547 51 case YGST_WSF_STARTED: 548 51 if (rt->state == YGST_RT_STARTING) { 549 50 if (rt->end_sent || rt->end_requested) { 550 19 send_end(rt, &a, now_ms); 551 } else { 552 31 start_streaming(rt, &a, now_ms); 553 } 554 } 555 51 break; 556 116 case YGST_WSF_PROGRESS: 557 /* delivered only with new words; empty words and bytes-only frames are not */ 558 116 if (rt->frame.word_count > 0) { 559 93 int skip = ygst_words_dedupe_skip(&rt->dedupe, rt->frame.read_units, rt->frame.word_count); 560 93 if (skip < rt->frame.word_count) { 561 71 a.progress = 1; 562 71 a.words_skip = skip; 563 } else { 564 22 a.progress_held = 1; 565 } 566 } 567 116 break; 568 18 case YGST_WSF_FINAL: 569 18 if (rt->state != YGST_RT_ENDING) { 570 1 if (!rt->end_sent) fall_back(rt, &a, "result frame before end"); 571 1 break; 572 } 573 { 574 ygst_outcome o; 575 17 memset(&o, 0, sizeof o); 576 17 o.http_status = 200; 577 17 o.body = text; 578 17 o.body_len = n; 579 17 o.retry_after_ms = -1; 580 17 o.replayed = rt->frame.replayed; 581 17 ygst_classify(&o, &rt->cls); 582 } 583 17 if (rt->cls.success) { 584 16 (void)ygst_call_next(&rt->call, &rt->cls, -1, now_ms); 585 16 rt->state = YGST_RT_DONE; 586 16 clear_timers(rt); 587 16 a.close = 1; 588 16 a.final = 1; 589 } else { 590 1 attempt_failed(rt, &a, now_ms); 591 } 592 17 break; 593 10 case YGST_WSF_ERROR: 594 10 ygst_ws_classify_error(rt->frame.root, &rt->cls); 595 10 if (!rt->end_sent) { 596 1 fall_back(rt, &a, ygst_buf_cstr(&rt->cls.message)); 597 } else { 598 9 attempt_failed(rt, &a, now_ms); 599 } 600 10 break; 601 1 default: 602 1 break; /* pong, other events */ 603 } 604 248 return a; 605 } 606 607 3ygst_rt_actions ygst_rt_on_pong(ygst_rt *rt, int64_t now_ms) { 608 3 ygst_rt_actions a = no_actions(); 609 3 if (rt->state == YGST_RT_STREAMING && rt->pong_deadline_ms) { 610 1 rt->pong_deadline_ms = 0; 611 1 if (rt->ping_interval_ms > 0) rt->next_ping_ms = now_ms + rt->ping_interval_ms; 612 } 613 3 return a; 614 } 615 616 20ygst_rt_actions ygst_rt_on_failure(ygst_rt *rt, int local_code, int http_status, const char *message, 617 int64_t now_ms) { 618 20 ygst_rt_actions a = no_actions(); 619 20 if (!on_connection(rt->state)) return a; 620 18 connection_failed(rt, &a, local_code, http_status, message, now_ms); 621 18 return a; 622 } 623 624 706ygst_rt_actions ygst_rt_on_tick(ygst_rt *rt, int64_t now_ms) { 625 706 ygst_rt_actions a = no_actions(); 626 char msg[96]; 627 706 if (rt->state == YGST_RT_BACKOFF) { 628 77 if (now_ms >= rt->reconnect_at_ms) open_connection(rt, &a, now_ms); 629 77 return a; 630 } 631 629 if (!on_connection(rt->state)) return a; 632 463 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 459 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 457 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 457 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 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Retry policy (DESIGN 2.3), Shengtong errId mapping (DESIGN 6.2), response classification and 6 * the per-call controller that decides between delivering, retrying and failing. The Objective-C 7 * layer executes the decisions with NSURLSession; the Linux tests execute them with sockets. 8 */ 9 #include "ygst_core.h" 10 11 #include <math.h> 12 #include <stdio.h> 13 #include <stdlib.h> 14 #include <string.h> 15 16 /* ------------------------------------------------------------------ table lookups */ 17 18 273static const ygst_error_entry *find_in(const ygst_error_entry *t, size_t n, int code) { 19 size_t i; 20 3789 for (i = 0; i < n; i++) { 21 3775 if (t[i].code == code) return &t[i]; 22 } 23 14 return NULL; 24 } 25 26 196const ygst_error_entry *ygst_error_find(int code) { 27 196 return find_in(ygst_table_errors, ygst_table_errors_count, code); 28 } 29 30 9const ygst_error_entry *ygst_warning_find(int code) { 31 9 return find_in(ygst_table_warnings, ygst_table_warnings_count, code); 32 } 33 34 68const ygst_error_entry *ygst_local_find(int code) { 35 68 return find_in(ygst_table_local, ygst_table_local_count, code); 36 } 37 38 34const char *ygst_compat_message(int err_id) { 39 size_t i; 40 158 for (i = 0; i < ygst_table_compat_count; i++) { 41 154 if (ygst_table_compat[i].err_id == err_id) return ygst_table_compat[i].message; 42 } 43 4 return NULL; 44 } 45 46 46int ygst_http_retryable(int status) { 47 size_t i; 48 291 for (i = 0; i < ygst_table_retryable_http_count; i++) { 49 267 if (ygst_table_retryable_http[i] == status) return 1; 50 } 51 24 return 0; 52 } 53 54 /* ------------------------------------------------------------------ RNG (splitmix64) */ 55 56 87void ygst_rng_seed(ygst_rng *r, uint64_t seed) { 57 87 r->s = seed; 58 87} 59 60 1074uint64_t ygst_rng_next(ygst_rng *r) { 61 1074 uint64_t z = (r->s += 0x9E3779B97F4A7C15ull); 62 1074 z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; 63 1074 z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; 64 1074 return z ^ (z >> 31); 65 } 66 67 1072double ygst_rng_uniform(ygst_rng *r) { 68 1072 return (double)(ygst_rng_next(r) >> 11) * (1.0 / 9007199254740992.0); 69 } 70 71 /* ------------------------------------------------------------------ policy */ 72 73 113void ygst_retry_policy_default(ygst_retry_policy *p) { 74 113 memset(p, 0, sizeof *p); 75 113 p->max_retries = 2; 76 113 p->initial_delay_ms = 200; 77 113 p->multiplier = 2.0; 78 113 p->max_delay_ms = 4000; 79 113 p->jitter = 0.3; 80 113 p->respect_retry_after = 1; 81 113 p->max_retry_after_ms = 30000; 82 113 p->total_timeout_ms = 300000; 83 113 p->auto_retry = 0; 84 113 p->max_auto_retries = 2; 85 113 p->err_ids[0] = YGST_ERRID_RETRYABLE; 86 113 p->n_err_ids = 1; 87 113} 88 89 28int ygst_retry_policy_add_err_id(ygst_retry_policy *p, long long err_id) { 90 28 if (err_id <= 0 || err_id > 2147483647LL) return -1; 91 25 if (ygst_retry_policy_has_err_id(p, (int)err_id)) return 0; 92 24 if (p->n_err_ids >= YGST_MAX_ERR_IDS) return -1; 93 17 p->err_ids[p->n_err_ids++] = (int)err_id; 94 17 return 0; 95 } 96 97 40int ygst_retry_policy_has_err_id(const ygst_retry_policy *p, int err_id) { 98 int i; 99 280 for (i = 0; i < p->n_err_ids; i++) { 100 253 if (p->err_ids[i] == err_id) return 1; 101 } 102 27 return 0; 103 } 104 105 81long ygst_backoff_delay_ms(const ygst_retry_policy *p, int n, ygst_rng *rng) { 106 double base; 107 81 double factor = 1.0; 108 double d; 109 81 if (n < 1) n = 1; 110 81 base = (double)p->initial_delay_ms * pow(p->multiplier > 0 ? p->multiplier : 1.0, (double)(n - 1)); 111 81 if (base > p->max_delay_ms) base = p->max_delay_ms; 112 81 if (p->jitter > 0 && rng) factor = 1.0 + (ygst_rng_uniform(rng) * 2.0 - 1.0) * p->jitter; 113 81 d = base * factor; 114 81 if (d < 0) d = 0; 115 81 return (long)(d + 0.5); 116 } 117 118 /* days since 1970-01-01 for a proleptic Gregorian date (H. Hinnant, days_from_civil) */ 119 3static long long days_from_civil(long long y, unsigned m, unsigned d) { 120 long long era; 121 unsigned yoe, doy, doe; 122 3 y -= m <= 2; 123 3 era = (y >= 0 ? y : y - 399) / 400; 124 3 yoe = (unsigned)(y - era * 400); 125 3 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; 126 3 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; 127 3 return era * 146097 + (long long)doe - 719468; 128 } 129 130 15int ygst_parse_retry_after(const char *value, int64_t now_ms, long *out_ms) { 131 static const char *const MON[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", 132 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 133 char v[64]; 134 size_t n; 135 size_t i; 136 15 if (!value || !out_ms) return -1; 137 14 n = ygst_trim_copy(value, v, sizeof v); 138 14 if (n == 0) return -1; 139 30 for (i = 0; i < n && v[i] >= '0' && v[i] <= '9'; i++) { 140 } 141 13 if (i == n) { 142 6 long secs = 0; 143 21 for (i = 0; i < n; i++) { 144 16 if (secs > 100000000L) return -1; 145 15 secs = secs * 10 + (v[i] - '0'); 146 } 147 5 *out_ms = secs * 1000L; 148 5 return 0; 149 } 150 /* IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT" */ 151 7 if (n == 29 && v[3] == ',' && v[4] == ' ' && v[7] == ' ' && v[11] == ' ' && v[16] == ' ' && v[19] == ':' && 152 5 v[22] == ':' && v[25] == ' ' && strcmp(v + 26, "GMT") == 0) { 153 5 unsigned day, mon = 0, hh, mm, ss; 154 long long year; 155 long long t; 156 65 for (i = 0; i < 12; i++) { 157 60 if (strncmp(v + 8, MON[i], 3) == 0) mon = (unsigned)i + 1; 158 } 159 5 if (!mon) return -1; 160 4 day = (unsigned)((v[5] - '0') * 10 + (v[6] - '0')); 161 4 year = (v[12] - '0') * 1000 + (v[13] - '0') * 100 + (v[14] - '0') * 10 + (v[15] - '0'); 162 4 hh = (unsigned)((v[17] - '0') * 10 + (v[18] - '0')); 163 4 mm = (unsigned)((v[20] - '0') * 10 + (v[21] - '0')); 164 4 ss = (unsigned)((v[23] - '0') * 10 + (v[24] - '0')); 165 4 if (day < 1 || day > 31 || hh > 23 || mm > 59 || ss > 60) return -1; 166 3 t = days_from_civil(year, mon, day) * 86400LL + hh * 3600LL + mm * 60LL + ss; 167 3 t = t * 1000LL - now_ms; 168 3 *out_ms = t > 0 ? (long)(t > 2000000000LL ? 2000000000LL : t) : 0; 169 3 return 0; 170 } 171 2 return -1; 172 } 173 174 /* ------------------------------------------------------------------ retry and errId rules */ 175 176 236int ygst_is_retryable(int local_code, int biz_code, int http_status) { 177 const ygst_error_entry *e; 178 236 if (local_code) { 179 50 return local_code == YGST_LOCAL_NETWORK || local_code == YGST_LOCAL_TIMEOUT || 180 local_code == YGST_LOCAL_RESULT_TIMEOUT; 181 } 182 186 if (biz_code) { 183 152 e = ygst_error_find(biz_code); 184 152 if (e) return e->retryable; 185 } 186 38 return ygst_http_retryable(http_status); 187 } 188 189 23int ygst_http_fallback_code(int status) { 190 23 const char *cat = ygst_http_fallback_category(status); 191 23 if (strcmp(cat, "AUTH") == 0) return 40100; 192 21 if (strcmp(cat, "PERMISSION") == 0) return 40300; 193 19 if (strcmp(cat, "NOT_FOUND") == 0) return 40400; 194 17 if (strcmp(cat, "CONFLICT") == 0) return 40900; 195 16 if (strcmp(cat, "RATE_LIMIT") == 0) return 42900; 196 15 if (strcmp(cat, "SERVER") == 0) return status == 501 ? 50010 : 50000; 197 12 if (strcmp(cat, "UPSTREAM") == 0) return 50200; 198 10 if (strcmp(cat, "TIMEOUT") == 0) return YGST_ERRID_RETRYABLE; 199 9 if (strcmp(cat, "INVALID_PARAM") == 0) return 40001; 200 3 return YGST_LOCAL_PROTOCOL; 201 } 202 203 206int ygst_errid_for(int local_code, int biz_code, int http_status, int retryable) { 204 206 if (local_code >= 60001 && local_code <= 60009) return local_code; 205 195 if (retryable) return YGST_ERRID_RETRYABLE; 206 100 if (biz_code) return biz_code; 207 44 if (local_code) return local_code; 208 23 return ygst_http_fallback_code(http_status); 209 } 210 211 /* ------------------------------------------------------------------ classification */ 212 213 211void ygst_classification_init(ygst_classification *c) { 214 211 memset(c, 0, sizeof *c); 215 211 ygst_buf_init(&c->record_id); 216 211 ygst_buf_init(&c->audio_url); 217 211 ygst_buf_init(&c->message); 218 211} 219 220 211void ygst_classification_free(ygst_classification *c) { 221 211 ygst_buf_free(&c->record_id); 222 211 ygst_buf_free(&c->audio_url); 223 211 ygst_buf_free(&c->message); 224 211} 225 226 121static int member_int(ygst_span obj, const char *key, long long *out) { 227 ygst_span v; 228 121 if (ygst_json_member(obj, key, &v) != 1) return 0; 229 98 if (ygst_json_type(v) == 's') { 230 /* some gateways quote numeric codes */ 231 ygst_buf b; 232 1 int ok = 0; 233 1 ygst_buf_init(&b); 234 1 if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 12) { 235 1 char *end = NULL; 236 1 long long x = strtoll(ygst_buf_cstr(&b), &end, 10); 237 1 if (end && *end == 0) { 238 1 *out = x; 239 1 ok = 1; 240 } 241 } 242 1 ygst_buf_free(&b); 243 1 return ok; 244 } 245 97 return ygst_json_int(v, out) == 0; 246 } 247 248 /* First of the keys whose value is a JSON string. */ 249 109static int first_string(ygst_span obj, const char *const *keys, ygst_buf *out) { 250 141 for (; *keys; keys++) { 251 ygst_span v; 252 132 if (ygst_json_member(obj, *keys, &v) == 1 && ygst_json_type(v) == 's') { 253 100 ygst_buf_reset(out); 254 100 return ygst_json_string(v, out) == 0; 255 } 256 } 257 9 return 0; 258 } 259 260 /* "[2001] signature mismatch": the compat engine code in a FastAPI detail, 3 to 6 digits. */ 261 7static int bracket_code(const char *msg) { 262 7 long v = 0; 263 7 int digits = 0; 264 7 if (!msg) return 0; 265 8 while (*msg == ' ' || *msg == '\t' || *msg == '\n' || *msg == '\r') msg++; 266 7 if (*msg != '[') return 0; 267 3 msg++; 268 14 while (*msg >= '0' && *msg <= '9' && digits < 7) { 269 11 v = v * 10 + (*msg - '0'); 270 11 msg++; 271 11 digits++; 272 } 273 3 if (*msg != ']' || digits < 3 || digits > 6) return 0; 274 2 return (int)v; 275 } 276 277 59static void set_message(ygst_buf *m, const char *s) { 278 59 ygst_buf_reset(m); 279 59 ygst_buf_append_str(m, s ? s : ""); 280 59 ygst_buf_cstr(m); 281 59} 282 283 231int ygst_classify(const ygst_outcome *o, ygst_classification *c) { 284 static const char *const MESSAGE_KEYS[] = {"message", "msg", "error", NULL}; 285 ygst_span root; 286 231 int parsed = 0; 287 231 long long code = 0; 288 231 c->success = 0; 289 231 c->http_status = o->http_status; 290 231 c->local_code = o->local_code; 291 231 c->biz_code = 0; 292 231 c->has_audio_url = 0; 293 231 c->has_record_id = 0; 294 231 c->record_id_raw.p = NULL; 295 231 c->record_id_raw.n = 0; 296 231 c->root.p = NULL; 297 231 c->root.n = 0; 298 231 c->result.p = NULL; 299 231 c->result.n = 0; 300 231 ygst_buf_reset(&c->record_id); 301 231 ygst_buf_reset(&c->audio_url); 302 231 ygst_buf_reset(&c->message); 303 304 231 if (o->local_code || o->http_status <= 0) { 305 const ygst_error_entry *e; 306 43 if (!c->local_code) c->local_code = YGST_LOCAL_NETWORK; 307 43 e = ygst_local_find(c->local_code); 308 65 set_message(&c->message, (o->local_message && o->local_message[0]) ? o->local_message 309 22 : (e ? e->message : "network error")); 310 43 c->retryable = ygst_is_retryable(c->local_code, 0, 0); 311 43 c->err_id = ygst_errid_for(c->local_code, 0, 0, c->retryable); 312 43 return 0; 313 } 314 315 188 if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o'; 316 317 188 if (o->http_status >= 200 && o->http_status < 300) { 318 ygst_span result, v; 319 74 if (parsed && ygst_json_member(root, "result", &result) == 1) { 320 ygst_span inner; 321 71 c->success = 1; 322 71 c->root = root; 323 71 c->result = result; 324 71 if (ygst_json_member(root, "recordId", &v) == 1) { 325 68 c->has_record_id = 1; 326 68 c->record_id_raw = v; 327 68 if (ygst_json_type(v) == 's') { 328 67 ygst_json_string(v, &c->record_id); 329 } else { 330 1 ygst_buf_append(&c->record_id, v.p, v.n); 331 } 332 } 333 71 if (ygst_json_member(root, "audioUrl", &v) == 1) { 334 5 if (ygst_json_type(v) == 's') c->has_audio_url = ygst_json_string(v, &c->audio_url) == 0; 335 68 } else if (ygst_json_type(result) == 'o' && ygst_json_member(result, "audioUrl", &inner) == 1 && 336 2 ygst_json_type(inner) == 's') { 337 1 c->has_audio_url = ygst_json_string(inner, &c->audio_url) == 0; 338 } 339 71 if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0; 340 71 ygst_buf_cstr(&c->record_id); 341 71 ygst_buf_cstr(&c->audio_url); 342 71 c->retryable = 0; 343 71 c->err_id = 0; 344 71 return 0; 345 } 346 3 c->local_code = YGST_LOCAL_PROTOCOL; 347 3 set_message(&c->message, parsed ? "invalid platform response: platform response has no result" 348 : "invalid platform response: not a JSON object"); 349 3 c->retryable = 0; 350 3 c->err_id = YGST_LOCAL_PROTOCOL; 351 3 return 0; 352 } 353 354 114 if (parsed) { 355 ygst_span detail; 356 109 if (member_int(root, "code", &code) && code != 0) c->biz_code = (int)code; 357 109 if (!c->biz_code && member_int(root, "errId", &code) && code != 0) c->biz_code = (int)code; 358 109 first_string(root, MESSAGE_KEYS, &c->message); 359 109 if (ygst_json_member(root, "detail", &detail) == 1) { 360 9 if (ygst_json_type(detail) == 's') { 361 ygst_buf d; 362 7 ygst_buf_init(&d); 363 7 ygst_json_string(detail, &d); 364 7 if (!c->biz_code) c->biz_code = bracket_code(ygst_buf_cstr(&d)); 365 7 if (!c->message.len) set_message(&c->message, ygst_buf_cstr(&d)); 366 7 ygst_buf_free(&d); 367 2 } else if (!c->message.len) { 368 1 ygst_buf_reset(&c->message); 369 1 ygst_buf_append(&c->message, detail.p, detail.n > 300 ? 300 : detail.n); 370 } 371 } 372 } 373 114 if (!c->message.len) { 374 char tmp[32]; 375 6 snprintf(tmp, sizeof tmp, "HTTP %d", o->http_status); 376 6 set_message(&c->message, tmp); 377 } 378 114 ygst_buf_cstr(&c->message); 379 114 c->retryable = ygst_is_retryable(0, c->biz_code, o->http_status); 380 114 c->err_id = ygst_errid_for(0, c->biz_code, o->http_status, c->retryable); 381 114 return 0; 382 } 383 384 25int ygst_error_message(const ygst_classification *c, int err_id, int attempts, ygst_buf *out) { 385 25 const char *m = c->message.data ? (const char *)c->message.data : ""; 386 25 if (err_id == YGST_ERRID_RETRYABLE) { 387 10 const char *base = ygst_compat_message(YGST_ERRID_RETRYABLE); 388 size_t mark; 389 10 ygst_buf_append_str(out, base ? base : ""); 390 10 ygst_buf_append_str(out, ": "); 391 10 mark = out->len; 392 10 if (c->http_status > 0) { 393 4 ygst_buf_append_str(out, "HTTP "); 394 4 ygst_buf_append_int(out, c->http_status); 395 4 if (c->biz_code) { 396 2 ygst_buf_append_str(out, " code="); 397 2 ygst_buf_append_int(out, c->biz_code); 398 } 399 6 } else if (c->local_code) { 400 3 const ygst_error_entry *e = ygst_local_find(c->local_code); 401 3 if (e) { 402 2 ygst_buf_append_str(out, e->name); 403 } else { 404 1 ygst_buf_append_str(out, "code="); 405 1 ygst_buf_append_int(out, c->local_code); 406 } 407 3 } else if (c->biz_code) { 408 /* a WebSocket error frame: a business code without an HTTP status */ 409 2 ygst_buf_append_str(out, "code="); 410 2 ygst_buf_append_int(out, c->biz_code); 411 } 412 10 if (m[0]) { 413 10 if (out->len > mark) ygst_buf_append_char(out, ' '); 414 10 ygst_buf_append_str(out, m); 415 } 416 10 if (attempts > 0) { 417 9 ygst_buf_append_str(out, " (attempts "); 418 9 ygst_buf_append_int(out, attempts); 419 9 ygst_buf_append_char(out, ')'); 420 } 421 15 } else if (m[0]) { 422 11 ygst_buf_append_str(out, m); 423 } else { 424 4 const char *cm = ygst_compat_message(err_id); 425 4 const ygst_error_entry *e = cm ? NULL : ygst_error_find(err_id); 426 4 if (!cm && !e) e = ygst_local_find(err_id); 427 4 ygst_buf_append_str(out, cm ? cm : (e ? e->message : "")); 428 } 429 25 ygst_buf_cstr(out); 430 25 return out->oom ? -1 : 0; 431 } 432 433 /* ------------------------------------------------------------------ controller */ 434 435 83void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) { 436 83 memset(c, 0, sizeof *c); 437 83 if (p) { 438 82 c->policy = *p; 439 } else { 440 1 ygst_retry_policy_default(&c->policy); 441 } 442 83 ygst_rng_seed(&c->rng, seed); 443 83 c->start_ms = now_ms; 444 83} 445 446 60static long apply_retry_after(const ygst_retry_policy *p, long delay, long retry_after_ms) { 447 60 if (p->respect_retry_after && retry_after_ms >= 0) { 448 4 long ra = retry_after_ms > p->max_retry_after_ms ? p->max_retry_after_ms : retry_after_ms; 449 4 if (ra > delay) delay = ra; 450 } 451 60 return delay; 452 } 453 454 134ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) { 455 ygst_action a; 456 134 int64_t deadline = c->start_ms + c->policy.total_timeout_ms; 457 134 a.kind = YGST_ACT_FAIL; 458 134 a.delay_ms = 0; 459 134 a.err_id = cls->err_id; 460 134 a.auto_retry = 0; 461 134 c->attempt++; 462 134 c->total_attempts++; 463 134 if (cls->success) { 464 52 a.kind = YGST_ACT_SUCCESS; 465 52 a.err_id = 0; 466 52 return a; 467 } 468 82 if (cls->retryable && c->attempt <= c->policy.max_retries) { 469 49 long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->attempt, &c->rng), retry_after_ms); 470 49 if (now_ms + d <= deadline) { 471 47 a.kind = YGST_ACT_RETRY; 472 47 a.delay_ms = d; 473 47 return a; 474 } 475 2 return a; 476 } 477 46 if (c->policy.auto_retry && c->submission < c->policy.max_auto_retries && 478 13 ygst_retry_policy_has_err_id(&c->policy, cls->err_id)) { 479 11 long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->submission + 1, &c->rng), 480 retry_after_ms); 481 11 if (now_ms + d <= deadline) { 482 10 c->submission++; 483 10 c->attempt = 0; 484 10 a.kind = YGST_ACT_RETRY; 485 10 a.delay_ms = d; 486 10 a.auto_retry = 1; 487 10 return a; 488 } 489 } 490 23 return a; 491 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Per-phase timing of one evaluation (2.1.1, BRIEF C and D): the Server-Timing header of the compat 6 * REST responses, the "timing" object the platform adds to a final JSON on request, the INFO 7 * summary line and the DEBUG detail line. The Objective-C layer and the integration driver record 8 * the phases; everything that turns them into text is here, so that the Linux tests cover it. 9 */ 10 #include "ygst_core.h" 11 12 #include <stdio.h> 13 #include <string.h> 14 15 75void ygst_timing_init(ygst_timing *t) { 16 75 if (!t) return; 17 74 memset(t, 0, sizeof *t); 18 74 t->mode = YGST_TM_NONE; 19 74 t->upload_ms = -1; 20 74 t->wait_ms = -1; 21 74 t->download_ms = -1; 22 74 t->audio_bytes = -1; 23 74 t->upload_bytes = -1; 24 74 t->queued_at_stop = -1; 25 74 t->wire_bytes = -1; 26 74 t->body_bytes = -1; 27 74 t->server_total_ms = -1; 28 74 t->server_engine_ms = -1; 29 74 t->server_platform_ms = -1; 30 } 31 32 5int ygst_timing_wanted(int log_level) { 33 5 return log_level >= 2; 34 } 35 36 /* ------------------------------------------------------------------ Server-Timing */ 37 38 536static int is_ows(char c) { 39 536 return c == ' ' || c == '\t'; 40 } 41 42 /* The n bytes at s equal word (lower case) ignoring ASCII case. */ 43 67static int word_eq(const char *s, size_t n, const char *word) { 44 size_t i; 45 67 if (strlen(word) != n) return 0; 46 223 for (i = 0; i < n; i++) { 47 180 char c = s[i]; 48 180 if (c >= 'A' && c <= 'Z') c = (char)(c + 32); 49 180 if (c != word[i]) return 0; 50 } 51 43 return 1; 52 } 53 54 /* A plain non-negative decimal (5321, 12.75) in the n bytes at s, rounded half up to whole 55 * milliseconds without the C locale's strtod. -1 when it is not one. */ 56 26static long long decimal_ms(const char *s, size_t n) { 57 26 long long whole = 0; 58 26 size_t i = 0; 59 26 int digits = 0; 60 26 int up = 0; 61 90 while (i < n && s[i] >= '0' && s[i] <= '9') { 62 65 if (whole > 900000000000000LL) return -1; 63 64 whole = whole * 10 + (s[i] - '0'); 64 64 digits++; 65 64 i++; 66 } 67 25 if (i < n && s[i] == '.') { 68 6 i++; 69 6 if (i < n && s[i] >= '0' && s[i] <= '9') up = s[i] >= '5'; 70 12 while (i < n && s[i] >= '0' && s[i] <= '9') { 71 6 digits++; 72 6 i++; 73 } 74 } 75 25 if (!digits || i != n) return -1; 76 20 return whole + up; 77 } 78 79 20int ygst_server_timing_parse(const char *value, ygst_timing *t) { 80 20 const char *p = value; 81 20 unsigned found = 0; 82 20 if (!value || !t) return 0; 83 29 for (;;) { 84 const char *name; 85 size_t name_n; 86 47 long long dur = -1; 87 73 while (*p == ',' || is_ows(*p)) p++; 88 47 if (!*p) break; 89 29 name = p; 90 182 while (*p && *p != ';' && *p != ',' && !is_ows(*p)) p++; 91 29 name_n = (size_t)(p - name); 92 29 for (;;) { /* ;param=value, value a token or a quoted string */ 93 const char *pn; 94 const char *pv; 95 size_t pn_n; 96 size_t pv_n; 97 60 while (is_ows(*p)) p++; 98 58 if (*p != ';') break; 99 29 p++; 100 30 while (is_ows(*p)) p++; 101 29 pn = p; 102 118 while (*p && *p != '=' && *p != ';' && *p != ',' && !is_ows(*p)) p++; 103 29 pn_n = (size_t)(p - pn); 104 30 while (is_ows(*p)) p++; 105 29 if (*p != '=') continue; 106 28 p++; 107 29 while (is_ows(*p)) p++; 108 28 if (*p == '"') { 109 3 pv = ++p; 110 16 while (*p && *p != '"') { 111 13 if (*p == '\\' && p[1]) p++; 112 13 p++; 113 } 114 3 pv_n = (size_t)(p - pv); 115 3 if (*p == '"') p++; 116 } else { 117 25 pv = p; 118 108 while (*p && *p != ';' && *p != ',' && !is_ows(*p)) p++; 119 25 pv_n = (size_t)(p - pv); 120 } 121 28 if (word_eq(pn, pn_n, "dur")) dur = decimal_ms(pv, pv_n); 122 } 123 29 while (*p && *p != ',') p++; /* anything else of this metric */ 124 29 if (dur < 0) continue; 125 20 if (word_eq(name, name_n, "total")) { 126 8 t->server_total_ms = dur; 127 8 found |= 1u; 128 12 } else if (word_eq(name, name_n, "engine")) { 129 5 t->server_engine_ms = dur; 130 5 found |= 2u; 131 7 } else if (word_eq(name, name_n, "platform")) { 132 4 t->server_platform_ms = dur; 133 4 found |= 4u; 134 } 135 } 136 18 return (int)((found & 1u) + ((found >> 1) & 1u) + ((found >> 2) & 1u)); 137 } 138 139 /* ------------------------------------------------------------------ timing object */ 140 141 15static void number_ms_member(ygst_span obj, const char *key, long long *out) { 142 ygst_span v; 143 double d; 144 15 if (ygst_json_member(obj, key, &v) != 1 || ygst_json_type(v) != 'n' || ygst_json_number(v, &d) != 0) return; 145 14 if (d >= 0 && d < 9.0e15) *out = (long long)(d + 0.5); 146 } 147 148 54int ygst_timing_member(ygst_span root, ygst_timing *t, ygst_span *timing) { 149 ygst_span v; 150 54 if (timing) { 151 53 timing->p = NULL; 152 53 timing->n = 0; 153 } 154 54 if (!root.p || ygst_json_type(root) != 'o') return -1; 155 52 if (ygst_json_member(root, "timing", &v) != 1 || ygst_json_type(v) != 'o') return 0; 156 7 if (timing) *timing = v; 157 7 if (t) { 158 5 number_ms_member(v, "totalMs", &t->server_total_ms); 159 5 number_ms_member(v, "engineMs", &t->server_engine_ms); 160 5 number_ms_member(v, "platformMs", &t->server_platform_ms); 161 } 162 7 return 1; 163 } 164 165 /* ------------------------------------------------------------------ log lines */ 166 167 9void ygst_timing_set_fallback(ygst_timing *t, const char *reason) { 168 9 size_t i = 0; 169 9 size_t n = 0; 170 9 if (!t) return; 171 251 while (reason && reason[i]) { 172 244 unsigned char lead = (unsigned char)reason[i]; 173 244 size_t len = lead < 0x80 ? 1 : (lead >= 0xF0 ? 4 : (lead >= 0xE0 ? 3 : 2)); 174 size_t k; 175 244 if (n + len >= sizeof t->fallback) break; /* whole UTF-8 sequences only */ 176 592 for (k = 0; k < len && reason[i + k]; k++) { 177 349 char c = reason[i + k]; 178 349 if (c == '"') c = '\''; 179 349 if (c == '\r' || c == '\n' || c == '\t') c = ' '; 180 349 t->fallback[n++] = c; 181 } 182 243 i += k; 183 } 184 8 t->fallback[n] = 0; 185 } 186 187 292static long long between(int64_t from, int64_t to) { 188 292 if (!from || !to || to < from) return -1; 189 267 return (long long)(to - from); 190 } 191 192 406static void put_ms(ygst_buf *b, const char *key, long long ms) { 193 406 ygst_buf_append_char(b, ' '); 194 406 ygst_buf_append_str(b, key); 195 406 ygst_buf_append_char(b, '='); 196 406 if (ms < 0) { 197 29 ygst_buf_append_char(b, '-'); 198 29 return; 199 } 200 377 ygst_buf_append_int(b, ms); 201 377 ygst_buf_append_str(b, "ms"); 202 } 203 204 183static void put_bytes(ygst_buf *b, const char *key, long long n) { 205 183 ygst_buf_append_char(b, ' '); 206 183 ygst_buf_append_str(b, key); 207 183 ygst_buf_append_char(b, '='); 208 183 if (n < 0) { 209 11 ygst_buf_append_char(b, '-'); 210 11 return; 211 } 212 172 ygst_buf_append_int(b, n); 213 172 ygst_buf_append_char(b, 'B'); 214 } 215 216 61static const char *mode_name(ygst_tm_mode m) { 217 61 switch (m) { 218 39 case YGST_TM_REST: 219 39 return "rest"; 220 16 case YGST_TM_WS: 221 16 return "ws"; 222 5 case YGST_TM_WS_REST: 223 5 return "ws>rest"; 224 1 default: 225 1 return "local"; 226 } 227 } 228 229 61static void put_audio(ygst_buf *b, const ygst_timing *t) { 230 char s[48]; 231 120 if (t->audio_bytes >= 0 && t->audio_bytes_per_sec > 0) { 232 59 long long bps = t->audio_bytes_per_sec; 233 59 long long hundredths = (t->audio_bytes * 100 + bps / 2) / bps; 234 59 snprintf(s, sizeof s, " audio=%lld.%02llds", hundredths / 100, hundredths % 100); 235 59 ygst_buf_append_str(b, s); 236 } else { 237 2 ygst_buf_append_str(b, " audio=-"); 238 } 239 61} 240 241 120static void put_token(ygst_buf *b, const char *token_id) { 242 120 ygst_buf_append_str(b, "timing tokenId="); 243 120 ygst_buf_append_str(b, token_id ? token_id : ""); 244 120} 245 246 63int ygst_timing_summary(const ygst_timing *t, const char *token_id, ygst_buf *out) { 247 int ws; 248 int64_t done; 249 long long upload, wait, download; 250 63 if (!t || !out) return -1; 251 61 ws = t->mode == YGST_TM_WS; 252 61 if (ws) { 253 16 int64_t first = t->ws_final_first_ms; 254 16 done = t->ws_final_ms; 255 16 upload = between(t->stop_ms, t->ws_end_ms); 256 16 wait = between(t->ws_end_ms, first ? first : done); 257 16 download = between(first, done); 258 } else { 259 /* the split of the last attempt from the transport, placed after its start */ 260 45 int64_t body_sent = t->rest_start_ms && t->upload_ms >= 0 ? t->rest_start_ms + t->upload_ms : 0; 261 45 done = t->rest_done_ms; 262 45 upload = between(t->stop_ms, body_sent); 263 45 wait = t->wait_ms; 264 45 download = t->download_ms; 265 } 266 61 put_token(out, token_id); 267 61 ygst_buf_append_str(out, " mode="); 268 61 ygst_buf_append_str(out, mode_name(t->mode)); 269 61 put_audio(out, t); 270 61 put_ms(out, "afterStop", between(t->stop_ms, t->delivered_ms)); 271 61 put_ms(out, "upload", upload); 272 61 put_ms(out, "wait", wait); 273 61 put_ms(out, "download", download); 274 61 put_ms(out, "parse", between(done, t->parsed_ms)); 275 61 put_ms(out, "deliver", between(t->parsed_ms, t->delivered_ms)); 276 61 if (ws) { 277 16 put_ms(out, "handshake", between(t->ws_connect_ms, t->ws_handshake_ms)); 278 16 put_bytes(out, "queued", t->queued_at_stop); 279 16 put_bytes(out, "final", t->body_bytes); 280 16 put_bytes(out, "wire", t->wire_bytes); 281 16 if (t->deflate) ygst_buf_append_str(out, " deflate"); 282 16 if (t->attempts > 1) { 283 4 ygst_buf_append_str(out, " connections="); 284 4 ygst_buf_append_int(out, t->attempts); 285 } 286 } else { 287 45 ygst_buf_append_str(out, " attempts="); 288 45 ygst_buf_append_int(out, t->attempts); 289 45 put_bytes(out, "sent", t->upload_bytes); 290 45 put_bytes(out, "body", t->body_bytes); 291 45 put_bytes(out, "wire", t->wire_bytes); 292 45 if (t->encoding[0]) { 293 1 ygst_buf_append_char(out, ' '); 294 1 ygst_buf_append_str(out, t->encoding); 295 } 296 } 297 61 if (t->server_total_ms >= 0) put_ms(out, "server.total", t->server_total_ms); 298 61 if (t->server_engine_ms >= 0) put_ms(out, "server.engine", t->server_engine_ms); 299 61 if (t->server_platform_ms >= 0) put_ms(out, "server.platform", t->server_platform_ms); 300 61 if (t->replayed) ygst_buf_append_str(out, " replayed"); 301 61 if (!t->ok && t->err_id) { 302 11 ygst_buf_append_str(out, " errId="); 303 11 ygst_buf_append_int(out, t->err_id); 304 } 305 61 if (t->mode == YGST_TM_WS_REST && t->fallback[0]) { 306 5 ygst_buf_append_str(out, " fallback=\""); 307 5 ygst_buf_append_str(out, t->fallback); 308 5 ygst_buf_append_char(out, '"'); 309 } 310 61 ygst_buf_cstr(out); 311 61 return out->oom ? -1 : 0; 312 } 313 314 57int ygst_timing_phases(const ygst_timing *t, const char *token_id, ygst_buf *out) { 315 static const char *const names[] = {"start", "connect", "handshake", "params", "firstAudio", 316 "stop", "endWritten", "finalFirstByte", "final", "requestStart", 317 "responseDone", "parsed", "delivered"}; 318 int64_t at[sizeof names / sizeof names[0]]; 319 57 int64_t origin = 0; 320 size_t i; 321 57 if (!t || !out) return -1; 322 56 at[0] = t->start_ms; 323 56 at[1] = t->ws_connect_ms; 324 56 at[2] = t->ws_handshake_ms; 325 56 at[3] = t->ws_params_ms; 326 56 at[4] = t->ws_first_audio_ms; 327 56 at[5] = t->stop_ms; 328 56 at[6] = t->ws_end_ms; 329 56 at[7] = t->ws_final_first_ms; 330 56 at[8] = t->ws_final_ms; 331 56 at[9] = t->rest_start_ms; 332 56 at[10] = t->rest_done_ms; 333 56 at[11] = t->parsed_ms; 334 56 at[12] = t->delivered_ms; 335 784 for (i = 0; i < sizeof names / sizeof names[0]; i++) { 336 728 if (at[i] && (!origin || at[i] < origin)) origin = at[i]; 337 } 338 56 put_token(out, token_id); 339 56 ygst_buf_append_str(out, " phases"); 340 784 for (i = 0; i < sizeof names / sizeof names[0]; i++) { 341 728 if (!at[i]) continue; 342 399 ygst_buf_append_char(out, ' '); 343 399 ygst_buf_append_str(out, names[i]); 344 399 ygst_buf_append_str(out, "=+"); 345 399 ygst_buf_append_int(out, (long long)(at[i] - origin)); 346 399 ygst_buf_append_str(out, "ms"); 347 } 348 56 ygst_buf_cstr(out); 349 56 return out->oom ? -1 : 0; 350 } 351 352 /* " key=<number as the platform wrote it>" */ 353 12static void put_raw_number(ygst_buf *b, ygst_span obj, const char *key) { 354 ygst_span v; 355 12 if (ygst_json_member(obj, key, &v) != 1 || ygst_json_type(v) != 'n') return; 356 8 ygst_buf_append_char(b, ' '); 357 8 ygst_buf_append_str(b, key); 358 8 ygst_buf_append_char(b, '='); 359 8 ygst_buf_append(b, v.p, v.n); 360 } 361 362 58int ygst_timing_stages(ygst_span timing, const char *token_id, ygst_buf *out) { 363 ygst_buf members; 364 ygst_span stages; 365 int rc; 366 58 if (!out) return -1; 367 57 if (!timing.p || ygst_json_type(timing) != 'o') return 0; 368 4 ygst_buf_init(&members); 369 4 if (ygst_json_member(timing, "engineStages", &stages) == 1 && ygst_json_type(stages) == 'o') { 370 3 size_t pos = 0; 371 ygst_span key, value; 372 13 while (ygst_json_object_next(stages, &pos, &key, &value) == 1) { 373 10 if (ygst_json_type(value) != 'n') continue; 374 8 ygst_buf_append_char(&members, ' '); 375 8 ygst_json_string(key, &members); 376 8 ygst_buf_append_char(&members, '='); 377 8 ygst_buf_append(&members, value.p, value.n); 378 } 379 } 380 4 put_raw_number(&members, timing, "receivedAt"); 381 4 put_raw_number(&members, timing, "wsFirstAudioAt"); 382 4 put_raw_number(&members, timing, "audioBytes"); 383 4 rc = members.oom ? -1 : 0; 384 4 if (rc == 0 && members.len) { 385 3 put_token(out, token_id); 386 3 ygst_buf_append_str(out, " engineStages"); 387 3 ygst_buf_append(out, members.data, members.len); 388 3 ygst_buf_cstr(out); 389 3 rc = out->oom ? -1 : 1; 390 } 391 4 ygst_buf_free(&members); 392 4 return rc; 393 }