From 7357a63fdf499d5be38f290c8cabe1a7aeb16f6c Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 17:38:25 +0300 Subject: [PATCH] Define bounded typed attachment metadata for PM and GUI --- src/media_async/attachment.c | 87 ++++++++++++++++++++++++++++++++++++ src/media_async/attachment.h | 30 +++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/media_async/attachment.c create mode 100644 src/media_async/attachment.h diff --git a/src/media_async/attachment.c b/src/media_async/attachment.c new file mode 100644 index 00000000..f59c1de7 --- /dev/null +++ b/src/media_async/attachment.c @@ -0,0 +1,87 @@ +/* Ограниченный бинарный формат: kind:1, name_len:2, duration_ms:4, width:2, + * height:2, UTF-8 name, waveform:100 для voice. Числа big-endian. */ +#include +#include "../../lib/debug_config.h" +#include "attachment.h" + +/* Имя должно быть корректным UTF-8 basename без управляющих символов. */ +static int valid_name(const char* name, size_t n) { + if (!n || n > 255 || (n == 1 && name[0] == '.') || (n == 2 && !memcmp(name, "..", 2))) return 0; + for (size_t i = 0; i < n;) { + unsigned c = (unsigned char)name[i++]; + if (c < 0x20 || c == 0x7f || c == '/' || c == '\\') return 0; + if (c < 0x80) continue; + unsigned count, scalar, minimum; + if (c >= 0xc2 && c <= 0xdf) { count = 1; scalar = c & 31; minimum = 0x80; } + else if (c >= 0xe0 && c <= 0xef) { count = 2; scalar = c & 15; minimum = 0x800; } + else if (c >= 0xf0 && c <= 0xf4) { count = 3; scalar = c & 7; minimum = 0x10000; } + else return 0; + if (count > n - i) return 0; + while (count--) { + c = (unsigned char)name[i++]; + if ((c & 0xc0) != 0x80) return 0; + scalar = (scalar << 6) | (c & 63); + } + if (scalar < minimum || scalar > 0x10ffff || (scalar >= 0xd800 && scalar <= 0xdfff)) return 0; + } + return 1; +} + +/* Один набор правил для отправки, приёма и снимка истории. */ +int attachment_validate(const struct attachment_info* info) { + if (!info || !valid_name(info->name, strnlen(info->name, sizeof(info->name)))) goto invalid; + size_t wave = strnlen(info->waveform, sizeof(info->waveform)); + if (info->kind == ATTACHMENT_VOICE) { + if (!info->duration_ms || info->duration_ms > 86400000 || info->width || info->height || wave != 100) goto invalid; + if (strspn(info->waveform, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") != wave) goto invalid; + } else { + if (wave) goto invalid; + if (info->kind == ATTACHMENT_FILE) { + if (info->duration_ms || info->width || info->height) goto invalid; + } else if (info->kind == ATTACHMENT_VIDEO || info->kind == ATTACHMENT_IMAGE) { + if (!info->width || !info->height || info->width > 16384 || info->height > 16384) goto invalid; + if (info->kind == ATTACHMENT_VIDEO ? (!info->duration_ms || info->duration_ms > 86400000) : info->duration_ms) goto invalid; + } else goto invalid; + } + return 0; +invalid: + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: invalid metadata kind=%d", info ? (int)info->kind : 0); + return -1; +} + +int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t capacity) { + if (attachment_validate(info)) return -1; + size_t n = strlen(info->name), length = 11 + n + (info->kind == ATTACHMENT_VOICE ? 100 : 0); + if (!out || capacity < length) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: encode buffer too small"); return -1; } + out[0] = (uint8_t)info->kind; out[1] = (uint8_t)(n >> 8); out[2] = (uint8_t)n; + for (unsigned i = 0; i < 4; i++) out[3 + i] = (uint8_t)(info->duration_ms >> (24 - 8 * i)); + out[7] = (uint8_t)(info->width >> 8); out[8] = (uint8_t)info->width; + out[9] = (uint8_t)(info->height >> 8); out[10] = (uint8_t)info->height; + memcpy(out + 11, info->name, n); + if (info->kind == ATTACHMENT_VOICE) memcpy(out + 11 + n, info->waveform, 100); + return (int)length; +} + +int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info) { + if (!data || !info || length < 11) goto invalid; + size_t n = ((size_t)data[1] << 8) | data[2]; + if (!n || n > 255 || length != 11 + n + (data[0] == ATTACHMENT_VOICE ? 100 : 0) || memchr(data + 11, 0, n)) goto invalid; + memset(info, 0, sizeof(*info)); info->kind = (enum attachment_kind)data[0]; + for (unsigned i = 0; i < 4; i++) info->duration_ms = (info->duration_ms << 8) | data[3 + i]; + info->width = (uint16_t)((data[7] << 8) | data[8]); info->height = (uint16_t)((data[9] << 8) | data[10]); + memcpy(info->name, data + 11, n); + if (info->kind == ATTACHMENT_VOICE) memcpy(info->waveform, data + 11 + n, 100); + return attachment_validate(info); +invalid: + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: malformed wire metadata bytes=%zu", length); + return -1; +} + +const char* attachment_content_type(enum attachment_kind kind) { + switch (kind) { + case ATTACHMENT_VOICE: return "audio/opus"; + case ATTACHMENT_VIDEO: return "video/mp4"; + case ATTACHMENT_IMAGE: return "image/jpeg"; + default: return "application/octet-stream"; + } +} diff --git a/src/media_async/attachment.h b/src/media_async/attachment.h new file mode 100644 index 00000000..65ba2ede --- /dev/null +++ b/src/media_async/attachment.h @@ -0,0 +1,30 @@ +/* Общие метаданные готового вложения. Подготовка не знает адресата и транспорта. + * PM шифрует компактный wire-формат; GUI получает разобранные поля. + * encode/decode возвращают длину/0 или -1; неизвестные типы и лишние байты запрещены. */ +#ifndef ATTACHMENT_H +#define ATTACHMENT_H +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +enum attachment_kind { ATTACHMENT_FILE = 1, ATTACHMENT_VOICE, ATTACHMENT_VIDEO, ATTACHMENT_IMAGE }; +#define ATTACHMENT_WAVE_SIZE 100 +#define ATTACHMENT_WIRE_MAX (11 + 255 + ATTACHMENT_WAVE_SIZE) +struct attachment_info { + enum attachment_kind kind; + char name[256]; + uint32_t duration_ms; + uint16_t width, height; + char waveform[ATTACHMENT_WAVE_SIZE + 1]; /* 100 уровней из A-Za-z0-9+/; только voice. */ +}; +int attachment_validate(const struct attachment_info* info); +int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t capacity); +int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info); +const char* attachment_content_type(enum attachment_kind kind); + +#ifdef __cplusplus +} +#endif +#endif