Total 98.76% (2154 of 2181 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 | 458 | 458 | 100.00% |
| Sources/STKouyuEngine/core/ygst_retry.c | 324 | 324 | 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 75static void put_le32(unsigned char *p, uint32_t v) { 14 75 p[0] = (unsigned char)v; 15 75 p[1] = (unsigned char)(v >> 8); 16 75 p[2] = (unsigned char)(v >> 16); 17 75 p[3] = (unsigned char)(v >> 24); 18 75} 19 20 60static void put_le16(unsigned char *p, uint16_t v) { 21 60 p[0] = (unsigned char)v; 22 60 p[1] = (unsigned char)(v >> 8); 23 60} 24 25 257static uint32_t le32(const unsigned char *p) { 26 257 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); 27 } 28 29 198static uint16_t le16(const unsigned char *p) { 30 198 return (uint16_t)(p[0] | (p[1] << 8)); 31 } 32 33 15void ygst_wav_header(unsigned char out[44], uint32_t data_len, int sample_rate, int channels, int bits) { 34 15 uint32_t block = (uint32_t)(channels * (bits / 8)); 35 15 memcpy(out, "RIFF", 4); 36 15 put_le32(out + 4, data_len > 0xFFFFFFFFu - 36u ? 0xFFFFFFFFu : data_len + 36u); 37 15 memcpy(out + 8, "WAVE", 4); 38 15 memcpy(out + 12, "fmt ", 4); 39 15 put_le32(out + 16, 16); 40 15 put_le16(out + 20, 1); 41 15 put_le16(out + 22, (uint16_t)channels); 42 15 put_le32(out + 24, (uint32_t)sample_rate); 43 15 put_le32(out + 28, (uint32_t)sample_rate * block); 44 15 put_le16(out + 32, (uint16_t)block); 45 15 put_le16(out + 34, (uint16_t)bits); 46 15 memcpy(out + 36, "data", 4); 47 15 put_le32(out + 40, data_len); 48 15} 49 50 75int ygst_is_riff_wave(const unsigned char *p, size_t n) { 51 75 return n >= 12 && memcmp(p, "RIFF", 4) == 0 && memcmp(p + 8, "WAVE", 4) == 0; 52 } 53 54 73int ygst_wav_parse(const unsigned char *p, size_t n, ygst_wav_info *info) { 55 73 size_t pos = 12; 56 73 int have_fmt = 0; 57 73 memset(info, 0, sizeof *info); 58 73 if (!ygst_is_riff_wave(p, n)) return -1; 59 191 while (pos + 8 <= n) { 60 191 uint32_t size = le32(p + pos + 4); 61 191 const unsigned char *id = p + pos; 62 191 size_t body = pos + 8; 63 191 if (memcmp(id, "fmt ", 4) == 0) { 64 67 if (size < 16 || body + 16 > n) return -1; 65 66 info->format = le16(p + body); 66 66 info->channels = le16(p + body + 2); 67 66 info->sample_rate = (int)le32(p + body + 4); 68 66 info->bits = le16(p + body + 14); 69 66 have_fmt = 1; 70 124 } else if (memcmp(id, "data", 4) == 0) { 71 67 if (!have_fmt) return -1; 72 66 info->data_offset = body; 73 /* Streaming writers leave 0 or 0xFFFFFFFF; clamp to what the file holds. */ 74 66 info->data_len = (size == 0 || size > n - body) ? n - body : size; 75 66 return 0; 76 } 77 123 if (size > n - body) return -1; 78 122 pos = body + size + (size & 1u); 79 } 80 0 return -1; 81 } 82 83 69long ygst_pcm_duration_ms(size_t bytes, int sample_rate, int channels, int bits) { 84 double per_second; 85 69 if (sample_rate <= 0 || channels <= 0 || bits <= 0) return -1; 86 67 per_second = (double)sample_rate * channels * (bits / 8.0); 87 67 if (per_second <= 0) return -1; 88 67 return (long)((double)bytes * 1000.0 / per_second); 89 } 90 91 /* Exact duration test without rounding: bytes * 1000 compared with ms * bytes per second. */ 92 57static int pcm_longer_than(size_t bytes, int sample_rate, int channels, int bits, long ms) { 93 57 double per_second = (double)sample_rate * channels * (bits / 8.0); 94 57 if (per_second <= 0) return 0; 95 56 return (double)bytes * 1000.0 > (double)ms * per_second; 96 } 97 98 21int ygst_audio_check_pcm(size_t pcm_len, int sample_rate, int channels, int bits) { 99 long ms; 100 21 if (pcm_len == 0) return YGST_ERRID_AUDIO_EMPTY; 101 20 if (pcm_len > YGST_MAX_AUDIO_BYTES - 44u) return YGST_ERRID_AUDIO_TOO_LARGE; 102 19 ms = ygst_pcm_duration_ms(pcm_len, sample_rate, channels, bits); 103 19 if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT; 104 17 if (pcm_longer_than(pcm_len, sample_rate, channels, bits, YGST_MAX_AUDIO_MS)) return YGST_ERRID_AUDIO_TOO_LARGE; 105 15 return 0; 106 } 107 108 49int ygst_audio_check_upload(const unsigned char *data, size_t len) { 109 ygst_wav_info info; 110 49 if (!data || len == 0) return YGST_ERRID_AUDIO_EMPTY; 111 47 if (len > YGST_MAX_AUDIO_BYTES) return YGST_ERRID_AUDIO_TOO_LARGE; 112 46 if (ygst_wav_parse(data, len, &info) == 0) { 113 long ms; 114 44 if (info.data_len == 0) return YGST_ERRID_AUDIO_EMPTY; 115 42 if (info.format == 1 || info.format == 3 || info.format == 0xFFFE) { 116 42 ms = ygst_pcm_duration_ms(info.data_len, info.sample_rate, info.channels, info.bits); 117 42 if (ms >= 0 && ms < YGST_MIN_AUDIO_MS) return YGST_ERRID_AUDIO_TOO_SHORT; 118 40 if (pcm_longer_than(info.data_len, info.sample_rate, info.channels, info.bits, YGST_MAX_AUDIO_MS)) 119 1 return YGST_ERRID_AUDIO_TOO_LARGE; 120 } 121 } 122 41 return 0; 123 } 124 125 /* ------------------------------------------------------------------ VAD */ 126 127 #define VAD_ABS_DB (-42.0) /* a frame quieter than this is never speech */ 128 #define VAD_OVER_NOISE_DB 10.0 /* speech must also exceed the noise floor by this much */ 129 #define VAD_START_FRAMES 3 /* 30 ms of speech starts an utterance */ 130 #define VAD_NOISE_MIN_DB (-90.0) 131 #define VAD_NOISE_MAX_DB (-35.0) 132 133 10void ygst_vad_init(ygst_vad *v, int sample_rate, double seek_10ms, double ref_length_10ms) { 134 10 memset(v, 0, sizeof *v); 135 10 v->sample_rate = sample_rate > 0 ? sample_rate : 16000; 136 10 v->frame_samples = v->sample_rate / 100; 137 10 if (v->frame_samples <= 0) v->frame_samples = 1; 138 10 v->seek_ms = (int)((seek_10ms > 0 ? seek_10ms : 60) * 10.0); 139 10 v->ref_length_ms = ref_length_10ms > 0 ? (int)(ref_length_10ms * 10.0) : 0; 140 10 v->noise_db = -60.0; 141 10} 142 143 1341int ygst_sound_intensity_db(double db) { 144 1341 double x = (db + 60.0) * 100.0 / 60.0; 145 1341 if (x < 0) x = 0; 146 1341 if (x > 100) x = 100; 147 1341 return (int)(x + 0.5); 148 } 149 150 1338static double rms_db(double sumsq, int n) { 151 double rms; 152 1338 if (n <= 0) return -120.0; 153 1337 rms = sqrt(sumsq / n); 154 1337 if (rms < 1.0) return -120.0; 155 651 return 20.0 * log10(rms / 32768.0); 156 } 157 158 3int ygst_sound_intensity(const int16_t *pcm, size_t samples) { 159 3 double sumsq = 0; 160 size_t i; 161 3203 for (i = 0; i < samples; i++) sumsq += (double)pcm[i] * pcm[i]; 162 3 return ygst_sound_intensity_db(rms_db(sumsq, (int)samples)); 163 } 164 165 1335static void vad_frame(ygst_vad *v, double db) { 166 1335 double threshold = v->noise_db + VAD_OVER_NOISE_DB; 167 int speech; 168 1335 if (threshold < VAD_ABS_DB) threshold = VAD_ABS_DB; 169 1335 speech = db > threshold; 170 1335 v->intensity = ygst_sound_intensity_db(db); 171 1335 if (!speech) { 172 927 v->noise_db = v->noise_db * 0.95 + db * 0.05; 173 927 if (v->noise_db < VAD_NOISE_MIN_DB) v->noise_db = VAD_NOISE_MIN_DB; 174 927 if (v->noise_db > VAD_NOISE_MAX_DB) v->noise_db = VAD_NOISE_MAX_DB; 175 } 176 1335 v->speech_run = speech ? v->speech_run + 1 : 0; 177 1335 if (v->status != 1) { 178 644 if (v->speech_run >= VAD_START_FRAMES) { 179 7 v->status = 1; 180 7 v->speaking_ms = 0; 181 7 v->silence_ms = 0; 182 } 183 644 return; 184 } 185 691 v->speaking_ms += 10; 186 691 v->silence_ms = speech ? 0 : v->silence_ms + 10; 187 691 if (v->silence_ms >= v->seek_ms && v->speaking_ms >= v->ref_length_ms) { 188 4 v->status = 2; 189 4 v->ended_edge = 1; 190 } 191 } 192 193 757int ygst_vad_process(ygst_vad *v, const int16_t *pcm, size_t samples) { 194 size_t i; 195 757 v->ended_edge = 0; 196 214357 for (i = 0; i < samples; i++) { 197 213600 v->acc_sumsq += (double)pcm[i] * pcm[i]; 198 213600 v->acc_n++; 199 213600 if (v->acc_n >= v->frame_samples) { 200 1335 vad_frame(v, rms_db(v->acc_sumsq, v->acc_n)); 201 1335 v->acc_sumsq = 0; 202 1335 v->acc_n = 0; 203 } 204 } 205 757 return v->status; 206 } 207 208 131int ygst_vad_process_bytes(ygst_vad *v, const unsigned char *bytes, size_t n) { 209 int16_t chunk[256]; 210 131 size_t used = 0; 211 131 int ended = 0; 212 131 size_t i = 0; 213 131 if (v->has_carry && n > 0) { 214 20 chunk[used++] = (int16_t)(uint16_t)(v->carry | (bytes[0] << 8)); 215 20 v->has_carry = 0; 216 20 i = 1; 217 } 218 150831 for (; i + 1 < n; i += 2) { 219 150700 chunk[used++] = (int16_t)(uint16_t)(bytes[i] | (bytes[i + 1] << 8)); 220 150700 if (used == sizeof chunk / sizeof chunk[0]) { 221 542 ygst_vad_process(v, chunk, used); 222 542 ended |= v->ended_edge; 223 542 used = 0; 224 } 225 } 226 131 if (i < n) { 227 20 v->carry = bytes[i]; 228 20 v->has_carry = 1; 229 } 230 131 if (used) { 231 130 ygst_vad_process(v, chunk, used); 232 130 ended |= v->ended_edge; 233 } 234 131 v->ended_edge = ended; 235 131 return v->status; 236 } 237 238 4void ygst_tick_compute(double duration_ms, double elapsed_ms, double *millis_left, double *percent_left) { 239 double left; 240 4 if (duration_ms <= 0) { 241 1 *millis_left = 0; 242 1 *percent_left = 0; 243 1 return; 244 } 245 3 left = duration_ms - (elapsed_ms > 0 ? elapsed_ms : 0); 246 3 if (left < 0) left = 0; 247 3 *millis_left = left; 248 3 *percent_left = left * 100.0 / duration_ms; 249 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Growable byte buffer and small string helpers. 6 */ 7 #include "ygst_core.h" 8 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 13 4602void ygst_buf_init(ygst_buf *b) { 14 4602 b->data = NULL; 15 4602 b->len = 0; 16 4602 b->cap = 0; 17 4602 b->oom = 0; 18 4602} 19 20 2301void ygst_buf_free(ygst_buf *b) { 21 2301 free(b->data); 22 2301 ygst_buf_init(b); 23 2301} 24 25 3852void ygst_buf_reset(ygst_buf *b) { 26 3852 b->len = 0; 27 3852 b->oom = 0; 28 3852 if (b->data) b->data[0] = 0; 29 3852} 30 31 20220int ygst_buf_reserve(ygst_buf *b, size_t extra) { 32 size_t need; 33 size_t cap; 34 unsigned char *p; 35 20220 if (b->oom) return -1; 36 20220 if (extra > (size_t)-1 - b->len - 1) { 37 0 b->oom = 1; 38 0 return -1; 39 } 40 20220 need = b->len + extra + 1; /* room for the terminating NUL */ 41 20220 if (need <= b->cap) return 0; 42 2457 cap = b->cap ? b->cap : 64; 43 4950 while (cap < need) { 44 2493 if (cap > ((size_t)-1) / 2) { 45 0 cap = need; 46 0 break; 47 } 48 2493 cap *= 2; 49 } 50 2457 p = (unsigned char *)realloc(b->data, cap); 51 2457 if (!p) { 52 0 b->oom = 1; 53 0 return -1; 54 } 55 2457 b->data = p; 56 2457 b->cap = cap; 57 2457 return 0; 58 } 59 60 20114int ygst_buf_append(ygst_buf *b, const void *p, size_t n) { 61 20114 if (n == 0) return b->oom ? -1 : 0; 62 20107 if (ygst_buf_reserve(b, n) != 0) return -1; 63 20107 memcpy(b->data + b->len, p, n); 64 20107 b->len += n; 65 20107 b->data[b->len] = 0; 66 20107 return 0; 67 } 68 69 2617int ygst_buf_append_str(ygst_buf *b, const char *s) { 70 2617 return ygst_buf_append(b, s ? s : "", s ? strlen(s) : 0); 71 } 72 73 10162int ygst_buf_append_char(ygst_buf *b, char c) { 74 10162 return ygst_buf_append(b, &c, 1); 75 } 76 77 72int ygst_buf_append_int(ygst_buf *b, long long v) { 78 char tmp[32]; 79 72 int n = snprintf(tmp, sizeof tmp, "%lld", v); 80 72 if (n < 0) return -1; 81 72 return ygst_buf_append(b, tmp, (size_t)n); 82 } 83 84 9234const char *ygst_buf_cstr(ygst_buf *b) { 85 9234 if (!b->data) { 86 113 if (ygst_buf_reserve(b, 0) != 0) return ""; 87 113 b->data[0] = 0; 88 } 89 9234 return (const char *)b->data; 90 } 91 92 816char *ygst_strdup(const char *s) { 93 size_t n; 94 char *p; 95 816 if (!s) return NULL; 96 815 n = strlen(s); 97 815 p = (char *)malloc(n + 1); 98 815 if (!p) return NULL; 99 815 memcpy(p, s, n + 1); 100 815 return p; 101 } 102 103 900static int lower_ascii(int c) { 104 900 return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; 105 } 106 107 227int ygst_strcaseeq(const char *a, const char *b) { 108 227 if (!a || !b) return 0; 109 553 while (*a && *b) { 110 450 if (lower_ascii((unsigned char)*a) != lower_ascii((unsigned char)*b)) return 0; 111 327 a++; 112 327 b++; 113 } 114 103 return *a == 0 && *b == 0; 115 } 116 117 11int ygst_has_suffix_ci(const char *s, const char *suffix) { 118 size_t n; 119 size_t m; 120 11 if (!s || !suffix) return 0; 121 10 n = strlen(s); 122 10 m = strlen(suffix); 123 10 if (m > n) return 0; 124 9 return ygst_strcaseeq(s + (n - m), suffix); 125 } 126 127 488static int is_space(unsigned char c) { 128 488 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; 129 } 130 131 144size_t ygst_trim_copy(const char *s, char *out, size_t cap) { 132 const char *e; 133 size_t n; 134 144 if (!out || cap == 0) return 0; 135 144 out[0] = 0; 136 144 if (!s) return 0; 137 149 while (*s && is_space((unsigned char)*s)) s++; 138 142 e = s + strlen(s); 139 147 while (e > s && is_space((unsigned char)e[-1])) e--; 140 142 n = (size_t)(e - s); 141 142 if (n >= cap) n = cap - 1; 142 142 memcpy(out, s, n); 143 142 out[n] = 0; 144 142 return n; 145 } 146 147 206int ygst_is_blank(const char *s) { 148 206 if (!s) return 1; 149 208 while (*s) { 150 196 if (!is_space((unsigned char)*s)) return 0; 151 11 s++; 152 } 153 12 return 1; 154 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 2104), Base64 (RFC 4648). Written from the 6 * specifications so that the core has no dependency on CommonCrypto or OpenSSL. 7 */ 8 #include "ygst_core.h" 9 10 #include <string.h> 11 12 static const uint32_t K[64] = { 13 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, 14 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, 15 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 16 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 17 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 18 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, 19 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, 20 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u}; 21 22 #define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n)))) 23 24 16046static void sha256_block(ygst_sha256_ctx *c, const unsigned char *p) { 25 uint32_t w[64]; 26 uint32_t a, b, cc, d, e, f, g, h, t1, t2; 27 int i; 28 272782 for (i = 0; i < 16; i++) { 29 256736 w[i] = ((uint32_t)p[i * 4] << 24) | ((uint32_t)p[i * 4 + 1] << 16) | ((uint32_t)p[i * 4 + 2] << 8) | 30 256736 (uint32_t)p[i * 4 + 3]; 31 } 32 786254 for (i = 16; i < 64; i++) { 33 770208 uint32_t s0 = ROTR(w[i - 15], 7) ^ ROTR(w[i - 15], 18) ^ (w[i - 15] >> 3); 34 770208 uint32_t s1 = ROTR(w[i - 2], 17) ^ ROTR(w[i - 2], 19) ^ (w[i - 2] >> 10); 35 770208 w[i] = w[i - 16] + s0 + w[i - 7] + s1; 36 } 37 16046 a = c->h[0]; 38 16046 b = c->h[1]; 39 16046 cc = c->h[2]; 40 16046 d = c->h[3]; 41 16046 e = c->h[4]; 42 16046 f = c->h[5]; 43 16046 g = c->h[6]; 44 16046 h = c->h[7]; 45 1042990 for (i = 0; i < 64; i++) { 46 1026944 uint32_t S1 = ROTR(e, 6) ^ ROTR(e, 11) ^ ROTR(e, 25); 47 1026944 uint32_t ch = (e & f) ^ ((~e) & g); 48 1026944 uint32_t S0 = ROTR(a, 2) ^ ROTR(a, 13) ^ ROTR(a, 22); 49 1026944 uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc); 50 1026944 t1 = h + S1 + ch + K[i] + w[i]; 51 1026944 t2 = S0 + maj; 52 1026944 h = g; 53 1026944 g = f; 54 1026944 f = e; 55 1026944 e = d + t1; 56 1026944 d = cc; 57 1026944 cc = b; 58 1026944 b = a; 59 1026944 a = t1 + t2; 60 } 61 16046 c->h[0] += a; 62 16046 c->h[1] += b; 63 16046 c->h[2] += cc; 64 16046 c->h[3] += d; 65 16046 c->h[4] += e; 66 16046 c->h[5] += f; 67 16046 c->h[6] += g; 68 16046 c->h[7] += h; 69 16046} 70 71 177void ygst_sha256_init(ygst_sha256_ctx *c) { 72 177 c->h[0] = 0x6a09e667u; 73 177 c->h[1] = 0xbb67ae85u; 74 177 c->h[2] = 0x3c6ef372u; 75 177 c->h[3] = 0xa54ff53au; 76 177 c->h[4] = 0x510e527fu; 77 177 c->h[5] = 0x9b05688cu; 78 177 c->h[6] = 0x1f83d9abu; 79 177 c->h[7] = 0x5be0cd19u; 80 177 c->total = 0; 81 177 c->used = 0; 82 177} 83 84 1339void ygst_sha256_update(ygst_sha256_ctx *c, const void *data, size_t n) { 85 1339 const unsigned char *p = (const unsigned char *)data; 86 1339 c->total += (uint64_t)n; 87 1339 if (c->used) { 88 1008 size_t take = 64 - c->used; 89 1008 if (take > n) take = n; 90 1008 memcpy(c->block + c->used, p, take); 91 1008 c->used += take; 92 1008 p += take; 93 1008 n -= take; 94 1008 if (c->used < 64) return; 95 996 sha256_block(c, c->block); 96 996 c->used = 0; 97 } 98 16178 while (n >= 64) { 99 14851 sha256_block(c, p); 100 14851 p += 64; 101 14851 n -= 64; 102 } 103 1327 if (n) { 104 1167 memcpy(c->block, p, n); 105 1167 c->used = n; 106 } 107 } 108 109 177void ygst_sha256_final(ygst_sha256_ctx *c, unsigned char out[32]) { 110 177 uint64_t bits = c->total * 8u; 111 int i; 112 177 c->block[c->used++] = 0x80; 113 177 if (c->used > 56) { 114 22 memset(c->block + c->used, 0, 64 - c->used); 115 22 sha256_block(c, c->block); 116 22 c->used = 0; 117 } 118 177 memset(c->block + c->used, 0, 56 - c->used); 119 1593 for (i = 0; i < 8; i++) c->block[56 + i] = (unsigned char)(bits >> (56 - 8 * i)); 120 177 sha256_block(c, c->block); 121 1593 for (i = 0; i < 8; i++) { 122 1416 out[i * 4] = (unsigned char)(c->h[i] >> 24); 123 1416 out[i * 4 + 1] = (unsigned char)(c->h[i] >> 16); 124 1416 out[i * 4 + 2] = (unsigned char)(c->h[i] >> 8); 125 1416 out[i * 4 + 3] = (unsigned char)c->h[i]; 126 } 127 177} 128 129 5void ygst_sha256(const void *data, size_t n, unsigned char out[32]) { 130 ygst_sha256_ctx c; 131 5 ygst_sha256_init(&c); 132 5 ygst_sha256_update(&c, data, n); 133 5 ygst_sha256_final(&c, out); 134 5} 135 136 69void ygst_hmac_sha256(const void *key, size_t key_len, const void *msg, size_t msg_len, unsigned char out[32]) { 137 unsigned char k[64]; 138 unsigned char pad[64]; 139 unsigned char inner[32]; 140 ygst_sha256_ctx c; 141 int i; 142 69 memset(k, 0, sizeof k); 143 69 if (key_len > 64) { 144 1 ygst_sha256(key, key_len, k); 145 68 } else if (key_len) { 146 68 memcpy(k, key, key_len); 147 } 148 4485 for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x36); 149 69 ygst_sha256_init(&c); 150 69 ygst_sha256_update(&c, pad, 64); 151 69 ygst_sha256_update(&c, msg, msg_len); 152 69 ygst_sha256_final(&c, inner); 153 4485 for (i = 0; i < 64; i++) pad[i] = (unsigned char)(k[i] ^ 0x5c); 154 69 ygst_sha256_init(&c); 155 69 ygst_sha256_update(&c, pad, 64); 156 69 ygst_sha256_update(&c, inner, 32); 157 69 ygst_sha256_final(&c, out); 158 69} 159 160 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 161 162 91size_t ygst_base64_encode(const unsigned char *in, size_t n, char *out, size_t cap) { 163 91 size_t need = ((n + 2) / 3) * 4; 164 size_t i; 165 91 size_t o = 0; 166 91 if (!out || cap < need + 1) return 0; 167 834 for (i = 0; i + 2 < n; i += 3) { 168 745 uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) | (uint32_t)in[i + 2]; 169 745 out[o++] = B64[(v >> 18) & 63]; 170 745 out[o++] = B64[(v >> 12) & 63]; 171 745 out[o++] = B64[(v >> 6) & 63]; 172 745 out[o++] = B64[v & 63]; 173 } 174 89 if (n - i == 1) { 175 18 uint32_t v = (uint32_t)in[i] << 16; 176 18 out[o++] = B64[(v >> 18) & 63]; 177 18 out[o++] = B64[(v >> 12) & 63]; 178 18 out[o++] = '='; 179 18 out[o++] = '='; 180 71 } else if (n - i == 2) { 181 68 uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8); 182 68 out[o++] = B64[(v >> 18) & 63]; 183 68 out[o++] = B64[(v >> 12) & 63]; 184 68 out[o++] = B64[(v >> 6) & 63]; 185 68 out[o++] = '='; 186 } 187 89 out[o] = 0; 188 89 return o; 189 } 190 191 168void ygst_hex_lower(const unsigned char *in, size_t n, char *out) { 192 static const char H[] = "0123456789abcdef"; 193 size_t i; 194 2400 for (i = 0; i < n; i++) { 195 2232 out[i * 2] = H[in[i] >> 4]; 196 2232 out[i * 2 + 1] = H[in[i] & 15]; 197 } 198 168 out[n * 2] = 0; 199 168}
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Shengtong style result, error and reading progress JSON (DESIGN 6.2, 14.3), the dtLastResponse 6 * clock text, the request params echo, base URL resolution and small identifiers. 7 */ 8 #include "ygst_core.h" 9 10 #include <stdio.h> 11 #include <string.h> 12 13 /* civil_from_days (H. Hinnant) */ 14 90static void civil_from_days(long long z, long long *y, unsigned *m, unsigned *d) { 15 long long era; 16 unsigned doe, yoe, doy, mp; 17 90 z += 719468; 18 90 era = (z >= 0 ? z : z - 146096) / 146097; 19 90 doe = (unsigned)(z - era * 146097); 20 90 yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; 21 90 doy = doe - (365 * yoe + yoe / 4 - yoe / 100); 22 90 mp = (5 * doy + 2) / 153; 23 90 *d = doy - (153 * mp + 2) / 5 + 1; 24 90 *m = mp < 10 ? mp + 3 : mp - 9; 25 90 *y = (long long)yoe + era * 400 + (*m <= 2); 26 90} 27 28 90int ygst_format_dt(int64_t epoch_ms, int tz_offset_min, char out[32]) { 29 90 long long ms = (long long)epoch_ms + (long long)tz_offset_min * 60000LL; 30 90 long long days = ms >= 0 ? ms / 86400000LL : -((-ms + 86399999LL) / 86400000LL); 31 90 long long rem = ms - days * 86400000LL; 32 long long y; 33 unsigned m, d, hh, mi, ss, mss; 34 90 civil_from_days(days, &y, &m, &d); 35 90 if (y < 0) y = 0; 36 90 if (y > 9999) y = 9999; 37 90 hh = (unsigned)(rem / 3600000LL) % 24u; 38 90 mi = (unsigned)(rem / 60000LL) % 60u; 39 90 ss = (unsigned)(rem / 1000LL) % 60u; 40 90 mss = (unsigned)rem % 1000u; 41 90 snprintf(out, 32, "%04u-%02u-%02u %02u:%02u:%02u:%03u", (unsigned)y % 10000u, m % 13u, d % 32u, hh, mi, ss, 42 mss); 43 90 return 0; 44 } 45 46 52int ygst_align_result(ygst_span result, ygst_buf *out) { 47 ygst_span sentences, sent, details, d, scores, v, so, sp; 48 52 size_t spos = 0; 49 52 size_t copied = 0; /* bytes of result already copied to out */ 50 52 int added = 0; 51 52 if (!result.p) return -1; 52 98 if (ygst_json_type(result) != 'o' || ygst_json_member(result, "sentences", &sentences) != 1 || 53 47 ygst_json_type(sentences) != 'a') { 54 5 return ygst_buf_append(out, result.p, result.n) == 0 ? 0 : -1; 55 } 56 97 while (ygst_json_array_next(sentences, &spos, &sent) == 1) { 57 51 size_t dpos = 0; 58 97 if (ygst_json_type(sent) != 'o' || ygst_json_member(sent, "details", &details) != 1 || 59 46 ygst_json_type(details) != 'a') { 60 6 continue; 61 } 62 301 while (ygst_json_array_next(details, &dpos, &d) == 1) { 63 int add_overall, add_pron; 64 size_t close; 65 256 if (ygst_json_type(d) != 'o' || ygst_json_member(d, "overall", &v) == 1) continue; 66 254 if (ygst_json_member(d, "scores", &scores) != 1 || ygst_json_type(scores) != 'o') continue; 67 252 add_overall = ygst_json_member(scores, "overall", &so) == 1; 68 252 add_pron = ygst_json_member(d, "pronunciation", &v) != 1 && ygst_json_member(scores, "pronunciation", &sp) == 1; 69 252 if (!add_overall && !add_pron) continue; 70 251 close = (size_t)(d.p + d.n - 1 - result.p); /* the closing brace of the item */ 71 251 ygst_buf_append(out, result.p + copied, close - copied); 72 251 if (add_overall) { 73 251 ygst_buf_append_str(out, ",\"overall\":"); 74 251 ygst_buf_append(out, so.p, so.n); 75 251 added++; 76 } 77 251 if (add_pron) { 78 248 ygst_buf_append_str(out, ",\"pronunciation\":"); 79 248 ygst_buf_append(out, sp.p, sp.n); 80 248 added++; 81 } 82 251 copied = close; 83 } 84 } 85 46 ygst_buf_append(out, result.p + copied, result.n - copied); 86 46 return out->oom ? -1 : added; 87 } 88 89 897static void key(ygst_buf *out, const char *k, int first) { 90 897 if (!first) ygst_buf_append_char(out, ','); 91 897 ygst_json_escape(out, k); 92 897 ygst_buf_append_char(out, ':'); 93 897} 94 95 42int ygst_envelope_result(ygst_buf *out, const char *token_id, const char *record_id_json, const char *app_key, 96 const char *user_id, const char *ref_text, const char *dt_last_response, 97 ygst_span result_raw, const char *params_json, const char *audio_url) { 98 42 if (!result_raw.p || result_raw.n == 0) return -1; 99 41 ygst_buf_append_char(out, '{'); 100 41 key(out, "tokenId", 1); 101 41 ygst_json_escape(out, token_id); 102 41 if (record_id_json && record_id_json[0]) { 103 40 key(out, "recordId", 0); 104 40 ygst_buf_append_str(out, record_id_json); 105 } 106 41 key(out, "applicationId", 0); 107 41 ygst_json_escape(out, app_key); 108 41 key(out, "userId", 0); 109 41 ygst_json_escape(out, user_id); 110 41 key(out, "refText", 0); 111 41 ygst_json_escape(out, ref_text); 112 41 key(out, "eof", 0); 113 41 ygst_buf_append_char(out, '1'); 114 41 key(out, "dtLastResponse", 0); 115 41 ygst_json_escape(out, dt_last_response); 116 41 key(out, "result", 0); 117 41 if (ygst_align_result(result_raw, out) < 0) return -1; 118 41 if (params_json && params_json[0]) { 119 2 key(out, "params", 0); 120 2 ygst_buf_append_str(out, params_json); 121 } 122 41 if (audio_url && audio_url[0]) { 123 3 key(out, "audioUrl", 0); 124 3 ygst_json_escape(out, audio_url); 125 } 126 41 ygst_buf_append_char(out, '}'); 127 41 ygst_buf_cstr(out); 128 41 return out->oom ? -1 : 0; 129 } 130 131 20int ygst_envelope_error(ygst_buf *out, const char *token_id, int err_id, const char *message, 132 const char *app_key) { 133 20 ygst_buf_append_char(out, '{'); 134 20 key(out, "tokenId", 1); 135 20 ygst_json_escape(out, token_id); 136 20 key(out, "errId", 0); 137 20 ygst_buf_append_int(out, err_id); 138 20 key(out, "error", 0); 139 20 ygst_json_escape(out, message); 140 20 key(out, "eof", 0); 141 20 ygst_buf_append_char(out, '1'); 142 20 key(out, "applicationId", 0); 143 20 ygst_json_escape(out, app_key); 144 20 ygst_buf_append_char(out, '}'); 145 20 ygst_buf_cstr(out); 146 20 return out->oom ? -1 : 0; 147 } 148 149 /* The progress result as the platform sent it, without the first skip items of "words". */ 150 54static int append_progress_result(ygst_buf *out, ygst_span result, int skip) { 151 ygst_span words, item; 152 54 size_t pos = 0; 153 54 int i = 0; 154 54 int first = 1; 155 56 if (skip <= 0 || ygst_json_type(result) != 'o' || ygst_json_member(result, "words", &words) != 1 || 156 2 ygst_json_type(words) != 'a') { 157 52 return ygst_buf_append(out, result.p, result.n); 158 } 159 2 ygst_buf_append(out, result.p, (size_t)(words.p - result.p)); 160 2 ygst_buf_append_char(out, '['); 161 8 while (ygst_json_array_next(words, &pos, &item) == 1) { 162 6 if (i++ < skip) continue; 163 1 if (!first) ygst_buf_append_char(out, ','); 164 1 first = 0; 165 1 ygst_buf_append(out, item.p, item.n); 166 } 167 2 ygst_buf_append_char(out, ']'); 168 2 ygst_buf_append(out, words.p + words.n, (size_t)(result.p + result.n - (words.p + words.n))); 169 2 return out->oom ? -1 : 0; 170 } 171 172 56int ygst_envelope_progress(ygst_buf *out, const char *token_id, const char *app_key, const char *user_id, 173 const char *ref_text, const char *dt_last_response, ygst_span result_raw, 174 int skip_words) { 175 56 if (!result_raw.p || result_raw.n == 0) return -1; 176 54 ygst_buf_append_char(out, '{'); 177 54 key(out, "tokenId", 1); 178 54 ygst_json_escape(out, token_id); 179 /* Shengtong's intermediate results carry recordId; the platform names the record only in the final */ 180 54 key(out, "recordId", 0); 181 54 ygst_buf_append_str(out, "\"\""); 182 54 key(out, "applicationId", 0); 183 54 ygst_json_escape(out, app_key); 184 54 key(out, "userId", 0); 185 54 ygst_json_escape(out, user_id); 186 54 key(out, "refText", 0); 187 54 ygst_json_escape(out, ref_text); 188 54 key(out, "eof", 0); 189 54 ygst_buf_append_char(out, '0'); 190 54 key(out, "dtLastResponse", 0); 191 54 ygst_json_escape(out, dt_last_response); 192 54 key(out, "result", 0); 193 54 if (append_progress_result(out, result_raw, skip_words) != 0) return -1; 194 54 ygst_buf_append_char(out, '}'); 195 54 ygst_buf_cstr(out); 196 54 return out->oom ? -1 : 0; 197 } 198 199 3int ygst_params_json(ygst_buf *out, const char *app_key, const char *user_id, int64_t timestamp_sec, 200 const char *audio_type, int sample_rate, int channel, int sample_bytes, 201 const char *core_type, const char *token_id, const ygst_fields *request_fields) { 202 size_t i; 203 char ts[32]; 204 3 snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec); 205 3 ygst_buf_append_str(out, "{\"app\":{"); 206 3 key(out, "applicationId", 1); 207 3 ygst_json_escape(out, app_key); 208 3 key(out, "userId", 0); 209 3 ygst_json_escape(out, user_id); 210 3 key(out, "timestamp", 0); 211 3 ygst_json_escape(out, ts); 212 3 ygst_buf_append_str(out, "},\"audio\":{"); 213 3 key(out, "audioType", 1); 214 3 ygst_json_escape(out, audio_type); 215 3 key(out, "sampleRate", 0); 216 3 ygst_buf_append_int(out, sample_rate); 217 3 key(out, "channel", 0); 218 3 ygst_buf_append_int(out, channel); 219 3 key(out, "sampleBytes", 0); 220 3 ygst_buf_append_int(out, sample_bytes); 221 3 ygst_buf_append_str(out, "},\"request\":{"); 222 3 key(out, "coreType", 1); 223 3 ygst_json_escape(out, core_type); 224 3 key(out, "tokenId", 0); 225 3 ygst_json_escape(out, token_id); 226 3 if (request_fields) { 227 9 for (i = 0; i < request_fields->count; i++) { 228 7 const char *n = request_fields->items[i].name; 229 7 if (strcmp(n, "coreType") == 0 || strcmp(n, "tokenId") == 0) continue; 230 6 key(out, n, 0); 231 6 ygst_json_escape(out, request_fields->items[i].value); 232 } 233 } 234 3 ygst_buf_append_str(out, "}}"); 235 3 ygst_buf_cstr(out); 236 3 return out->oom ? -1 : 0; 237 } 238 239 /* ------------------------------------------------------------------ base URL */ 240 241 46static int prefix_ci(const char *s, const char *p) { 242 157 while (*p) { 243 144 char a = *s, b = *p; 244 144 if (a >= 'A' && a <= 'Z') a = (char)(a + 32); 245 144 if (a != b) return 0; 246 111 s++; 247 111 p++; 248 } 249 13 return 1; 250 } 251 252 13static int host_is_shengtong(const char *host, size_t n) { 253 static const char D[] = "stkouyu.com"; 254 13 size_t m = sizeof D - 1; 255 char h[256]; 256 size_t i; 257 13 if (n == 0 || n >= sizeof h) return 0; 258 195 for (i = 0; i < n; i++) { 259 182 char c = host[i]; 260 182 h[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; 261 } 262 13 h[n] = 0; 263 13 if (n == m) return strcmp(h, D) == 0; 264 9 return n > m && strcmp(h + n - m, D) == 0 && h[n - m - 1] == '.'; 265 } 266 267 18ygst_base_kind ygst_resolve_base_url(const char *server, char *out, size_t cap) { 268 char s[1024]; 269 const char *scheme; 270 const char *rest; 271 const char *host_start; 272 const char *host_end; 273 const char *end; 274 size_t n; 275 int written; 276 18 if (!out || cap == 0) return YGST_BASE_DEFAULT_EMPTY; 277 18 snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL); 278 18 n = ygst_trim_copy(server, s, sizeof s); 279 18 if (n == 0) return YGST_BASE_DEFAULT_EMPTY; 280 16 if (prefix_ci(s, "https://")) { 281 2 scheme = "https"; 282 2 rest = s + 8; 283 14 } else if (prefix_ci(s, "http://")) { 284 5 scheme = "http"; 285 5 rest = s + 7; 286 9 } else if (prefix_ci(s, "wss://")) { 287 2 scheme = "https"; 288 2 rest = s + 6; 289 7 } else if (prefix_ci(s, "ws://")) { 290 4 scheme = "http"; 291 4 rest = s + 5; 292 } else { 293 3 return host_is_shengtong(s, strcspn(s, "/:?#")) ? YGST_BASE_DEFAULT_SHENGTONG : YGST_BASE_ERR_SCHEME; 294 } 295 13 host_start = rest; 296 13 if (*host_start == '[') { 297 2 host_end = strchr(host_start, ']'); 298 2 if (!host_end) return YGST_BASE_ERR_HOST; 299 1 host_end++; 300 } else { 301 11 host_end = host_start; 302 159 while (*host_end && *host_end != '/' && *host_end != ':' && *host_end != '?' && *host_end != '#') host_end++; 303 } 304 12 if (host_end == host_start) return YGST_BASE_ERR_HOST; 305 10 if (host_is_shengtong(host_start, (size_t)(host_end - host_start))) return YGST_BASE_DEFAULT_SHENGTONG; 306 7 end = rest; 307 145 while (*end && *end != '?' && *end != '#') end++; 308 9 while (end > host_end && end[-1] == '/') end--; 309 7 written = snprintf(out, cap, "%s://%.*s", scheme, (int)(end - rest), rest); 310 7 if (written < 0 || (size_t)written >= cap) { 311 1 snprintf(out, cap, "%s", YGST_DEFAULT_BASE_URL); 312 1 return YGST_BASE_ERR_HOST; 313 } 314 6 return YGST_BASE_CUSTOM; 315 } 316 317 39int ygst_build_url(const char *base, const char *core_type, char *out, size_t cap) { 318 size_t n; 319 int w; 320 39 if (!base || !core_type || !out) return -1; 321 38 n = strlen(base); 322 39 while (n > 0 && base[n - 1] == '/') n--; 323 38 w = snprintf(out, cap, "%.*s/%s", (int)n, base, core_type); 324 38 return (w < 0 || (size_t)w >= cap) ? -1 : 0; 325 } 326 327 53int ygst_token_id(const unsigned char rnd[16], char out[33]) { 328 53 ygst_hex_lower(rnd, 16, out); 329 53 return 0; 330 } 331 332 48int ygst_idempotency_key_ok(const char *k) { 333 48 size_t n = 0; 334 48 if (!k) return 0; 335 1704 for (; k[n]; n++) { 336 1662 unsigned char c = (unsigned char)k[n]; 337 1662 if (c < 0x21 || c > 0x7E) return 0; 338 1658 if (n >= 200) return 0; 339 } 340 42 return n >= 1; 341 } 342 343 3void ygst_mask_app_key(const char *app_key, char out[16]) { 344 3 size_t n = app_key ? strlen(app_key) : 0; 345 3 if (n > 4) n = 4; 346 3 memcpy(out, app_key ? app_key : "", n); 347 3 memcpy(out + n, "***", 4); 348 3}
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 402void ygst_fields_init(ygst_fields *f) { 14 402 f->items = NULL; 15 402 f->count = 0; 16 402 f->cap = 0; 17 402 f->oom = 0; 18 402} 19 20 197void ygst_fields_free(ygst_fields *f) { 21 size_t i; 22 598 for (i = 0; i < f->count; i++) { 23 401 free(f->items[i].name); 24 401 free(f->items[i].value); 25 } 26 197 free(f->items); 27 197 ygst_fields_init(f); 28 197} 29 30 424static long find_index(const ygst_fields *f, const char *name) { 31 size_t i; 32 2479 for (i = 0; i < f->count; i++) { 33 2072 if (strcmp(f->items[i].name, name) == 0) return (long)i; 34 } 35 407 return -1; 36 } 37 38 2int ygst_fields_remove(ygst_fields *f, const char *name) { 39 2 long i = name ? find_index(f, name) : -1; 40 2 if (i < 0) return 0; 41 1 free(f->items[i].name); 42 1 free(f->items[i].value); 43 1 memmove(&f->items[i], &f->items[i + 1], (f->count - (size_t)i - 1) * sizeof(ygst_field)); 44 1 f->count--; 45 1 return 1; 46 } 47 48 414int ygst_fields_set(ygst_fields *f, const char *name, const char *value) { 49 long i; 50 char *v; 51 414 if (!name || f->oom) return -1; 52 413 if (!value) { 53 1 ygst_fields_remove(f, name); 54 1 return 0; 55 } 56 412 i = find_index(f, name); 57 412 v = ygst_strdup(value); 58 412 if (!v) { 59 0 f->oom = 1; 60 0 return -1; 61 } 62 412 if (i >= 0) { 63 10 free(f->items[i].value); 64 10 f->items[i].value = v; 65 10 return 0; 66 } 67 402 if (f->count == f->cap) { 68 137 size_t cap = f->cap ? f->cap * 2 : 16; 69 137 ygst_field *p = (ygst_field *)realloc(f->items, cap * sizeof(ygst_field)); 70 137 if (!p) { 71 0 free(v); 72 0 f->oom = 1; 73 0 return -1; 74 } 75 137 f->items = p; 76 137 f->cap = cap; 77 } 78 402 f->items[f->count].name = ygst_strdup(name); 79 402 if (!f->items[f->count].name) { 80 0 free(v); 81 0 f->oom = 1; 82 0 return -1; 83 } 84 402 f->items[f->count].value = v; 85 402 f->count++; 86 402 return 0; 87 } 88 89 10const char *ygst_fields_get(const ygst_fields *f, const char *name) { 90 10 long i = name ? find_index(f, name) : -1; 91 10 return i < 0 ? NULL : f->items[i].value; 92 } 93 94 3int ygst_fields_copy(const ygst_fields *src, ygst_fields *dst) { 95 size_t i; 96 54 for (i = 0; i < src->count; i++) { 97 51 if (ygst_fields_set(dst, src->items[i].name, src->items[i].value) != 0) return -1; 98 } 99 3 return 0; 100 } 101 102 /* Decodes one UTF-8 sequence. Invalid bytes decode as themselves so that ordering stays total. */ 103 722static uint32_t next_cp(const unsigned char **s) { 104 722 const unsigned char *p = *s; 105 722 uint32_t c = p[0]; 106 722 int n = 0; 107 int i; 108 722 if (c < 0x80) { 109 712 *s = p + 1; 110 712 return c; 111 } 112 10 if ((c & 0xE0) == 0xC0) { 113 3 n = 1; 114 3 c &= 0x1F; 115 7 } else if ((c & 0xF0) == 0xE0) { 116 2 n = 2; 117 2 c &= 0x0F; 118 5 } else if ((c & 0xF8) == 0xF0) { 119 4 n = 3; 120 4 c &= 0x07; 121 } else { 122 1 *s = p + 1; 123 1 return 0xFFFD; 124 } 125 27 for (i = 1; i <= n; i++) { 126 19 if ((p[i] & 0xC0) != 0x80) { 127 1 *s = p + 1; 128 1 return 0xFFFD; 129 } 130 18 c = (c << 6) | (p[i] & 0x3F); 131 } 132 8 *s = p + n + 1; 133 8 return c; 134 } 135 136 /* Emits the UTF-16 code units of cp into u, returns how many (1 or 2). */ 137 722static int utf16_units(uint32_t cp, uint32_t u[2]) { 138 722 if (cp >= 0x10000 && cp <= 0x10FFFF) { 139 4 cp -= 0x10000; 140 4 u[0] = 0xD800 + (cp >> 10); 141 4 u[1] = 0xDC00 + (cp & 0x3FF); 142 4 return 2; 143 } 144 718 u[0] = cp; 145 718 return 1; 146 } 147 148 286int ygst_utf16_compare(const char *a, const char *b) { 149 286 const unsigned char *pa = (const unsigned char *)a; 150 286 const unsigned char *pb = (const unsigned char *)b; 151 uint32_t ua[2], ub[2]; 152 286 int na = 0, nb = 0, ia = 0, ib = 0; 153 77 for (;;) { 154 uint32_t ca, cb; 155 363 if (ia == na) { 156 363 if (!*pa) { 157 2 na = 0; 158 } else { 159 361 na = utf16_units(next_cp(&pa), ua); 160 } 161 363 ia = 0; 162 } 163 363 if (ib == nb) { 164 363 if (!*pb) { 165 2 nb = 0; 166 } else { 167 361 nb = utf16_units(next_cp(&pb), ub); 168 } 169 363 ib = 0; 170 } 171 363 if (na == 0 || nb == 0) { 172 3 if (na == 0 && nb == 0) return 0; 173 2 return na == 0 ? -1 : 1; 174 } 175 360 ca = ua[ia++]; 176 360 cb = ub[ib++]; 177 360 if (ca != cb) return ca < cb ? -1 : 1; 178 } 179 } 180 181 278static int cmp_field_ptr(const void *x, const void *y) { 182 278 const ygst_field *a = *(const ygst_field *const *)x; 183 278 const ygst_field *b = *(const ygst_field *const *)y; 184 278 return ygst_utf16_compare(a->name, b->name); 185 } 186 187 75int ygst_sign_payload(const ygst_fields *f, ygst_buf *out) { 188 const ygst_field **v; 189 75 size_t n = 0; 190 size_t i; 191 75 int first = 1; 192 75 if (f->oom) return -1; 193 75 v = (const ygst_field **)malloc((f->count ? f->count : 1) * sizeof(*v)); 194 75 if (!v) return -1; 195 341 for (i = 0; i < f->count; i++) { 196 266 if (f->items[i].value && f->items[i].value[0]) v[n++] = &f->items[i]; 197 } 198 75 qsort(v, n, sizeof(*v), cmp_field_ptr); 199 319 for (i = 0; i < n; i++) { 200 244 if (!first) ygst_buf_append_char(out, '&'); 201 244 first = 0; 202 244 ygst_buf_append_str(out, v[i]->name); 203 244 ygst_buf_append_char(out, '='); 204 244 ygst_buf_append_str(out, v[i]->value); 205 } 206 75 free(v); 207 75 ygst_buf_cstr(out); 208 75 return out->oom ? -1 : 0; 209 } 210 211 67int ygst_sign(const ygst_fields *f, const char *secret, char out[45]) { 212 ygst_buf payload; 213 unsigned char mac[32]; 214 int rc; 215 67 out[0] = 0; 216 67 if (!secret) return -1; 217 66 ygst_buf_init(&payload); 218 66 rc = ygst_sign_payload(f, &payload); 219 66 if (rc == 0) { 220 66 ygst_hmac_sha256(secret, strlen(secret), payload.data ? payload.data : (const unsigned char *)"", 221 payload.len, mac); 222 66 if (ygst_base64_encode(mac, sizeof mac, out, 45) != 44) rc = -1; 223 } 224 66 ygst_buf_free(&payload); 225 66 return rc; 226 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Minimal validating JSON scanner (RFC 8259). It never builds a tree: values are spans into the 6 * original text, so the platform result can be passed to the caller byte for byte. 7 */ 8 #include "ygst_core.h" 9 10 #include <stdlib.h> 11 #include <string.h> 12 13 #define YGST_JSON_MAX_DEPTH 128 14 15 5141943static const char *ws(const char *p, const char *e) { 16 11122986 while (p < e && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; 17 5141943 return p; 18 } 19 20 156static int hexval(char c) { 21 156 if (c >= '0' && c <= '9') return c - '0'; 22 27 if (c >= 'a' && c <= 'f') return c - 'a' + 10; 23 0 if (c >= 'A' && c <= 'F') return c - 'A' + 10; 24 0 return -1; 25 } 26 27 1411912static const char *skip_string(const char *p, const char *e) { 28 1411912 if (p >= e || *p != '"') return NULL; 29 1411909 p++; 30 10119336 while (p < e) { 31 10119334 unsigned char c = (unsigned char)*p; 32 10119334 if (c == '"') return p + 1; 33 8707430 if (c < 0x20) return NULL; 34 8707429 if (c == '\\') { 35 790 p++; 36 790 if (p >= e) return NULL; 37 790 switch (*p) { 38 765 case '"': 39 case '\\': 40 case '/': 41 case 'b': 42 case 'f': 43 case 'n': 44 case 'r': 45 case 't': 46 765 p++; 47 765 break; 48 24 case 'u': { 49 int i; 50 24 if (e - p < 5) return NULL; 51 115 for (i = 1; i <= 4; i++) { 52 92 if (hexval(p[i]) < 0) return NULL; 53 } 54 23 p += 5; 55 23 break; 56 } 57 1 default: 58 1 return NULL; 59 } 60 788 continue; 61 } 62 8706639 p++; 63 } 64 2 return NULL; 65 } 66 67 1286429static int is_digit(char c) { 68 1286429 return c >= '0' && c <= '9'; 69 } 70 71 393857static const char *skip_number(const char *p, const char *e) { 72 393857 if (p < e && *p == '-') p++; 73 393857 if (p >= e) return NULL; 74 393856 if (*p == '0') { 75 142261 p++; 76 251595 } else if (*p >= '1' && *p <= '9') { 77 824084 while (p < e && is_digit(*p)) p++; 78 } else { 79 0 return NULL; 80 } 81 393856 if (p < e && *p == '.') { 82 22181 p++; 83 22181 if (p >= e || !is_digit(*p)) return NULL; 84 53446 while (p < e && is_digit(*p)) p++; 85 } 86 393855 if (p < e && (*p == 'e' || *p == 'E')) { 87 8 p++; 88 8 if (p < e && (*p == '+' || *p == '-')) p++; 89 8 if (p >= e || !is_digit(*p)) return NULL; 90 19 while (p < e && is_digit(*p)) p++; 91 } 92 393854 return p; 93 } 94 95 234830static const char *skip_lit(const char *p, const char *e, const char *lit) { 96 234830 size_t n = strlen(lit); 97 234830 if ((size_t)(e - p) < n || memcmp(p, lit, n) != 0) return NULL; 98 234826 return p + n; 99 } 100 101 1269204static const char *skip_value(const char *p, const char *e, int depth) { 102 1269204 if (depth > YGST_JSON_MAX_DEPTH) return NULL; 103 1269203 p = ws(p, e); 104 1269203 if (p >= e) return NULL; 105 1269199 switch (*p) { 106 235553 case '{': 107 235553 p = ws(p + 1, e); 108 235553 if (p < e && *p == '}') return p + 1; 109 for (;;) { 110 985657 p = skip_string(ws(p, e), e); 111 985657 if (!p) return NULL; 112 985654 p = ws(p, e); 113 985654 if (p >= e || *p != ':') return NULL; 114 985653 p = skip_value(p + 1, e, depth + 1); 115 985653 if (!p) return NULL; 116 985651 p = ws(p, e); 117 985651 if (p >= e) return NULL; 118 985651 if (*p == ',') { 119 750159 p++; 120 750159 continue; 121 } 122 235492 if (*p == '}') return p + 1; 123 1 return NULL; 124 } 125 80744 case '[': 126 80744 p = ws(p + 1, e); 127 80744 if (p < e && *p == ']') return p + 1; 128 for (;;) { 129 157353 p = skip_value(p, e, depth + 1); 130 157353 if (!p) return NULL; 131 157222 p = ws(p, e); 132 157222 if (p >= e) return NULL; 133 157222 if (*p == ',') { 134 99109 p++; 135 99109 continue; 136 } 137 58113 if (*p == ']') return p + 1; 138 1 return NULL; 139 } 140 326035 case '"': 141 326035 return skip_string(p, e); 142 110678 case 't': 143 110678 return skip_lit(p, e, "true"); 144 124127 case 'f': 145 124127 return skip_lit(p, e, "false"); 146 25 case 'n': 147 25 return skip_lit(p, e, "null"); 148 392037 default: 149 392037 if (*p == '-' || is_digit(*p)) return skip_number(p, e); 150 6 return NULL; 151 } 152 } 153 154 631int ygst_json_parse(const char *s, size_t n, ygst_span *root) { 155 const char *e; 156 const char *start; 157 const char *end; 158 631 if (!s) return -1; 159 630 e = s + n; 160 630 start = ws(s, e); 161 630 end = skip_value(start, e, 0); 162 630 if (!end) return -1; 163 602 if (ws(end, e) != e) return -1; 164 600 if (root) { 165 600 root->p = start; 166 600 root->n = (size_t)(end - start); 167 } 168 600 return 0; 169 } 170 171 223087int ygst_json_type(ygst_span v) { 172 223087 if (!v.p || v.n == 0) return 0; 173 223086 switch (v.p[0]) { 174 145241 case '{': 175 145241 return 'o'; 176 54999 case '[': 177 54999 return 'a'; 178 6825 case '"': 179 6825 return 's'; 180 6566 case 't': 181 6566 return 't'; 182 6503 case 'f': 183 6503 return 'f'; 184 6 case 'n': 185 6 return 'z'; 186 2946 default: 187 2946 return (v.p[0] == '-' || is_digit(v.p[0])) ? 'n' : 0; 188 } 189 } 190 191 117837int ygst_json_object_next(ygst_span obj, size_t *pos, ygst_span *key, ygst_span *value) { 192 117837 const char *e = obj.p + obj.n; 193 const char *p; 194 const char *k; 195 const char *v; 196 117837 if (ygst_json_type(obj) != 'o') return -1; 197 117836 p = obj.p + (*pos ? *pos : 1); 198 117836 p = ws(p, e); 199 117836 if (p >= e) return -1; 200 117836 if (*p == '}') return 0; 201 95981 if (*pos) { 202 74133 if (*p != ',') return -1; 203 74133 p = ws(p + 1, e); 204 } 205 95981 k = p; 206 95981 p = skip_string(p, e); 207 95981 if (!p) return -1; 208 95981 if (key) { 209 95981 key->p = k; 210 95981 key->n = (size_t)(p - k); 211 } 212 95981 p = ws(p, e); 213 95981 if (p >= e || *p != ':') return -1; 214 95980 v = ws(p + 1, e); 215 95980 p = skip_value(v, e, 1); 216 95980 if (!p) return -1; 217 95980 if (value) { 218 95980 value->p = v; 219 95980 value->n = (size_t)(p - v); 220 } 221 95980 *pos = (size_t)(p - obj.p); 222 95980 return 1; 223 } 224 225 /* Compares a raw key token (with quotes, possibly escaped) with a plain key. */ 226 94141static int key_equals(ygst_span tok, const char *key) { 227 94141 size_t klen = strlen(key); 228 94141 if (tok.n >= 2 && memchr(tok.p, '\\', tok.n) == NULL) { 229 94136 return tok.n - 2 == klen && memcmp(tok.p + 1, key, klen) == 0; 230 } else { 231 ygst_buf b; 232 int eq; 233 5 ygst_buf_init(&b); 234 5 eq = ygst_json_string(tok, &b) == 0 && b.len == klen && memcmp(ygst_buf_cstr(&b), key, klen) == 0; 235 5 ygst_buf_free(&b); 236 5 return eq; 237 } 238 } 239 240 21588int ygst_json_member(ygst_span obj, const char *key, ygst_span *out) { 241 21588 size_t pos = 0; 242 ygst_span k, v; 243 21588 int found = 0; 244 int rc; 245 21588 if (ygst_json_type(obj) != 'o' || !key) return -1; 246 115728 while ((rc = ygst_json_object_next(obj, &pos, &k, &v)) == 1) { 247 94141 if (key_equals(k, key)) { 248 20386 found = 1; 249 20386 if (out) *out = v; 250 } 251 } 252 21587 if (rc < 0) return -1; 253 21586 return found; 254 } 255 256 30083int ygst_json_array_next(ygst_span arr, size_t *pos, ygst_span *out) { 257 30083 const char *e = arr.p + arr.n; 258 const char *p; 259 const char *v; 260 30083 if (ygst_json_type(arr) != 'a') return -1; 261 30082 p = arr.p + (*pos ? *pos : 1); 262 30082 p = ws(p, e); 263 30082 if (p >= e) return -1; 264 30082 if (*p == ']') return 0; 265 29589 if (*pos) { 266 27016 if (*p != ',') return -1; 267 27015 p = ws(p + 1, e); 268 } 269 29588 v = p; 270 29588 p = skip_value(v, e, 1); 271 29588 if (!p) return -1; 272 29588 if (out) { 273 29588 out->p = v; 274 29588 out->n = (size_t)(p - v); 275 } 276 29588 *pos = (size_t)(p - arr.p); 277 29588 return 1; 278 } 279 280 14static void put_utf8(ygst_buf *out, uint32_t cp) { 281 char b[4]; 282 14 if (cp < 0x80) { 283 6 b[0] = (char)cp; 284 6 ygst_buf_append(out, b, 1); 285 8 } else if (cp < 0x800) { 286 1 b[0] = (char)(0xC0 | (cp >> 6)); 287 1 b[1] = (char)(0x80 | (cp & 0x3F)); 288 1 ygst_buf_append(out, b, 2); 289 7 } else if (cp < 0x10000) { 290 6 b[0] = (char)(0xE0 | (cp >> 12)); 291 6 b[1] = (char)(0x80 | ((cp >> 6) & 0x3F)); 292 6 b[2] = (char)(0x80 | (cp & 0x3F)); 293 6 ygst_buf_append(out, b, 3); 294 } else { 295 1 b[0] = (char)(0xF0 | (cp >> 18)); 296 1 b[1] = (char)(0x80 | ((cp >> 12) & 0x3F)); 297 1 b[2] = (char)(0x80 | ((cp >> 6) & 0x3F)); 298 1 b[3] = (char)(0x80 | (cp & 0x3F)); 299 1 ygst_buf_append(out, b, 4); 300 } 301 14} 302 303 16static uint32_t hex4(const char *p) { 304 16 return (uint32_t)((hexval(p[0]) << 12) | (hexval(p[1]) << 8) | (hexval(p[2]) << 4) | hexval(p[3])); 305 } 306 307 4240int ygst_json_string(ygst_span v, ygst_buf *out) { 308 const char *p; 309 const char *e; 310 4240 if (ygst_json_type(v) != 's' || skip_string(v.p, v.p + v.n) != v.p + v.n) return -1; 311 4238 p = v.p + 1; 312 4238 e = v.p + v.n - 1; 313 4321 while (p < e) { 314 4245 const char *run = p; 315 31027 while (p < e && *p != '\\') p++; 316 4245 if (p > run) ygst_buf_append(out, run, (size_t)(p - run)); 317 4245 if (p >= e) break; 318 83 p++; /* backslash */ 319 83 switch (*p) { 320 1 case 'b': 321 1 ygst_buf_append_char(out, '\b'); 322 1 p++; 323 1 break; 324 1 case 'f': 325 1 ygst_buf_append_char(out, '\f'); 326 1 p++; 327 1 break; 328 1 case 'n': 329 1 ygst_buf_append_char(out, '\n'); 330 1 p++; 331 1 break; 332 1 case 'r': 333 1 ygst_buf_append_char(out, '\r'); 334 1 p++; 335 1 break; 336 1 case 't': 337 1 ygst_buf_append_char(out, '\t'); 338 1 p++; 339 1 break; 340 14 case 'u': { 341 14 uint32_t cp = hex4(p + 1); 342 14 p += 5; 343 14 if (cp >= 0xD800 && cp <= 0xDBFF) { 344 5 if (e - p >= 6 && p[0] == '\\' && p[1] == 'u') { 345 2 uint32_t lo = hex4(p + 2); 346 2 if (lo >= 0xDC00 && lo <= 0xDFFF) { 347 1 cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); 348 1 p += 6; 349 } else { 350 1 cp = 0xFFFD; 351 } 352 } else { 353 1 cp = 0xFFFD; 354 } 355 11 } else if (cp >= 0xDC00 && cp <= 0xDFFF) { 356 1 cp = 0xFFFD; 357 } 358 14 put_utf8(out, cp); 359 14 break; 360 } 361 64 default: /* " \ / */ 362 64 ygst_buf_append_char(out, *p); 363 64 p++; 364 64 break; 365 } 366 } 367 4238 ygst_buf_cstr(out); 368 4238 return out->oom ? -1 : 0; 369 } 370 371 1828static int number_text(ygst_span v, char *tmp, size_t cap) { 372 1828 if (ygst_json_type(v) != 'n' || v.n >= cap) return -1; 373 1826 if (skip_number(v.p, v.p + v.n) != v.p + v.n) return -1; 374 1826 memcpy(tmp, v.p, v.n); 375 1826 tmp[v.n] = 0; 376 1826 return 0; 377 } 378 379 7int ygst_json_number(ygst_span v, double *out) { 380 char tmp[64]; 381 7 char *end = NULL; 382 7 if (number_text(v, tmp, sizeof tmp) != 0) return -1; 383 6 *out = strtod(tmp, &end); 384 6 if (end && *end) { 385 /* A locale with a decimal comma: retry with the separator it expects. */ 386 0 char *dot = strchr(tmp, '.'); 387 0 if (dot) { 388 0 *dot = ','; 389 0 *out = strtod(tmp, &end); 390 } 391 0 if (end && *end) return -1; 392 } 393 6 return 0; 394 } 395 396 1821int ygst_json_int(ygst_span v, long long *out) { 397 char tmp[64]; 398 size_t i; 399 1821 int neg = 0; 400 1821 long long acc = 0; 401 1821 if (number_text(v, tmp, sizeof tmp) != 0) return -1; 402 1820 if (strpbrk(tmp, ".eE")) { 403 double d; 404 4 if (ygst_json_number(v, &d) != 0) return -1; 405 4 if (d != d || d > 9.2e18 || d < -9.2e18 || d != (double)(long long)d) return -1; 406 2 *out = (long long)d; 407 2 return 0; 408 } 409 1816 i = 0; 410 1816 if (tmp[0] == '-') { 411 1 neg = 1; 412 1 i = 1; 413 } 414 6311 for (; tmp[i]; i++) { 415 4496 int dgt = tmp[i] - '0'; 416 4496 if (acc > (9223372036854775807LL - dgt) / 10) return -1; 417 4495 acc = acc * 10 + dgt; 418 } 419 1815 *out = neg ? -acc : acc; 420 1815 return 0; 421 } 422 423 13069int ygst_json_bool(ygst_span v, int *out) { 424 13069 int t = ygst_json_type(v); 425 13069 if (t == 't' && v.n == 4) { 426 6565 *out = 1; 427 6565 return 0; 428 } 429 6504 if (t == 'f' && v.n == 5) { 430 6503 *out = 0; 431 6503 return 0; 432 } 433 1 return -1; 434 } 435 436 1568int ygst_json_escape_n(ygst_buf *out, const char *s, size_t n) { 437 static const char H[] = "0123456789abcdef"; 438 size_t i; 439 1568 size_t run = 0; 440 1568 ygst_buf_append_char(out, '"'); 441 24962 for (i = 0; i < n; i++) { 442 23394 unsigned char c = (unsigned char)s[i]; 443 23394 const char *rep = NULL; 444 char u[7]; 445 23394 if (c == '"') { 446 9 rep = "\\\""; 447 23385 } else if (c == '\\') { 448 2 rep = "\\\\"; 449 23383 } else if (c == '\n') { 450 3 rep = "\\n"; 451 23380 } else if (c == '\r') { 452 1 rep = "\\r"; 453 23379 } else if (c == '\t') { 454 1 rep = "\\t"; 455 23378 } else if (c == '\b') { 456 1 rep = "\\b"; 457 23377 } else if (c == '\f') { 458 1 rep = "\\f"; 459 23376 } else if (c < 0x20) { 460 2 u[0] = '\\'; 461 2 u[1] = 'u'; 462 2 u[2] = '0'; 463 2 u[3] = '0'; 464 2 u[4] = H[c >> 4]; 465 2 u[5] = H[c & 15]; 466 2 u[6] = 0; 467 2 rep = u; 468 } 469 23394 if (rep) { 470 20 if (i > run) ygst_buf_append(out, s + run, i - run); 471 20 ygst_buf_append_str(out, rep); 472 20 run = i + 1; 473 } 474 } 475 1568 if (n > run) ygst_buf_append(out, s + run, n - run); 476 1568 ygst_buf_append_char(out, '"'); 477 1568 return out->oom ? -1 : 0; 478 } 479 480 1568int ygst_json_escape(ygst_buf *out, const char *s) { 481 1568 if (!s) s = ""; 482 1568 return ygst_json_escape_n(out, s, strlen(s)); 483 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * multipart/form-data assembly (RFC 7578) for POST /{coreType}. Text fields carry no filename, 6 * so the platform treats them as form parameters and they are exactly the signed set; the audio 7 * is a file part and is not signed. 8 */ 9 #include "ygst_core.h" 10 11 #include <stdio.h> 12 #include <string.h> 13 14 37int ygst_multipart_boundary(const unsigned char rnd[16], char out[64]) { 15 char hex[33]; 16 37 ygst_hex_lower(rnd, 16, hex); 17 37 snprintf(out, 64, "----YuguSTCompat%s", hex); 18 37 return 0; 19 } 20 21 38int ygst_multipart_content_type(const char *boundary, char *out, size_t cap) { 22 int n; 23 38 if (!boundary || !out) return -1; 24 38 n = snprintf(out, cap, "multipart/form-data; boundary=%s", boundary); 25 38 return (n < 0 || (size_t)n >= cap) ? -1 : 0; 26 } 27 28 119static int contains(const unsigned char *hay, size_t n, const char *needle) { 29 119 size_t m = strlen(needle); 30 size_t i; 31 119 if (m == 0 || n < m) return 0; 32 2591927 for (i = 0; i + m <= n; i++) { 33 2591886 if (hay[i] == (unsigned char)needle[0] && memcmp(hay + i, needle, m) == 0) return 1; 34 } 35 41 return 0; 36 } 37 38 44int ygst_multipart_build(const ygst_fields *fields, const char *boundary, const char *file_field, 39 const char *filename, const char *file_content_type, const unsigned char *file, 40 size_t file_len, ygst_buf *out) { 41 size_t i; 42 44 if (!boundary || !boundary[0] || strlen(boundary) > 70 || !file_field || !filename) return -1; 43 42 if (!ygst_field_name_ok(file_field) || !ygst_field_name_ok(filename)) return -1; 44 121 for (i = 0; i < fields->count; i++) { 45 82 const char *v = fields->items[i].value ? fields->items[i].value : ""; 46 82 if (!ygst_field_name_ok(fields->items[i].name)) return -1; 47 81 if (contains((const unsigned char *)v, strlen(v), boundary)) return -2; 48 } 49 39 if (file && contains(file, file_len, boundary)) return -2; 50 118 for (i = 0; i < fields->count; i++) { 51 80 ygst_buf_append_str(out, "--"); 52 80 ygst_buf_append_str(out, boundary); 53 80 ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\""); 54 80 ygst_buf_append_str(out, fields->items[i].name); 55 80 ygst_buf_append_str(out, "\"\r\n\r\n"); 56 80 ygst_buf_append_str(out, fields->items[i].value ? fields->items[i].value : ""); 57 80 ygst_buf_append_str(out, "\r\n"); 58 } 59 38 ygst_buf_append_str(out, "--"); 60 38 ygst_buf_append_str(out, boundary); 61 38 ygst_buf_append_str(out, "\r\nContent-Disposition: form-data; name=\""); 62 38 ygst_buf_append_str(out, file_field); 63 38 ygst_buf_append_str(out, "\"; filename=\""); 64 38 ygst_buf_append_str(out, filename); 65 38 ygst_buf_append_str(out, "\"\r\nContent-Type: "); 66 38 ygst_buf_append_str(out, file_content_type ? file_content_type : "application/octet-stream"); 67 38 ygst_buf_append_str(out, "\r\n\r\n"); 68 38 if (file_len) ygst_buf_append(out, file, file_len); 69 38 ygst_buf_append_str(out, "\r\n--"); 70 38 ygst_buf_append_str(out, boundary); 71 38 ygst_buf_append_str(out, "--\r\n"); 72 38 return out->oom ? -1 : 0; 73 } 74 75 /* Upload extension for an audioType or a file extension. Unknown types upload as-is. */ 76 53const char *ygst_audio_ext(const char *audio_type) { 77 static const char *const KNOWN[] = {"wav", "mp3", "ogg", "amr", "flv", "opus", "aac", 78 "spx", "speex", "silk", "slk", "m4a", "pcm"}; 79 char t[16]; 80 size_t i; 81 53 if (ygst_is_blank(audio_type)) return "wav"; 82 51 ygst_trim_copy(audio_type, t, sizeof t); 83 51 if (t[0] == '.') memmove(t, t + 1, strlen(t)); 84 108 for (i = 0; i < sizeof KNOWN / sizeof KNOWN[0]; i++) { 85 105 if (ygst_strcaseeq(t, KNOWN[i])) return strcmp(KNOWN[i], "pcm") == 0 ? "wav" : KNOWN[i]; 86 } 87 3 if (ygst_strcaseeq(t, "wave")) return "wav"; 88 2 if (ygst_strcaseeq(t, "mpeg")) return "mp3"; 89 1 return "bin"; 90 } 91 92 47const char *ygst_audio_content_type(const char *ext) { 93 47 if (!ext) return "application/octet-stream"; 94 46 if (ygst_strcaseeq(ext, "wav")) return "audio/wav"; 95 9 if (ygst_strcaseeq(ext, "mp3")) return "audio/mpeg"; 96 8 if (ygst_strcaseeq(ext, "ogg") || ygst_strcaseeq(ext, "spx") || ygst_strcaseeq(ext, "speex")) return "audio/ogg"; 97 6 if (ygst_strcaseeq(ext, "opus")) return "audio/opus"; 98 5 if (ygst_strcaseeq(ext, "amr")) return "audio/amr"; 99 4 if (ygst_strcaseeq(ext, "aac")) return "audio/aac"; 100 3 if (ygst_strcaseeq(ext, "m4a")) return "audio/mp4"; 101 2 if (ygst_strcaseeq(ext, "flv")) return "video/x-flv"; 102 1 return "application/octet-stream"; 103 } 104 105 4int ygst_audio_type_is_pcm(const char *audio_type) { 106 char t[16]; 107 4 if (ygst_is_blank(audio_type)) return 1; 108 3 ygst_trim_copy(audio_type, t, sizeof t); 109 6 return ygst_strcaseeq(t, "wav") || ygst_strcaseeq(t, "pcm") || ygst_strcaseeq(t, "wave") || 110 6 ygst_strcaseeq(t, ".wav") || ygst_strcaseeq(t, ".pcm"); 111 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * coreType resolution and the mapping from KYTestConfig properties to the form fields of 6 * POST /{coreType}. The table mirrors DESIGN 6.1 so that iOS and Android send the same fields. 7 */ 8 #include "ygst_core.h" 9 10 #include <math.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 15 /* KYTestType in declaration order (KYTestConfig.h). NULL: Shengtong publishes no string. */ 16 static const char *const KY_TEST_TYPES[] = { 17 "word.eval", /* KYTestType_Word */ 18 "sent.eval", /* KYTestType_Sentence */ 19 "para.eval", /* KYTestType_Paragraph */ 20 "open.eval", /* KYTestType_Open */ 21 "choice.rec", /* KYTestType_Choice */ 22 "asr.rec", /* KYTestType_Asr */ 23 "align.eval", /* KYTestType_Align */ 24 "word.eval.pro", /* KYTestType_Word_Pro */ 25 "sent.eval.pro", /* KYTestType_Sentence_Pro */ 26 "word.eval.cn", /* KYTestType_Word_Cn */ 27 "sent.eval.cn", /* KYTestType_Sentence_Cn */ 28 "para.eval.cn", /* KYTestType_Paragraph_Cn */ 29 "asr.eval", /* KYTestType_AsrEval */ 30 "word.eval.fr", /* KYTestType_Word_fr */ 31 "sent.eval.fr", /* KYTestType_Sentence_fr */ 32 "para.eval.fr", /* KYTestType_Paragraph_fr */ 33 "word.eval.kr", /* KYTestType_Word_kr */ 34 "sent.eval.kr", /* KYTestType_Sentence_kr */ 35 "para.eval.kr", /* KYTestType_Paragraph_kr */ 36 "word.eval.jp", /* KYTestType_Word_jp */ 37 "sent.eval.jp", /* KYTestType_Sentence_jp */ 38 "para.eval.jp", /* KYTestType_Paragraph_jp */ 39 NULL /* KYTestType_Wordspell */ 40 }; 41 42 /* coreTypes the platform evaluates on POST /{coreType} (DESIGN 6.1). */ 43 static const char *const SUPPORTED[] = {"word.eval", "word.eval.pro", "sent.eval", "sent.eval.pro", 44 "para.eval", "word.eval.cn", "sent.eval.cn", "para.eval.cn", 45 "alpha.eval", "pinyin"}; 46 47 /* Shengtong coreTypes that are answered with errId 60003 without network I/O. */ 48 static const char *const UNSUPPORTED[] = {"choice.rec", "open.eval", "asr.rec", "asr.eval", 49 "align.eval", "word.eval.fr", "sent.eval.fr", "para.eval.fr", 50 "word.eval.jp", "sent.eval.jp", "para.eval.jp", "word.eval.kr", 51 "sent.eval.kr", "para.eval.kr"}; 52 53 54const char *ygst_coretype_for_enum(unsigned long kytesttype) { 54 54 if (kytesttype >= sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]) return NULL; 55 51 return KY_TEST_TYPES[kytesttype]; 56 } 57 58 25size_t ygst_coretype_enum_count(void) { 59 25 return sizeof KY_TEST_TYPES / sizeof KY_TEST_TYPES[0]; 60 } 61 62 108ygst_ct_status ygst_coretype_check(const char *core_type) { 63 size_t i; 64 108 if (!core_type || !core_type[0]) return YGST_CT_UNKNOWN; 65 824 for (i = 0; i < sizeof SUPPORTED / sizeof SUPPORTED[0]; i++) { 66 789 if (strcmp(core_type, SUPPORTED[i]) == 0) return YGST_CT_SUPPORTED; 67 } 68 279 for (i = 0; i < sizeof UNSUPPORTED / sizeof UNSUPPORTED[0]; i++) { 69 275 if (strcmp(core_type, UNSUPPORTED[i]) == 0) return YGST_CT_UNSUPPORTED; 70 } 71 4 return YGST_CT_UNKNOWN; 72 } 73 74 62ygst_ct_status ygst_coretype_resolve(const char *core_type_ns, unsigned long kytesttype, char *out, size_t cap) { 75 const char *mapped; 76 62 if (!out || cap == 0) return YGST_CT_UNKNOWN; 77 61 out[0] = 0; 78 61 if (!ygst_is_blank(core_type_ns)) { 79 55 ygst_trim_copy(core_type_ns, out, cap); 80 55 return ygst_coretype_check(out); 81 } 82 6 mapped = ygst_coretype_for_enum(kytesttype); 83 6 if (!mapped) return YGST_CT_UNSUPPORTED; 84 4 if (strlen(mapped) >= cap) return YGST_CT_UNKNOWN; 85 3 strcpy(out, mapped); 86 3 return ygst_coretype_check(out); 87 } 88 89 9int ygst_coretype_is_chinese(const char *core_type) { 90 9 return core_type && (ygst_has_suffix_ci(core_type, ".cn") || strcmp(core_type, "pinyin") == 0); 91 } 92 93 176int ygst_coretype_is_paragraph(const char *core_type) { 94 176 return core_type && strncmp(core_type, "para.", 5) == 0; 95 } 96 97 88int ygst_fields_apply_coretype_rules(ygst_fields *f, const char *core_type) { 98 88 if (ygst_coretype_is_paragraph(core_type)) return ygst_fields_set(f, "paragraph_need_word_score", "1"); 99 81 return 0; 100 } 101 102 60int ygst_validate_request(ygst_ct_status status, const char *core_type, const char *ref_text, 103 const char *ref_pinyin) { 104 60 if (status != YGST_CT_SUPPORTED) return YGST_ERRID_CORETYPE_UNSUPPORTED; 105 55 if (!ygst_is_blank(ref_text)) return 0; 106 7 if (!ygst_is_blank(ref_pinyin) && ygst_coretype_is_chinese(core_type)) return 0; 107 4 return YGST_ERRID_REFTEXT_EMPTY; 108 } 109 110 /* ------------------------------------------------------------------ number text */ 111 112 69static void fix_decimal_point(char *s) { 113 431 for (; *s; s++) { 114 362 if (*s == ',') *s = '.'; 115 } 116 69} 117 118 3static void trim_fraction_zeros(char *s) { 119 3 char *dot = strchr(s, '.'); 120 char *end; 121 3 if (!dot) return; 122 3 end = s + strlen(s); 123 3 while (end > dot + 1 && end[-1] == '0') end--; 124 3 if (end == dot + 1) end = dot; 125 3 *end = 0; 126 } 127 128 28int ygst_fmt_double(double v, char *out, size_t cap) { 129 char tmp[64]; 130 int p; 131 28 if (!out || cap == 0) return -1; 132 27 out[0] = 0; 133 27 if (v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308) return -1; /* NaN, inf */ 134 25 if (v == 0) { 135 2 tmp[0] = '0'; 136 2 tmp[1] = 0; 137 23 } else if (fabs(v) < 9.0e15 && v == floor(v)) { 138 11 snprintf(tmp, sizeof tmp, "%.0f", v); 139 12 } else if (fabs(v) >= 9.0e15) { 140 1 snprintf(tmp, sizeof tmp, "%.0f", v); 141 } else { 142 41 for (p = 1; p <= 17; p++) { 143 41 char *end = NULL; 144 double back; 145 41 snprintf(tmp, sizeof tmp, "%.*g", p, v); 146 41 fix_decimal_point(tmp); 147 41 back = strtod(tmp, &end); 148 41 if (end && *end) { 149 /* decimal comma locale: compare through the locale's own separator */ 150 char alt[64]; 151 char *dot; 152 0 strcpy(alt, tmp); 153 0 dot = strchr(alt, '.'); 154 0 if (dot) *dot = ','; 155 0 back = strtod(alt, NULL); 156 } 157 41 if (back == v) break; 158 } 159 11 if (strchr(tmp, 'e') || strchr(tmp, 'E')) { 160 /* plain decimal: enough fraction digits to keep the significant ones */ 161 3 int exp10 = (int)floor(log10(fabs(v))); 162 3 int digits = (p > 17 ? 17 : p) - 1 - exp10; 163 3 if (digits < 0) digits = 0; 164 3 if (digits > 40) digits = 40; 165 3 snprintf(tmp, sizeof tmp, "%.*f", digits, v); 166 3 fix_decimal_point(tmp); 167 3 trim_fraction_zeros(tmp); 168 } 169 } 170 25 fix_decimal_point(tmp); 171 25 if (strcmp(tmp, "-0") == 0) strcpy(tmp, "0"); 172 25 if (strlen(tmp) >= cap) return -1; 173 24 strcpy(out, tmp); 174 24 return 0; 175 } 176 177 178int ygst_field_name_ok(const char *name) { 178 178 if (!name || !name[0]) return 0; 179 1716 for (; *name; name++) { 180 1545 if (*name == '"' || *name == '\r' || *name == '\n') return 0; 181 } 182 171 return 1; 183 } 184 185 /* ------------------------------------------------------------------ mapping */ 186 187 81void ygst_test_params_init(ygst_test_params *p) { 188 81 memset(p, 0, sizeof *p); 189 81} 190 191 863static int set_str(ygst_fields *f, const char *name, const char *v) { 192 863 if (!v || !v[0]) return 0; 193 67 return ygst_fields_set(f, name, v); 194 } 195 196 602static int set_flag(ygst_fields *f, const char *name, int on) { 197 602 return on ? ygst_fields_set(f, name, "1") : 0; 198 } 199 200 431static int set_num(ygst_fields *f, const char *name, double v) { 201 char tmp[64]; 202 431 if (v == 0 || ygst_fmt_double(v, tmp, sizeof tmp) != 0) return 0; 203 12 return ygst_fields_set(f, name, tmp); 204 } 205 206 3static int set_int(ygst_fields *f, const char *name, long v) { 207 char tmp[32]; 208 3 snprintf(tmp, sizeof tmp, "%ld", v); 209 3 return ygst_fields_set(f, name, tmp); 210 } 211 212 86int ygst_params_to_fields(const ygst_test_params *p, ygst_fields *out) { 213 static const char *const DICT[] = {NULL, "CMU", "KK", "IPA88"}; 214 86 int rc = 0; 215 86 rc |= set_str(out, "refText", p->ref_text); 216 86 rc |= set_str(out, "refPinyin", p->ref_pinyin); 217 86 if (p->age_group >= 1 && p->age_group <= 3) rc |= set_int(out, "agegroup", p->age_group); 218 86 rc |= set_num(out, "scale", p->scale); 219 86 rc |= set_num(out, "precision", p->precision); 220 86 rc |= set_num(out, "slack", p->slack); 221 86 rc |= set_flag(out, "paragraph_need_word_score", 222 86 p->paragraph_need_word_score || ygst_coretype_is_paragraph(p->core_type)); 223 86 rc |= set_flag(out, "phoneme_output", p->phoneme_output); 224 86 rc |= set_flag(out, "attachAudioUrl", p->attach_audio_url); 225 86 if (p->phoneme_option >= 1 && p->phoneme_option <= 3) rc |= set_str(out, "dict_type", DICT[p->phoneme_option]); 226 86 rc |= set_str(out, "dict_dialect", p->dict_dialect); 227 86 rc |= set_str(out, "customized_lexicon", p->customized_lexicon_json); 228 86 rc |= set_str(out, "customized_pron", p->customized_pron_json); 229 86 if (p->readtype_diagnosis != 0) rc |= set_num(out, "readtype_diagnosis", p->readtype_diagnosis); 230 86 rc |= set_flag(out, "output_rawtext", p->output_rawtext); 231 86 rc |= set_flag(out, "punctuate", p->punctuate); 232 86 rc |= set_num(out, "itn", p->itn); 233 86 rc |= set_flag(out, "detect_nonscorable", p->detect_nonscorable); 234 /* KYTestConfig.vad_detection travels under the Android RecordSetting name. */ 235 86 rc |= set_flag(out, "vad_detction", p->vad_detection); 236 86 rc |= set_str(out, "keywords", p->keywords); 237 86 rc |= set_str(out, "keypoints", p->keypoints_json); 238 86 rc |= set_num(out, "keypoints_weight", p->keypoints_weight); 239 86 rc |= set_str(out, "negative_keypoints", p->negative_keypoints_json); 240 86 rc |= set_str(out, "negativeReftext", p->negative_reftext); 241 86 if (p->mode == 1) rc |= ygst_fields_set(out, "mode", "home"); 242 86 if (p->q_type != 0) rc |= set_int(out, "qType", p->q_type); 243 /* customParams in the order of the Android RecordSetting newParams: a name that is already 244 * mapped keeps its position and takes the new value; request comes last */ 245 86 if (p->custom_params) { 246 size_t i; 247 57 for (i = 0; i < p->custom_params->count; i++) { 248 9 const ygst_field *cp = &p->custom_params->items[i]; 249 9 if (!ygst_field_name_ok(cp->name) || !cp->value || !cp->value[0]) continue; 250 6 rc |= ygst_fields_set(out, cp->name, cp->value); 251 } 252 } 253 86 rc |= set_str(out, "request", p->request_json); 254 /* customParams cannot switch the paragraph word details off */ 255 86 rc |= ygst_fields_apply_coretype_rules(out, p->core_type); 256 86 return (rc != 0 || out->oom) ? -1 : 0; 257 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Realtime feedback (2.1.0, DESIGN 14.3, CONTRACT 5.2 and 5.5): the compat WebSocket round of one 6 * evaluation. Handshake URL and its signature, parameter frame, classification of platform 7 * frames, the words dedupe of the reconnect rule and the round controller that decides 8 * connecting, streaming, ending, falling back to REST before end and reconnecting after end. 9 */ 10 #include "ygst_core.h" 11 12 #include <stdio.h> 13 #include <stdlib.h> 14 #include <string.h> 15 16 /* ------------------------------------------------------------------ URL */ 17 18 198int ygst_url_encode(ygst_buf *out, const char *s) { 19 static const char H[] = "0123456789ABCDEF"; 20 198 const unsigned char *p = (const unsigned char *)(s ? s : ""); 21 3231 for (; *p; p++) { 22 3033 unsigned char c = *p; 23 3033 if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || 24 73 c == '_' || c == '~') { 25 2961 ygst_buf_append_char(out, (char)c); 26 } else { 27 char e[3]; 28 72 e[0] = '%'; 29 72 e[1] = H[c >> 4]; 30 72 e[2] = H[c & 15]; 31 72 ygst_buf_append(out, e, 3); 32 } 33 } 34 198 ygst_buf_cstr(out); 35 198 return out->oom ? -1 : 0; 36 } 37 38 57static int starts_ci(const char *s, const char *prefix) { 39 316 while (*prefix) { 40 290 char a = *s; 41 290 if (a >= 'A' && a <= 'Z') a = (char)(a + 32); 42 290 if (a != *prefix) return 0; 43 259 s++; 44 259 prefix++; 45 } 46 26 return 1; 47 } 48 49 30int ygst_ws_base_url(const char *rest_base, char *out, size_t cap) { 50 const char *scheme; 51 const char *rest; 52 size_t n; 53 int w; 54 30 if (!out || cap == 0) return -1; 55 29 out[0] = 0; 56 29 if (!rest_base) return -1; 57 28 if (starts_ci(rest_base, "https://")) { 58 6 scheme = "wss://"; 59 6 rest = rest_base + 8; 60 22 } else if (starts_ci(rest_base, "http://")) { 61 18 scheme = "ws://"; 62 18 rest = rest_base + 7; 63 4 } else if (starts_ci(rest_base, "wss://")) { 64 1 scheme = "wss://"; 65 1 rest = rest_base + 6; 66 3 } else if (starts_ci(rest_base, "ws://")) { 67 1 scheme = "ws://"; 68 1 rest = rest_base + 5; 69 } else { 70 2 return -1; 71 } 72 26 n = strlen(rest); 73 30 while (n > 0 && rest[n - 1] == '/') n--; 74 26 if (n == 0 || rest[0] == '/') return -1; 75 24 w = snprintf(out, cap, "%s%.*s", scheme, (int)n, rest); 76 24 if (w < 0 || (size_t)w >= cap) { 77 1 out[0] = 0; 78 1 return -1; 79 } 80 23 return 0; 81 } 82 83 /* Query parameters the handshake sets itself. */ 84 9static int own_query_name(const char *n) { 85 8 return strcmp(n, "appKey") == 0 || strcmp(n, "timestamp") == 0 || strcmp(n, "nonce") == 0 || 86 17 strcmp(n, "idempotencyKey") == 0 || strcmp(n, "signature") == 0; 87 } 88 89 25int ygst_ws_handshake_url(const char *rest_base, const char *core_type, const char *app_key, const char *secret, 90 int64_t timestamp_sec, const char *nonce, const char *idempotency_key, 91 const ygst_fields *extra, ygst_buf *out) { 92 char base[1024]; 93 char ts[32]; 94 char sig[45]; 95 ygst_fields q; 96 size_t i; 97 25 int rc = -1; 98 25 if (!out || !core_type || !core_type[0] || ygst_is_blank(app_key) || !secret || !secret[0] || !nonce || 99 21 !nonce[0] || !ygst_idempotency_key_ok(idempotency_key)) { 100 6 return -1; 101 } 102 19 if (ygst_ws_base_url(rest_base, base, sizeof base) != 0) return -1; 103 18 snprintf(ts, sizeof ts, "%lld", (long long)timestamp_sec); 104 18 ygst_fields_init(&q); 105 18 ygst_fields_set(&q, "appKey", app_key); 106 18 ygst_fields_set(&q, "timestamp", ts); 107 18 ygst_fields_set(&q, "nonce", nonce); 108 18 ygst_fields_set(&q, "idempotencyKey", idempotency_key); 109 18 if (extra) { 110 27 for (i = 0; i < extra->count; i++) { 111 10 const ygst_field *f = &extra->items[i]; 112 10 if (!f->name || !f->name[0] || !f->value || !f->value[0] || own_query_name(f->name)) continue; 113 7 ygst_fields_set(&q, f->name, f->value); 114 } 115 } 116 18 if (q.oom || ygst_sign(&q, secret, sig) != 0) goto done; 117 18 ygst_buf_append_str(out, base); 118 18 ygst_buf_append_char(out, '/'); 119 18 ygst_url_encode(out, core_type); 120 97 for (i = 0; i < q.count; i++) { 121 79 ygst_buf_append_char(out, i == 0 ? '?' : '&'); 122 79 ygst_url_encode(out, q.items[i].name); 123 79 ygst_buf_append_char(out, '='); 124 79 ygst_url_encode(out, q.items[i].value); 125 } 126 18 ygst_buf_append_str(out, "&signature="); 127 18 ygst_url_encode(out, sig); 128 18 ygst_buf_cstr(out); 129 18 rc = out->oom ? -1 : 0; 130 18done: 131 18 ygst_fields_free(&q); 132 18 return rc; 133 } 134 135 /* ------------------------------------------------------------------ parameter frame */ 136 137 /* The platform reads these names as commands (cmd, end, data) or sets them from its own fields. */ 138 43static int reserved_frame_name(const char *n) { 139 42 return strcmp(n, "cmd") == 0 || strcmp(n, "end") == 0 || strcmp(n, "data") == 0 || 140 85 strcmp(n, "realtime_feedback") == 0 || strcmp(n, "idempotencyKey") == 0; 141 } 142 143 21int ygst_ws_param_frame(const ygst_fields *fields, const char *idempotency_key, ygst_buf *out) { 144 size_t i; 145 21 int skipped = 0; 146 21 if (!fields || !out || fields->oom || !ygst_idempotency_key_ok(idempotency_key)) return -1; 147 19 ygst_buf_append_char(out, '{'); 148 62 for (i = 0; i < fields->count; i++) { 149 43 const ygst_field *f = &fields->items[i]; 150 43 if (!f->name || reserved_frame_name(f->name)) { 151 5 skipped++; 152 5 continue; 153 } 154 38 ygst_json_escape(out, f->name); 155 38 ygst_buf_append_char(out, ':'); 156 38 ygst_json_escape(out, f->value ? f->value : ""); 157 38 ygst_buf_append_char(out, ','); 158 } 159 19 ygst_buf_append_str(out, "\"realtime_feedback\":true,\"idempotencyKey\":"); 160 19 ygst_json_escape(out, idempotency_key); 161 19 ygst_buf_append_char(out, '}'); 162 19 ygst_buf_cstr(out); 163 19 return out->oom ? -1 : skipped; 164 } 165 166 /* ------------------------------------------------------------------ frames */ 167 168 /* An integer member, also when a gateway quoted it. 1 found, 0 absent or not an integer. */ 169 1063static int int_member(ygst_span obj, const char *key, long long *out) { 170 ygst_span v; 171 1063 if (ygst_json_member(obj, key, &v) != 1) return 0; 172 1019 if (ygst_json_type(v) == 's') { 173 ygst_buf b; 174 6 int ok = 0; 175 6 ygst_buf_init(&b); 176 6 if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 20) { 177 6 char *end = NULL; 178 6 long long x = strtoll(ygst_buf_cstr(&b), &end, 10); 179 6 if (end && *end == 0) { 180 6 *out = x; 181 6 ok = 1; 182 } 183 } 184 6 ygst_buf_free(&b); 185 6 return ok; 186 } 187 1013 return ygst_json_int(v, out) == 0; 188 } 189 190 8285static int bool_member(ygst_span obj, const char *key) { 191 ygst_span v; 192 8285 int b = 0; 193 8285 if (ygst_json_member(obj, key, &v) != 1 || ygst_json_bool(v, &b) != 0) return 0; 194 8265 return b; 195 } 196 197 596static int count_value(long long v) { 198 596 if (v < 0) return -1; 199 596 return v > 2147483647LL ? 2147483647 : (int)v; 200 } 201 202 /* The event name when it is a short JSON string, else "". */ 203 130static void event_name(ygst_span ev, char *out, size_t cap) { 204 ygst_buf b; 205 130 out[0] = 0; 206 130 if (ygst_json_type(ev) != 's') return; 207 129 ygst_buf_init(&b); 208 129 if (ygst_json_string(ev, &b) == 0 && b.len < cap) memcpy(out, ygst_buf_cstr(&b), b.len + 1); 209 129 ygst_buf_free(&b); 210 } 211 212 203static void parse_progress(ygst_ws_frame *f) { 213 ygst_span r, list, item; 214 long long v; 215 203 size_t pos = 0; 216 203 int read = 0, matched = 0, total = 0; 217 203 if (ygst_json_member(f->root, "result", &r) != 1 || ygst_json_type(r) != 'o') return; 218 202 f->result = r; 219 202 if (int_member(r, "bytes", &v) && v >= 0) f->bytes = v; 220 202 f->read_units = int_member(r, "readUnits", &v) ? count_value(v) : -1; 221 202 f->matched_units = int_member(r, "matchedUnits", &v) ? count_value(v) : -1; 222 202 f->total_units = int_member(r, "totalUnits", &v) ? count_value(v) : -1; 223 /* every unit with its state */ 224 202 if (ygst_json_member(r, "progress", &list) == 1 && ygst_json_type(list) == 'a') { 225 191 f->has_progress = 1; 226 4323 while (ygst_json_array_next(list, &pos, &item) == 1) { 227 4132 total++; 228 4132 if (ygst_json_type(item) != 'o') continue; 229 4131 if (bool_member(item, "read")) read++; 230 4131 if (bool_member(item, "matched")) matched++; 231 } 232 191 if (f->read_units < 0) f->read_units = read; 233 191 if (f->matched_units < 0) f->matched_units = matched; 234 191 if (f->total_units < 0) f->total_units = total; 235 } 236 /* Shengtong items of the units read since the previous frame */ 237 202 if (ygst_json_member(r, "words", &list) == 1 && ygst_json_type(list) == 'a') { 238 169 f->has_words = 1; 239 169 f->words = list; 240 169 pos = 0; 241 481 while (ygst_json_array_next(list, &pos, &item) == 1) f->word_count++; 242 } 243 } 244 245 23static void parse_final(ygst_ws_frame *f) { 246 ygst_span r; 247 23 if (ygst_json_member(f->root, "result", &r) == 1) f->result = r; 248 23 f->replayed = bool_member(f->root, "replayed"); 249 23} 250 251 360int ygst_ws_frame_parse(const char *text, size_t n, ygst_ws_frame *f) { 252 ygst_span root, v; 253 360 if (!f) return -1; 254 359 memset(f, 0, sizeof *f); 255 359 f->kind = YGST_WSF_INVALID; 256 359 f->bytes = -1; 257 359 f->read_units = -1; 258 359 f->matched_units = -1; 259 359 f->total_units = -1; 260 359 if (!text || ygst_json_parse(text, n, &root) != 0 || ygst_json_type(root) != 'o') return -1; 261 355 f->root = root; 262 355 if (ygst_json_member(root, "event", &v) == 1) { 263 char ev[16]; 264 130 event_name(v, ev, sizeof ev); 265 130 if (strcmp(ev, "connected") == 0) { 266 53 f->kind = YGST_WSF_CONNECTED; 267 77 } else if (strcmp(ev, "started") == 0) { 268 53 f->kind = YGST_WSF_STARTED; 269 24 } else if (strcmp(ev, "pong") == 0) { 270 2 f->kind = YGST_WSF_PONG; 271 22 } else if (strcmp(ev, "error") == 0) { 272 18 long long code = 0; 273 18 f->kind = YGST_WSF_ERROR; 274 18 if (int_member(root, "code", &code) && code > 0) f->code = count_value(code); 275 4 } else if (strcmp(ev, "result") == 0) { 276 2 f->kind = YGST_WSF_FINAL; 277 2 parse_final(f); 278 } else { 279 2 f->kind = YGST_WSF_OTHER; 280 } 281 130 return 0; 282 } 283 225 if (ygst_json_member(root, "eof", &v) == 1) { 284 222 long long eof = -1; 285 222 if (int_member(root, "eof", &eof) && eof == 0) { 286 203 f->kind = YGST_WSF_PROGRESS; 287 203 parse_progress(f); 288 } else { 289 19 f->kind = YGST_WSF_FINAL; 290 19 parse_final(f); 291 } 292 222 return 0; 293 } 294 3 if (ygst_json_member(root, "result", NULL) == 1 || ygst_json_member(root, "recordId", NULL) == 1) { 295 2 f->kind = YGST_WSF_FINAL; 296 2 parse_final(f); 297 2 return 0; 298 } 299 1 f->kind = YGST_WSF_OTHER; 300 1 return 0; 301 } 302 303 15int ygst_ws_classify_error(ygst_span frame, ygst_classification *c) { 304 static const char *const KEYS[] = {"message", "msg", "error", NULL}; 305 const char *const *k; 306 15 long long code = 0; 307 15 c->success = 0; 308 15 c->http_status = 0; 309 15 c->local_code = 0; 310 15 c->biz_code = 0; 311 15 c->has_audio_url = 0; 312 15 c->has_record_id = 0; 313 15 c->record_id_raw.p = NULL; 314 15 c->record_id_raw.n = 0; 315 15 c->result.p = NULL; 316 15 c->result.n = 0; 317 15 ygst_buf_reset(&c->record_id); 318 15 ygst_buf_reset(&c->audio_url); 319 15 ygst_buf_reset(&c->message); 320 15 if (ygst_json_type(frame) == 'o') { 321 15 if (int_member(frame, "code", &code) && code > 0) c->biz_code = count_value(code); 322 19 for (k = KEYS; *k; k++) { 323 ygst_span v; 324 18 if (ygst_json_member(frame, *k, &v) == 1 && ygst_json_type(v) == 's') { 325 14 ygst_json_string(v, &c->message); 326 14 break; 327 } 328 } 329 } 330 15 if (!c->message.len) ygst_buf_append_str(&c->message, "error frame without a message"); 331 15 ygst_buf_cstr(&c->message); 332 15 ygst_buf_cstr(&c->record_id); 333 15 ygst_buf_cstr(&c->audio_url); 334 /* without a business code the platform failed while evaluating: replay like a 5xx */ 335 15 c->retryable = c->biz_code ? ygst_is_retryable(0, c->biz_code, 0) : 1; 336 15 c->err_id = ygst_errid_for(0, c->biz_code, 0, c->retryable); 337 15 return c->message.oom ? -1 : 0; 338 } 339 340 /* ------------------------------------------------------------------ words dedupe */ 341 342 41void ygst_words_dedupe_init(ygst_words_dedupe *d) { 343 41 memset(d, 0, sizeof *d); 344 41} 345 346 97int ygst_words_dedupe_skip(ygst_words_dedupe *d, int read_units, int word_count) { 347 long start; 348 long skip; 349 97 if (word_count <= 0) return 0; 350 96 if (read_units < 0) read_units = d->delivered + word_count; /* position unknown: the next units */ 351 96 start = (long)read_units - word_count; 352 96 skip = d->delivered > start ? (long)d->delivered - start : 0; 353 96 if (skip >= word_count) return word_count; /* every item was delivered already */ 354 72 if (read_units > d->delivered) d->delivered = read_units; 355 72 return (int)skip; 356 } 357 358 /* ------------------------------------------------------------------ round controller */ 359 360 39void ygst_rt_init(ygst_rt *rt) { 361 39 memset(rt, 0, sizeof *rt); 362 39 ygst_retry_policy_default(&rt->policy); 363 39 rt->connect_timeout_ms = 20000; 364 39 rt->attempt_timeout_ms = 60000; 365 39 rt->ping_interval_ms = 15000; 366 39 rt->pong_timeout_ms = 30000; 367 39 rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES; 368 39 rt->state = YGST_RT_IDLE; 369 39 rt->frame.kind = YGST_WSF_INVALID; 370 39 ygst_words_dedupe_init(&rt->dedupe); 371 39 ygst_classification_init(&rt->cls); 372 39 ygst_buf_init(&rt->failure); 373 39} 374 375 39void ygst_rt_free(ygst_rt *rt) { 376 39 ygst_classification_free(&rt->cls); 377 39 ygst_buf_free(&rt->failure); 378 39} 379 380 1184static ygst_rt_actions no_actions(void) { 381 ygst_rt_actions a; 382 1184 memset(&a, 0, sizeof a); 383 1184 return a; 384 } 385 386 795static int on_connection(ygst_rt_state s) { 387 795 return s == YGST_RT_CONNECTING || s == YGST_RT_STARTING || s == YGST_RT_STREAMING || s == YGST_RT_ENDING; 388 } 389 390 167static void clear_timers(ygst_rt *rt) { 391 167 rt->deadline_ms = 0; 392 167 rt->next_ping_ms = 0; 393 167 rt->pong_deadline_ms = 0; 394 167} 395 396 35static void note_failure(ygst_rt *rt, const char *why) { 397 35 ygst_buf_reset(&rt->failure); 398 35 ygst_buf_append_str(&rt->failure, why ? why : ""); 399 35 ygst_buf_cstr(&rt->failure); 400 35} 401 402 53static void open_connection(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 403 53 a->connect = 1; 404 53 rt->state = YGST_RT_CONNECTING; 405 53 rt->connections++; 406 53 rt->sent = 0; 407 53 clear_timers(rt); 408 53 rt->deadline_ms = now + rt->connect_timeout_ms; 409 53} 410 411 198static void send_pending_audio(ygst_rt *rt, ygst_rt_actions *a) { 412 198 if (rt->total > rt->sent) { 413 174 a->audio_from = rt->sent; 414 174 a->audio_to = rt->total; 415 174 rt->sent = rt->total; 416 } 417 198} 418 419 29static void start_streaming(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 420 29 rt->state = YGST_RT_STREAMING; 421 29 clear_timers(rt); 422 29 if (rt->ping_interval_ms > 0) rt->next_ping_ms = now + rt->ping_interval_ms; 423 29 send_pending_audio(rt, a); 424 29} 425 426 /* All audio from the current offset, then end. The first end starts the attempts of the REST 427 * call controller; a reconnect only sends it again. */ 428 37static void send_end(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 429 37 send_pending_audio(rt, a); 430 37 a->send_end = 1; 431 37 if (!rt->end_sent) { 432 25 rt->end_sent = 1; 433 25 ygst_call_init(&rt->call, &rt->policy, rt->seed, now); 434 } 435 37 rt->state = YGST_RT_ENDING; 436 /* no heartbeat while the platform evaluates: the result timeout bounds the wait, as a REST 437 * attempt's read timeout does */ 438 37 clear_timers(rt); 439 37 rt->deadline_ms = now + rt->attempt_timeout_ms; 440 37} 441 442 /* Before end: the platform never evaluated this round, REST evaluates it with the same tokenId. */ 443 9static void fall_back(ygst_rt *rt, ygst_rt_actions *a, const char *why) { 444 9 a->close = 1; 445 9 rt->state = YGST_RT_FALLBACK; 446 9 clear_timers(rt); 447 9 note_failure(rt, why); 448 9 if (rt->end_requested) a->fallback = 1; 449 9} 450 451 /* After end: rt->cls holds the outcome of this attempt; the REST call controller decides. */ 452 26static void attempt_failed(ygst_rt *rt, ygst_rt_actions *a, int64_t now) { 453 26 ygst_action next = ygst_call_next(&rt->call, &rt->cls, -1, now); 454 26 a->close = 1; 455 26 clear_timers(rt); 456 26 note_failure(rt, ygst_buf_cstr(&rt->cls.message)); 457 26 if (next.kind == YGST_ACT_RETRY) { 458 16 rt->state = YGST_RT_BACKOFF; 459 16 rt->reconnect_at_ms = now + next.delay_ms; 460 16 a->retry = 1; 461 16 a->delay_ms = next.delay_ms; 462 16 return; 463 } 464 10 rt->state = YGST_RT_DONE; 465 10 a->fail = 1; 466 10 a->err_id = next.kind == YGST_ACT_FAIL ? next.err_id : YGST_LOCAL_PROTOCOL; 467 } 468 469 23static void connection_failed(ygst_rt *rt, ygst_rt_actions *a, int local_code, int http_status, const char *message, 470 int64_t now) { 471 ygst_outcome o; 472 23 if (!rt->end_sent) { 473 7 fall_back(rt, a, message); 474 7 return; 475 } 476 16 memset(&o, 0, sizeof o); 477 16 o.local_code = local_code; 478 16 o.http_status = local_code ? 0 : http_status; 479 16 o.retry_after_ms = -1; 480 16 o.local_message = message; 481 16 ygst_classify(&o, &rt->cls); 482 16 if (!local_code && http_status > 0 && message && message[0]) { 483 /* a rejected handshake has no body: say so instead of repeating the status */ 484 2 ygst_buf_reset(&rt->cls.message); 485 2 ygst_buf_append_str(&rt->cls.message, message); 486 2 ygst_buf_cstr(&rt->cls.message); 487 } 488 16 attempt_failed(rt, a, now); 489 } 490 491 39ygst_rt_actions ygst_rt_start(ygst_rt *rt, int64_t now_ms) { 492 39 ygst_rt_actions a = no_actions(); 493 39 if (rt->state != YGST_RT_IDLE) return a; 494 38 if (rt->max_frame_bytes == 0) rt->max_frame_bytes = YGST_WS_MAX_FRAME_BYTES; 495 38 open_connection(rt, &a, now_ms); 496 38 return a; 497 } 498 499 249ygst_rt_actions ygst_rt_on_audio(ygst_rt *rt, size_t total_bytes, int64_t now_ms) { 500 249 ygst_rt_actions a = no_actions(); 501 (void)now_ms; 502 249 if (rt->end_requested) return a; /* the round audio is complete */ 503 248 if (total_bytes > rt->total) rt->total = total_bytes; 504 248 if (rt->state == YGST_RT_STREAMING) send_pending_audio(rt, &a); 505 248 return a; 506 } 507 508 33ygst_rt_actions ygst_rt_on_stop(ygst_rt *rt, size_t total_bytes, int64_t now_ms) { 509 33 ygst_rt_actions a = no_actions(); 510 33 if (rt->end_requested || rt->state == YGST_RT_DONE) return a; 511 31 if (total_bytes > rt->total) rt->total = total_bytes; 512 31 rt->end_requested = 1; 513 31 switch (rt->state) { 514 5 case YGST_RT_IDLE: 515 case YGST_RT_FALLBACK: 516 5 rt->state = YGST_RT_FALLBACK; 517 5 a.fallback = 1; 518 5 break; 519 21 case YGST_RT_STREAMING: 520 21 send_end(rt, &a, now_ms); 521 21 break; 522 5 default: 523 5 break; /* CONNECTING, STARTING: end goes out right after "started" */ 524 } 525 31 return a; 526 } 527 528 232ygst_rt_actions ygst_rt_on_text(ygst_rt *rt, const char *text, size_t n, int64_t now_ms) { 529 232 ygst_rt_actions a = no_actions(); 530 232 if (!on_connection(rt->state)) return a; 531 228 if (ygst_ws_frame_parse(text, n, &rt->frame) != 0) return a; 532 227 switch (rt->frame.kind) { 533 47 case YGST_WSF_CONNECTED: 534 47 if (rt->state == YGST_RT_CONNECTING) { 535 46 a.send_params = 1; 536 46 rt->state = YGST_RT_STARTING; 537 } 538 47 break; 539 46 case YGST_WSF_STARTED: 540 46 if (rt->state == YGST_RT_STARTING) { 541 45 if (rt->end_sent || rt->end_requested) { 542 16 send_end(rt, &a, now_ms); 543 } else { 544 29 start_streaming(rt, &a, now_ms); 545 } 546 } 547 46 break; 548 110 case YGST_WSF_PROGRESS: 549 /* delivered only with new words; empty words and bytes-only frames are not */ 550 110 if (rt->frame.word_count > 0) { 551 87 int skip = ygst_words_dedupe_skip(&rt->dedupe, rt->frame.read_units, rt->frame.word_count); 552 87 if (skip < rt->frame.word_count) { 553 65 a.progress = 1; 554 65 a.words_skip = skip; 555 } else { 556 22 a.progress_held = 1; 557 } 558 } 559 110 break; 560 13 case YGST_WSF_FINAL: 561 13 if (rt->state != YGST_RT_ENDING) { 562 1 if (!rt->end_sent) fall_back(rt, &a, "result frame before end"); 563 1 break; 564 } 565 { 566 ygst_outcome o; 567 12 memset(&o, 0, sizeof o); 568 12 o.http_status = 200; 569 12 o.body = text; 570 12 o.body_len = n; 571 12 o.retry_after_ms = -1; 572 12 o.replayed = rt->frame.replayed; 573 12 ygst_classify(&o, &rt->cls); 574 } 575 12 if (rt->cls.success) { 576 11 (void)ygst_call_next(&rt->call, &rt->cls, -1, now_ms); 577 11 rt->state = YGST_RT_DONE; 578 11 clear_timers(rt); 579 11 a.close = 1; 580 11 a.final = 1; 581 } else { 582 1 attempt_failed(rt, &a, now_ms); 583 } 584 12 break; 585 10 case YGST_WSF_ERROR: 586 10 ygst_ws_classify_error(rt->frame.root, &rt->cls); 587 10 if (!rt->end_sent) { 588 1 fall_back(rt, &a, ygst_buf_cstr(&rt->cls.message)); 589 } else { 590 9 attempt_failed(rt, &a, now_ms); 591 } 592 10 break; 593 1 default: 594 1 break; /* pong, other events */ 595 } 596 227 return a; 597 } 598 599 3ygst_rt_actions ygst_rt_on_pong(ygst_rt *rt, int64_t now_ms) { 600 3 ygst_rt_actions a = no_actions(); 601 3 if (rt->state == YGST_RT_STREAMING && rt->pong_deadline_ms) { 602 1 rt->pong_deadline_ms = 0; 603 1 if (rt->ping_interval_ms > 0) rt->next_ping_ms = now_ms + rt->ping_interval_ms; 604 } 605 3 return a; 606 } 607 608 19ygst_rt_actions ygst_rt_on_failure(ygst_rt *rt, int local_code, int http_status, const char *message, 609 int64_t now_ms) { 610 19 ygst_rt_actions a = no_actions(); 611 19 if (!on_connection(rt->state)) return a; 612 17 connection_failed(rt, &a, local_code, http_status, message, now_ms); 613 17 return a; 614 } 615 616 609ygst_rt_actions ygst_rt_on_tick(ygst_rt *rt, int64_t now_ms) { 617 609 ygst_rt_actions a = no_actions(); 618 char msg[96]; 619 609 if (rt->state == YGST_RT_BACKOFF) { 620 77 if (now_ms >= rt->reconnect_at_ms) open_connection(rt, &a, now_ms); 621 77 return a; 622 } 623 532 if (!on_connection(rt->state)) return a; 624 388 if (rt->deadline_ms && now_ms >= rt->deadline_ms) { 625 4 int ending = rt->state == YGST_RT_ENDING; 626 4 if (ending) { 627 2 snprintf(msg, sizeof msg, "timeout: no result within %d ms", rt->attempt_timeout_ms); 628 } else { 629 2 snprintf(msg, sizeof msg, "timeout: not started within %d ms", rt->connect_timeout_ms); 630 } 631 4 connection_failed(rt, &a, ending ? YGST_LOCAL_RESULT_TIMEOUT : YGST_LOCAL_TIMEOUT, 0, msg, now_ms); 632 4 return a; 633 } 634 384 if (rt->pong_deadline_ms && now_ms >= rt->pong_deadline_ms) { 635 2 snprintf(msg, sizeof msg, "timeout: no pong within %d ms", rt->pong_timeout_ms); 636 2 connection_failed(rt, &a, YGST_LOCAL_TIMEOUT, 0, msg, now_ms); 637 2 return a; 638 } 639 382 if (rt->state == YGST_RT_STREAMING && rt->next_ping_ms && !rt->pong_deadline_ms && now_ms >= rt->next_ping_ms) { 640 4 a.send_ping = 1; 641 4 rt->next_ping_ms = 0; 642 4 rt->pong_deadline_ms = now_ms + rt->pong_timeout_ms; 643 } 644 382 return a; 645 } 646 647 2void ygst_rt_cancel(ygst_rt *rt) { 648 2 rt->state = YGST_RT_DONE; 649 2 clear_timers(rt); 650 2} 651 652 12int ygst_rt_active(const ygst_rt *rt) { 653 12 return on_connection(rt->state) || rt->state == YGST_RT_BACKOFF; 654 } 655 656 5int ygst_rt_error_message(const ygst_rt *rt, int err_id, ygst_buf *out) { 657 5 return ygst_error_message(&rt->cls, err_id, rt->call.attempt, out); 658 }
1 /* 2 * Copyright 2026 优谷雅言 open.shengzhiai.com 3 * SPDX-License-Identifier: Apache-2.0 4 * 5 * Retry policy (DESIGN 2.3), Shengtong errId mapping (DESIGN 6.2), response classification and 6 * the per-call controller that decides between delivering, retrying and failing. The Objective-C 7 * layer executes the decisions with NSURLSession; the Linux tests execute them with sockets. 8 */ 9 #include "ygst_core.h" 10 11 #include <math.h> 12 #include <stdio.h> 13 #include <stdlib.h> 14 #include <string.h> 15 16 /* ------------------------------------------------------------------ table lookups */ 17 18 272static const ygst_error_entry *find_in(const ygst_error_entry *t, size_t n, int code) { 19 size_t i; 20 3776 for (i = 0; i < n; i++) { 21 3762 if (t[i].code == code) return &t[i]; 22 } 23 14 return NULL; 24 } 25 26 195const ygst_error_entry *ygst_error_find(int code) { 27 195 return find_in(ygst_table_errors, ygst_table_errors_count, code); 28 } 29 30 9const ygst_error_entry *ygst_warning_find(int code) { 31 9 return find_in(ygst_table_warnings, ygst_table_warnings_count, code); 32 } 33 34 68const ygst_error_entry *ygst_local_find(int code) { 35 68 return find_in(ygst_table_local, ygst_table_local_count, code); 36 } 37 38 34const char *ygst_compat_message(int err_id) { 39 size_t i; 40 158 for (i = 0; i < ygst_table_compat_count; i++) { 41 154 if (ygst_table_compat[i].err_id == err_id) return ygst_table_compat[i].message; 42 } 43 4 return NULL; 44 } 45 46 46int ygst_http_retryable(int status) { 47 size_t i; 48 291 for (i = 0; i < ygst_table_retryable_http_count; i++) { 49 267 if (ygst_table_retryable_http[i] == status) return 1; 50 } 51 24 return 0; 52 } 53 54 /* ------------------------------------------------------------------ RNG (splitmix64) */ 55 56 77void ygst_rng_seed(ygst_rng *r, uint64_t seed) { 57 77 r->s = seed; 58 77} 59 60 1074uint64_t ygst_rng_next(ygst_rng *r) { 61 1074 uint64_t z = (r->s += 0x9E3779B97F4A7C15ull); 62 1074 z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull; 63 1074 z = (z ^ (z >> 27)) * 0x94D049BB133111EBull; 64 1074 return z ^ (z >> 31); 65 } 66 67 1072double ygst_rng_uniform(ygst_rng *r) { 68 1072 return (double)(ygst_rng_next(r) >> 11) * (1.0 / 9007199254740992.0); 69 } 70 71 /* ------------------------------------------------------------------ policy */ 72 73 96void ygst_retry_policy_default(ygst_retry_policy *p) { 74 96 memset(p, 0, sizeof *p); 75 96 p->max_retries = 2; 76 96 p->initial_delay_ms = 200; 77 96 p->multiplier = 2.0; 78 96 p->max_delay_ms = 4000; 79 96 p->jitter = 0.3; 80 96 p->respect_retry_after = 1; 81 96 p->max_retry_after_ms = 30000; 82 96 p->total_timeout_ms = 300000; 83 96 p->auto_retry = 0; 84 96 p->max_auto_retries = 2; 85 96 p->err_ids[0] = YGST_ERRID_RETRYABLE; 86 96 p->n_err_ids = 1; 87 96} 88 89 28int ygst_retry_policy_add_err_id(ygst_retry_policy *p, long long err_id) { 90 28 if (err_id <= 0 || err_id > 2147483647LL) return -1; 91 25 if (ygst_retry_policy_has_err_id(p, (int)err_id)) return 0; 92 24 if (p->n_err_ids >= YGST_MAX_ERR_IDS) return -1; 93 17 p->err_ids[p->n_err_ids++] = (int)err_id; 94 17 return 0; 95 } 96 97 40int ygst_retry_policy_has_err_id(const ygst_retry_policy *p, int err_id) { 98 int i; 99 280 for (i = 0; i < p->n_err_ids; i++) { 100 253 if (p->err_ids[i] == err_id) return 1; 101 } 102 27 return 0; 103 } 104 105 81long ygst_backoff_delay_ms(const ygst_retry_policy *p, int n, ygst_rng *rng) { 106 double base; 107 81 double factor = 1.0; 108 double d; 109 81 if (n < 1) n = 1; 110 81 base = (double)p->initial_delay_ms * pow(p->multiplier > 0 ? p->multiplier : 1.0, (double)(n - 1)); 111 81 if (base > p->max_delay_ms) base = p->max_delay_ms; 112 81 if (p->jitter > 0 && rng) factor = 1.0 + (ygst_rng_uniform(rng) * 2.0 - 1.0) * p->jitter; 113 81 d = base * factor; 114 81 if (d < 0) d = 0; 115 81 return (long)(d + 0.5); 116 } 117 118 /* days since 1970-01-01 for a proleptic Gregorian date (H. Hinnant, days_from_civil) */ 119 3static long long days_from_civil(long long y, unsigned m, unsigned d) { 120 long long era; 121 unsigned yoe, doy, doe; 122 3 y -= m <= 2; 123 3 era = (y >= 0 ? y : y - 399) / 400; 124 3 yoe = (unsigned)(y - era * 400); 125 3 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; 126 3 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; 127 3 return era * 146097 + (long long)doe - 719468; 128 } 129 130 15int ygst_parse_retry_after(const char *value, int64_t now_ms, long *out_ms) { 131 static const char *const MON[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", 132 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 133 char v[64]; 134 size_t n; 135 size_t i; 136 15 if (!value || !out_ms) return -1; 137 14 n = ygst_trim_copy(value, v, sizeof v); 138 14 if (n == 0) return -1; 139 30 for (i = 0; i < n && v[i] >= '0' && v[i] <= '9'; i++) { 140 } 141 13 if (i == n) { 142 6 long secs = 0; 143 21 for (i = 0; i < n; i++) { 144 16 if (secs > 100000000L) return -1; 145 15 secs = secs * 10 + (v[i] - '0'); 146 } 147 5 *out_ms = secs * 1000L; 148 5 return 0; 149 } 150 /* IMF-fixdate: "Sun, 06 Nov 1994 08:49:37 GMT" */ 151 7 if (n == 29 && v[3] == ',' && v[4] == ' ' && v[7] == ' ' && v[11] == ' ' && v[16] == ' ' && v[19] == ':' && 152 5 v[22] == ':' && v[25] == ' ' && strcmp(v + 26, "GMT") == 0) { 153 5 unsigned day, mon = 0, hh, mm, ss; 154 long long year; 155 long long t; 156 65 for (i = 0; i < 12; i++) { 157 60 if (strncmp(v + 8, MON[i], 3) == 0) mon = (unsigned)i + 1; 158 } 159 5 if (!mon) return -1; 160 4 day = (unsigned)((v[5] - '0') * 10 + (v[6] - '0')); 161 4 year = (v[12] - '0') * 1000 + (v[13] - '0') * 100 + (v[14] - '0') * 10 + (v[15] - '0'); 162 4 hh = (unsigned)((v[17] - '0') * 10 + (v[18] - '0')); 163 4 mm = (unsigned)((v[20] - '0') * 10 + (v[21] - '0')); 164 4 ss = (unsigned)((v[23] - '0') * 10 + (v[24] - '0')); 165 4 if (day < 1 || day > 31 || hh > 23 || mm > 59 || ss > 60) return -1; 166 3 t = days_from_civil(year, mon, day) * 86400LL + hh * 3600LL + mm * 60LL + ss; 167 3 t = t * 1000LL - now_ms; 168 3 *out_ms = t > 0 ? (long)(t > 2000000000LL ? 2000000000LL : t) : 0; 169 3 return 0; 170 } 171 2 return -1; 172 } 173 174 /* ------------------------------------------------------------------ retry and errId rules */ 175 176 235int ygst_is_retryable(int local_code, int biz_code, int http_status) { 177 const ygst_error_entry *e; 178 235 if (local_code) { 179 50 return local_code == YGST_LOCAL_NETWORK || local_code == YGST_LOCAL_TIMEOUT || 180 local_code == YGST_LOCAL_RESULT_TIMEOUT; 181 } 182 185 if (biz_code) { 183 151 e = ygst_error_find(biz_code); 184 151 if (e) return e->retryable; 185 } 186 38 return ygst_http_retryable(http_status); 187 } 188 189 23int ygst_http_fallback_code(int status) { 190 23 const char *cat = ygst_http_fallback_category(status); 191 23 if (strcmp(cat, "AUTH") == 0) return 40100; 192 21 if (strcmp(cat, "PERMISSION") == 0) return 40300; 193 19 if (strcmp(cat, "NOT_FOUND") == 0) return 40400; 194 17 if (strcmp(cat, "CONFLICT") == 0) return 40900; 195 16 if (strcmp(cat, "RATE_LIMIT") == 0) return 42900; 196 15 if (strcmp(cat, "SERVER") == 0) return status == 501 ? 50010 : 50000; 197 12 if (strcmp(cat, "UPSTREAM") == 0) return 50200; 198 10 if (strcmp(cat, "TIMEOUT") == 0) return YGST_ERRID_RETRYABLE; 199 9 if (strcmp(cat, "INVALID_PARAM") == 0) return 40001; 200 3 return YGST_LOCAL_PROTOCOL; 201 } 202 203 204int ygst_errid_for(int local_code, int biz_code, int http_status, int retryable) { 204 204 if (local_code >= 60001 && local_code <= 60009) return local_code; 205 193 if (retryable) return YGST_ERRID_RETRYABLE; 206 100 if (biz_code) return biz_code; 207 44 if (local_code) return local_code; 208 23 return ygst_http_fallback_code(http_status); 209 } 210 211 /* ------------------------------------------------------------------ classification */ 212 213 198void ygst_classification_init(ygst_classification *c) { 214 198 memset(c, 0, sizeof *c); 215 198 ygst_buf_init(&c->record_id); 216 198 ygst_buf_init(&c->audio_url); 217 198 ygst_buf_init(&c->message); 218 198} 219 220 198void ygst_classification_free(ygst_classification *c) { 221 198 ygst_buf_free(&c->record_id); 222 198 ygst_buf_free(&c->audio_url); 223 198 ygst_buf_free(&c->message); 224 198} 225 226 120static int member_int(ygst_span obj, const char *key, long long *out) { 227 ygst_span v; 228 120 if (ygst_json_member(obj, key, &v) != 1) return 0; 229 97 if (ygst_json_type(v) == 's') { 230 /* some gateways quote numeric codes */ 231 ygst_buf b; 232 1 int ok = 0; 233 1 ygst_buf_init(&b); 234 1 if (ygst_json_string(v, &b) == 0 && b.len > 0 && b.len < 12) { 235 1 char *end = NULL; 236 1 long long x = strtoll(ygst_buf_cstr(&b), &end, 10); 237 1 if (end && *end == 0) { 238 1 *out = x; 239 1 ok = 1; 240 } 241 } 242 1 ygst_buf_free(&b); 243 1 return ok; 244 } 245 96 return ygst_json_int(v, out) == 0; 246 } 247 248 /* First of the keys whose value is a JSON string. */ 249 108static int first_string(ygst_span obj, const char *const *keys, ygst_buf *out) { 250 140 for (; *keys; keys++) { 251 ygst_span v; 252 131 if (ygst_json_member(obj, *keys, &v) == 1 && ygst_json_type(v) == 's') { 253 99 ygst_buf_reset(out); 254 99 return ygst_json_string(v, out) == 0; 255 } 256 } 257 9 return 0; 258 } 259 260 /* "[2001] signature mismatch": the compat engine code in a FastAPI detail, 3 to 6 digits. */ 261 7static int bracket_code(const char *msg) { 262 7 long v = 0; 263 7 int digits = 0; 264 7 if (!msg) return 0; 265 8 while (*msg == ' ' || *msg == '\t' || *msg == '\n' || *msg == '\r') msg++; 266 7 if (*msg != '[') return 0; 267 3 msg++; 268 14 while (*msg >= '0' && *msg <= '9' && digits < 7) { 269 11 v = v * 10 + (*msg - '0'); 270 11 msg++; 271 11 digits++; 272 } 273 3 if (*msg != ']' || digits < 3 || digits > 6) return 0; 274 2 return (int)v; 275 } 276 277 59static void set_message(ygst_buf *m, const char *s) { 278 59 ygst_buf_reset(m); 279 59 ygst_buf_append_str(m, s ? s : ""); 280 59 ygst_buf_cstr(m); 281 59} 282 283 218int ygst_classify(const ygst_outcome *o, ygst_classification *c) { 284 static const char *const MESSAGE_KEYS[] = {"message", "msg", "error", NULL}; 285 ygst_span root; 286 218 int parsed = 0; 287 218 long long code = 0; 288 218 c->success = 0; 289 218 c->http_status = o->http_status; 290 218 c->local_code = o->local_code; 291 218 c->biz_code = 0; 292 218 c->has_audio_url = 0; 293 218 c->has_record_id = 0; 294 218 c->record_id_raw.p = NULL; 295 218 c->record_id_raw.n = 0; 296 218 c->result.p = NULL; 297 218 c->result.n = 0; 298 218 ygst_buf_reset(&c->record_id); 299 218 ygst_buf_reset(&c->audio_url); 300 218 ygst_buf_reset(&c->message); 301 302 218 if (o->local_code || o->http_status <= 0) { 303 const ygst_error_entry *e; 304 43 if (!c->local_code) c->local_code = YGST_LOCAL_NETWORK; 305 43 e = ygst_local_find(c->local_code); 306 65 set_message(&c->message, (o->local_message && o->local_message[0]) ? o->local_message 307 22 : (e ? e->message : "network error")); 308 43 c->retryable = ygst_is_retryable(c->local_code, 0, 0); 309 43 c->err_id = ygst_errid_for(c->local_code, 0, 0, c->retryable); 310 43 return 0; 311 } 312 313 175 if (o->body && o->body_len) parsed = ygst_json_parse(o->body, o->body_len, &root) == 0 && ygst_json_type(root) == 'o'; 314 315 175 if (o->http_status >= 200 && o->http_status < 300) { 316 ygst_span result, v; 317 62 if (parsed && ygst_json_member(root, "result", &result) == 1) { 318 ygst_span inner; 319 59 c->success = 1; 320 59 c->result = result; 321 59 if (ygst_json_member(root, "recordId", &v) == 1) { 322 56 c->has_record_id = 1; 323 56 c->record_id_raw = v; 324 56 if (ygst_json_type(v) == 's') { 325 55 ygst_json_string(v, &c->record_id); 326 } else { 327 1 ygst_buf_append(&c->record_id, v.p, v.n); 328 } 329 } 330 59 if (ygst_json_member(root, "audioUrl", &v) == 1) { 331 5 if (ygst_json_type(v) == 's') c->has_audio_url = ygst_json_string(v, &c->audio_url) == 0; 332 56 } else if (ygst_json_type(result) == 'o' && ygst_json_member(result, "audioUrl", &inner) == 1 && 333 2 ygst_json_type(inner) == 's') { 334 1 c->has_audio_url = ygst_json_string(inner, &c->audio_url) == 0; 335 } 336 59 if (c->has_audio_url && c->audio_url.len == 0) c->has_audio_url = 0; 337 59 ygst_buf_cstr(&c->record_id); 338 59 ygst_buf_cstr(&c->audio_url); 339 59 c->retryable = 0; 340 59 c->err_id = 0; 341 59 return 0; 342 } 343 3 c->local_code = YGST_LOCAL_PROTOCOL; 344 3 set_message(&c->message, parsed ? "invalid platform response: platform response has no result" 345 : "invalid platform response: not a JSON object"); 346 3 c->retryable = 0; 347 3 c->err_id = YGST_LOCAL_PROTOCOL; 348 3 return 0; 349 } 350 351 113 if (parsed) { 352 ygst_span detail; 353 108 if (member_int(root, "code", &code) && code != 0) c->biz_code = (int)code; 354 108 if (!c->biz_code && member_int(root, "errId", &code) && code != 0) c->biz_code = (int)code; 355 108 first_string(root, MESSAGE_KEYS, &c->message); 356 108 if (ygst_json_member(root, "detail", &detail) == 1) { 357 9 if (ygst_json_type(detail) == 's') { 358 ygst_buf d; 359 7 ygst_buf_init(&d); 360 7 ygst_json_string(detail, &d); 361 7 if (!c->biz_code) c->biz_code = bracket_code(ygst_buf_cstr(&d)); 362 7 if (!c->message.len) set_message(&c->message, ygst_buf_cstr(&d)); 363 7 ygst_buf_free(&d); 364 2 } else if (!c->message.len) { 365 1 ygst_buf_reset(&c->message); 366 1 ygst_buf_append(&c->message, detail.p, detail.n > 300 ? 300 : detail.n); 367 } 368 } 369 } 370 113 if (!c->message.len) { 371 char tmp[32]; 372 6 snprintf(tmp, sizeof tmp, "HTTP %d", o->http_status); 373 6 set_message(&c->message, tmp); 374 } 375 113 ygst_buf_cstr(&c->message); 376 113 c->retryable = ygst_is_retryable(0, c->biz_code, o->http_status); 377 113 c->err_id = ygst_errid_for(0, c->biz_code, o->http_status, c->retryable); 378 113 return 0; 379 } 380 381 25int ygst_error_message(const ygst_classification *c, int err_id, int attempts, ygst_buf *out) { 382 25 const char *m = c->message.data ? (const char *)c->message.data : ""; 383 25 if (err_id == YGST_ERRID_RETRYABLE) { 384 10 const char *base = ygst_compat_message(YGST_ERRID_RETRYABLE); 385 size_t mark; 386 10 ygst_buf_append_str(out, base ? base : ""); 387 10 ygst_buf_append_str(out, ": "); 388 10 mark = out->len; 389 10 if (c->http_status > 0) { 390 4 ygst_buf_append_str(out, "HTTP "); 391 4 ygst_buf_append_int(out, c->http_status); 392 4 if (c->biz_code) { 393 2 ygst_buf_append_str(out, " code="); 394 2 ygst_buf_append_int(out, c->biz_code); 395 } 396 6 } else if (c->local_code) { 397 3 const ygst_error_entry *e = ygst_local_find(c->local_code); 398 3 if (e) { 399 2 ygst_buf_append_str(out, e->name); 400 } else { 401 1 ygst_buf_append_str(out, "code="); 402 1 ygst_buf_append_int(out, c->local_code); 403 } 404 3 } else if (c->biz_code) { 405 /* a WebSocket error frame: a business code without an HTTP status */ 406 2 ygst_buf_append_str(out, "code="); 407 2 ygst_buf_append_int(out, c->biz_code); 408 } 409 10 if (m[0]) { 410 10 if (out->len > mark) ygst_buf_append_char(out, ' '); 411 10 ygst_buf_append_str(out, m); 412 } 413 10 if (attempts > 0) { 414 9 ygst_buf_append_str(out, " (attempts "); 415 9 ygst_buf_append_int(out, attempts); 416 9 ygst_buf_append_char(out, ')'); 417 } 418 15 } else if (m[0]) { 419 11 ygst_buf_append_str(out, m); 420 } else { 421 4 const char *cm = ygst_compat_message(err_id); 422 4 const ygst_error_entry *e = cm ? NULL : ygst_error_find(err_id); 423 4 if (!cm && !e) e = ygst_local_find(err_id); 424 4 ygst_buf_append_str(out, cm ? cm : (e ? e->message : "")); 425 } 426 25 ygst_buf_cstr(out); 427 25 return out->oom ? -1 : 0; 428 } 429 430 /* ------------------------------------------------------------------ controller */ 431 432 73void ygst_call_init(ygst_call *c, const ygst_retry_policy *p, uint64_t seed, int64_t now_ms) { 433 73 memset(c, 0, sizeof *c); 434 73 if (p) { 435 72 c->policy = *p; 436 } else { 437 1 ygst_retry_policy_default(&c->policy); 438 } 439 73 ygst_rng_seed(&c->rng, seed); 440 73 c->start_ms = now_ms; 441 73} 442 443 60static long apply_retry_after(const ygst_retry_policy *p, long delay, long retry_after_ms) { 444 60 if (p->respect_retry_after && retry_after_ms >= 0) { 445 4 long ra = retry_after_ms > p->max_retry_after_ms ? p->max_retry_after_ms : retry_after_ms; 446 4 if (ra > delay) delay = ra; 447 } 448 60 return delay; 449 } 450 451 124ygst_action ygst_call_next(ygst_call *c, const ygst_classification *cls, long retry_after_ms, int64_t now_ms) { 452 ygst_action a; 453 124 int64_t deadline = c->start_ms + c->policy.total_timeout_ms; 454 124 a.kind = YGST_ACT_FAIL; 455 124 a.delay_ms = 0; 456 124 a.err_id = cls->err_id; 457 124 a.auto_retry = 0; 458 124 c->attempt++; 459 124 c->total_attempts++; 460 124 if (cls->success) { 461 42 a.kind = YGST_ACT_SUCCESS; 462 42 a.err_id = 0; 463 42 return a; 464 } 465 82 if (cls->retryable && c->attempt <= c->policy.max_retries) { 466 49 long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->attempt, &c->rng), retry_after_ms); 467 49 if (now_ms + d <= deadline) { 468 47 a.kind = YGST_ACT_RETRY; 469 47 a.delay_ms = d; 470 47 return a; 471 } 472 2 return a; 473 } 474 46 if (c->policy.auto_retry && c->submission < c->policy.max_auto_retries && 475 13 ygst_retry_policy_has_err_id(&c->policy, cls->err_id)) { 476 11 long d = apply_retry_after(&c->policy, ygst_backoff_delay_ms(&c->policy, c->submission + 1, &c->rng), 477 retry_after_ms); 478 11 if (now_ms + d <= deadline) { 479 10 c->submission++; 480 10 c->attempt = 0; 481 10 a.kind = YGST_ACT_RETRY; 482 10 a.delay_ms = d; 483 10 a.auto_retry = 1; 484 10 return a; 485 } 486 } 487 23 return a; 488 }