178 lines
6.9 KiB
C
178 lines
6.9 KiB
C
#include "sd_file_browser.h"
|
||
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
static uint8_t has_temperature_name(const char *name)
|
||
{
|
||
const size_t length = strlen(name);
|
||
const char prefix[] = "temperature_";
|
||
|
||
return (uint8_t)((length > (sizeof(prefix) - 1U + 3U)) &&
|
||
(strncmp(name, prefix, sizeof(prefix) - 1U) == 0) &&
|
||
(strcmp(name + length - 3U, ".md") == 0));
|
||
}
|
||
|
||
/* Сравнение расширения без учёта регистра для короткого суффикса ASCII. */
|
||
static uint8_t has_suffix_ci(const char *name, const char *suffix)
|
||
{
|
||
const size_t name_length = strlen(name);
|
||
const size_t suffix_length = strlen(suffix);
|
||
size_t index;
|
||
|
||
if (name_length <= suffix_length) {
|
||
return 0U;
|
||
}
|
||
for (index = 0U; index < suffix_length; ++index) {
|
||
char left = name[name_length - suffix_length + index];
|
||
char right = suffix[index];
|
||
|
||
if ((left >= 'A') && (left <= 'Z')) {
|
||
left = (char)(left - 'A' + 'a');
|
||
}
|
||
if ((right >= 'A') && (right <= 'Z')) {
|
||
right = (char)(right - 'A' + 'a');
|
||
}
|
||
if (left != right) {
|
||
return 0U;
|
||
}
|
||
}
|
||
return 1U;
|
||
}
|
||
|
||
/* Сырой образ прошивки: .bin или .fw. Intel HEX здесь не принимается — его
|
||
* адреса лежат внутри файла и требуют разбора, недоступного на этом этапе. */
|
||
static uint8_t has_firmware_name(const char *name)
|
||
{
|
||
return (uint8_t)((has_suffix_ci(name, ".bin") != 0U) ||
|
||
(has_suffix_ci(name, ".fw") != 0U));
|
||
}
|
||
|
||
/* Разрешены печатные ASCII-имена без разделителей ОС и управляющих байтов. */
|
||
static uint8_t path_character_is_safe(char character)
|
||
{
|
||
unsigned char value = (unsigned char)character;
|
||
return (uint8_t)((value >= 0x20U) && (value <= 0x7EU) &&
|
||
(character != '\\') && (character != ':'));
|
||
}
|
||
|
||
SdFileBrowserStatus SdFileBrowser_ValidatePath(const char *path)
|
||
{
|
||
size_t length;
|
||
size_t component_start;
|
||
size_t index;
|
||
|
||
/* strlen вызывается только после явной проверки внешнего указателя. */
|
||
if (path == NULL) {
|
||
return SD_FILE_BROWSER_INVALID_PATH;
|
||
}
|
||
length = strlen(path);
|
||
if ((length == 0U) || (length >= SD_FILE_BROWSER_MAX_PATH) ||
|
||
(path[0] != '/')) {
|
||
/* Абсолютный путь накопителя и усечённый путь fail-closed отклоняются. */
|
||
return SD_FILE_BROWSER_INVALID_PATH;
|
||
}
|
||
if (length == 1U) {
|
||
return SD_FILE_BROWSER_OK;
|
||
}
|
||
/* Компоненты анализируются за один проход, включая завершающий ноль. */
|
||
component_start = 1U;
|
||
for (index = 1U; index <= length; ++index) {
|
||
char character = path[index];
|
||
if ((character == '/') || (character == '\0')) {
|
||
size_t component_length = index - component_start;
|
||
if ((component_length == 0U) ||
|
||
(component_length > SD_FILE_BROWSER_MAX_NAME) ||
|
||
((component_length == 1U) &&
|
||
(path[component_start] == '.')) ||
|
||
((component_length == 2U) &&
|
||
(path[component_start] == '.') &&
|
||
(path[component_start + 1U] == '.'))) {
|
||
/* Пустые, точечные и слишком длинные компоненты не нормализуются. */
|
||
return SD_FILE_BROWSER_INVALID_PATH;
|
||
}
|
||
component_start = index + 1U;
|
||
} else if (path_character_is_safe(character) == 0U) {
|
||
/* Кодировка текущего FatFs-порта ASCII; байты UTF-8 не угадываются. */
|
||
return SD_FILE_BROWSER_INVALID_PATH;
|
||
}
|
||
}
|
||
return SD_FILE_BROWSER_OK;
|
||
}
|
||
|
||
SdFileBrowserStatus SdFileBrowser_ValidateLogFilePath(const char *path)
|
||
{
|
||
const char root[] = "/niceOne/mounth/";
|
||
const char *relative;
|
||
const char *separator;
|
||
const char *name;
|
||
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
|
||
|
||
if (status != SD_FILE_BROWSER_OK) {
|
||
return status;
|
||
}
|
||
/* TEMPLOG.MD сохранён только для старых карт, где журнал лежал в корне. */
|
||
if (strcmp(path, "/TEMPLOG.MD") == 0) {
|
||
return SD_FILE_BROWSER_OK;
|
||
}
|
||
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
relative = path + sizeof(root) - 1U;
|
||
separator = strchr(relative, '/');
|
||
/* Штатное дерево содержит ровно месяц и имя; обход дополнительных уровней запрещён. */
|
||
if ((separator == NULL) || (separator == relative) ||
|
||
(strchr(separator + 1U, '/') != NULL)) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
name = strrchr(path, '/');
|
||
if ((name == NULL) || (has_temperature_name(name + 1U) == 0U)) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
return SD_FILE_BROWSER_OK;
|
||
}
|
||
|
||
SdFileBrowserStatus SdFileBrowser_ValidateFirmwarePath(const char *path)
|
||
{
|
||
const char root[] = "/niceOne/firmware/";
|
||
const char *name;
|
||
SdFileBrowserStatus status = SdFileBrowser_ValidatePath(path);
|
||
|
||
if (status != SD_FILE_BROWSER_OK) {
|
||
return status;
|
||
}
|
||
/* Образы лежат ровно в одном каталоге; вложенные уровни не обходятся. */
|
||
if (strncmp(path, root, sizeof(root) - 1U) != 0) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
name = path + sizeof(root) - 1U;
|
||
if ((name[0] == '\0') || (strchr(name, '/') != NULL)) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
if (has_firmware_name(name) == 0U) {
|
||
return SD_FILE_BROWSER_NOT_LOG_FILE;
|
||
}
|
||
return SD_FILE_BROWSER_OK;
|
||
}
|
||
|
||
SdFileBrowserStatus SdFileBrowser_ListPage(
|
||
const SdFileBrowser *browser, const char *path, uint16_t offset,
|
||
uint8_t limit, SdFileBrowserPage *page)
|
||
{
|
||
SdFileBrowserStatus status;
|
||
|
||
/* limit проверяется до очистки, чтобы invalid call не трогал чужую память. */
|
||
if ((browser == NULL) || (browser->list_page == NULL) || (page == NULL) ||
|
||
(limit == 0U) || (limit > SD_FILE_BROWSER_MAX_PAGE_ITEMS)) {
|
||
return SD_FILE_BROWSER_INVALID_CURSOR;
|
||
}
|
||
status = SdFileBrowser_ValidatePath(path);
|
||
if (status != SD_FILE_BROWSER_OK) {
|
||
return status;
|
||
}
|
||
/* Нулевой tail исключает утечку содержимого предыдущего Modbus-ответа. */
|
||
memset(page, 0, sizeof(*page));
|
||
/* Portable слой не знает FatFs/HAL и передаёт только проверенный запрос. */
|
||
return browser->list_page(browser->context, path, offset, limit, page);
|
||
}
|