From 4910356db2585e55d2876001e40b21e9b148bcc4 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 4 Aug 2025 00:35:10 +0200 Subject: added fgla search --- src/glamac/glamac.c | 1 + src/glamac/glamac_errors.c | 31 +++ src/glamac/glass_data.c | 673 +++++++++++++++++++++++++++++++++------------ src/glautils/fgla.c | 521 +++++++++++++++++++++++++++++++++++ 4 files changed, 1057 insertions(+), 169 deletions(-) create mode 100644 src/glamac/glamac_errors.c create mode 100644 src/glautils/fgla.c (limited to 'src') diff --git a/src/glamac/glamac.c b/src/glamac/glamac.c index b834dbc..60d0a1e 100644 --- a/src/glamac/glamac.c +++ b/src/glamac/glamac.c @@ -103,6 +103,7 @@ int main(int argc, char* argv[]) { SDL_StopTextInput(window); // Clean up resources + cleanup_glass_data(); free_fonts(&fonts); clear_text_cache(); SDL_DestroyRenderer(renderer); diff --git a/src/glamac/glamac_errors.c b/src/glamac/glamac_errors.c new file mode 100644 index 0000000..d32d369 --- /dev/null +++ b/src/glamac/glamac_errors.c @@ -0,0 +1,31 @@ +/** + * glamac_errors.c - Unified error handling implementation + */ +#include "glamac_errors.h" + +const char* glamac_error_string(GlamacResult error) { + switch (error) { + case GLAMAC_SUCCESS: + return "Success"; + case GLAMAC_ERROR_MEMORY: + return "Memory allocation failed"; + case GLAMAC_ERROR_FILE_NOT_FOUND: + return "File not found"; + case GLAMAC_ERROR_FILE_TOO_LARGE: + return "File size exceeds security limit"; + case GLAMAC_ERROR_INVALID_JSON: + return "Invalid or malformed JSON"; + case GLAMAC_ERROR_INVALID_PATH: + return "Invalid file path"; + case GLAMAC_ERROR_BUFFER_OVERFLOW: + return "Buffer overflow prevented"; + case GLAMAC_ERROR_MANUFACTURER_NOT_FOUND: + return "Manufacturer not found in JSON"; + case GLAMAC_ERROR_NO_GLASSES_FOUND: + return "No glasses found for manufacturer"; + case GLAMAC_ERROR_INVALID_ARGUMENT: + return "Invalid argument provided"; + default: + return "Unknown error"; + } +} \ No newline at end of file diff --git a/src/glamac/glass_data.c b/src/glamac/glass_data.c index 794fa7b..40f7ea3 100644 --- a/src/glamac/glass_data.c +++ b/src/glamac/glass_data.c @@ -1,5 +1,5 @@ /** - * glass_data.c - Source file featuring some input data for the GlaMaC. Will be changed in the future. + * glass_data_secure.c - Secure glass data management with proper error handling * * Copyright (C) 2025 https://optics-design.com * @@ -12,14 +12,23 @@ */ #include "glamacdef.h" #include "glass_data.h" +#include "glamac_errors.h" #include #include #include +#include +#include -// Dynamic glass data storage -static Glass* glasses = NULL; -static u32 glass_count = 0; -static b32 using_json_data = 0; +// Glass data context structure +typedef struct { + Glass* glasses; + u32 count; + u32 capacity; + b32 using_json_data; +} GlassDataContext; + +// Global context (will be refactored to parameter-based later) +static GlassDataContext g_glass_context = {0}; // Fallback sample glass data (SCHOTT glasses for testing) static const Glass default_glasses[] = { @@ -38,245 +47,571 @@ static const Glass default_glasses[] = { #define DEFAULT_GLASS_COUNT (sizeof(default_glasses) / sizeof(default_glasses[0])) -// Get number of glasses in the catalog -u32 get_glass_count(void) { - return glass_count; -} - -// Get glass at index -const Glass* get_glass(u32 index) { - if (index < glass_count) { - return &glasses[index]; +// Security validation functions +static GlamacResult validate_file_path(const char* path) { + if (!path) return GLAMAC_ERROR_INVALID_ARGUMENT; + + size_t len = strlen(path); + if (len == 0 || len >= MAX_PATH_LEN) { + return GLAMAC_ERROR_INVALID_PATH; } - return NULL; -} - -// Get glass name -const byte* get_glass_name(u32 index) { - if (index < glass_count) { - return glasses[index].name; + + // Check for directory traversal attempts + if (strstr(path, "..") || strstr(path, "//") || + strchr(path, '|') || strchr(path, ';') || strchr(path, '&')) { + return GLAMAC_ERROR_INVALID_PATH; } - return NULL; + + return GLAMAC_SUCCESS; } -// Find data range in glass catalog -void find_glass_data_range(f32 *minAbbe, f32 *maxAbbe, f32 *minRI, f32 *maxRI) { - if (glass_count == 0) { - *minAbbe = *maxAbbe = 50.0f; - *minRI = *maxRI = 1.5f; - return; +static GlamacResult validate_file_size(FILE* file, long* size) { + if (!file || !size) return GLAMAC_ERROR_INVALID_ARGUMENT; + + if (fseek(file, 0, SEEK_END) != 0) { + return GLAMAC_ERROR_FILE_NOT_FOUND; } - *minAbbe = glasses[0].abbeNumber; - *maxAbbe = glasses[0].abbeNumber; - *minRI = glasses[0].refractiveIndex; - *maxRI = glasses[0].refractiveIndex; + *size = ftell(file); + if (*size < 0) { + return GLAMAC_ERROR_FILE_NOT_FOUND; + } - for (u32 i = 1; i < glass_count; i++) { - if (glasses[i].abbeNumber < *minAbbe) *minAbbe = glasses[i].abbeNumber; - if (glasses[i].abbeNumber > *maxAbbe) *maxAbbe = glasses[i].abbeNumber; - if (glasses[i].refractiveIndex < *minRI) *minRI = glasses[i].refractiveIndex; - if (glasses[i].refractiveIndex > *maxRI) *maxRI = glasses[i].refractiveIndex; + if (*size > MAX_JSON_FILE_SIZE) { + return GLAMAC_ERROR_FILE_TOO_LARGE; } - // Add a small margin to the range - const f32 abbeMargin = (*maxAbbe - *minAbbe) * 0.1f; - const f32 riMargin = (*maxRI - *minRI) * 0.1f; - *minAbbe -= abbeMargin; - *maxAbbe += abbeMargin; - *minRI -= riMargin; - *maxRI += riMargin; + if (fseek(file, 0, SEEK_SET) != 0) { + return GLAMAC_ERROR_FILE_NOT_FOUND; + } + + return GLAMAC_SUCCESS; } -// Simple JSON string helper functions -static byte* find_json_string(const byte* json, const byte* key) { - byte search_pattern[256]; - snprintf((char*)search_pattern, sizeof(search_pattern), "\"%s\":", (char*)key); +// Secure JSON parsing functions +static char* safe_find_json_string(const char* json, const char* key, size_t json_len) { + if (!json || !key) return NULL; - byte* found = (byte*)strstr((char*)json, (char*)search_pattern); + // Create search pattern with proper bounds checking + char search_pattern[128]; + int ret = snprintf(search_pattern, sizeof(search_pattern), "\"%s\":", key); + if (ret < 0 || ret >= (int)sizeof(search_pattern)) { + return NULL; + } + + const char* found = strstr(json, search_pattern); if (!found) return NULL; + // Ensure we don't go beyond json buffer + size_t offset = found - json; + if (offset >= json_len) return NULL; + // Move past the key and find the opening quote - found = (byte*)strchr((char*)found + strlen((char*)search_pattern), '"'); - if (!found) return NULL; + found += strlen(search_pattern); + while (*found == ' ' || *found == '\t') found++; + + if (*found != '"') return NULL; found++; // Move past opening quote - // Find closing quote - byte* end = (byte*)strchr((char*)found, '"'); - if (!end) return NULL; + // Find closing quote, handling escape sequences + const char* end = found; + while (*end && *end != '"') { + if (*end == '\\' && *(end + 1)) { + end += 2; // Skip escaped character + } else { + end++; + } + + // Prevent runaway parsing + if ((size_t)(end - json) >= json_len) return NULL; + } + + if (*end != '"') return NULL; - // Allocate and copy string + // Calculate length and validate size_t len = end - found; - byte* result = (byte*)malloc(len + 1); - if (result) { - memcpy(result, found, len); - result[len] = '\0'; + if (len == 0 || len > MAX_JSON_STRING_LEN) { + return NULL; } + + // Allocate and copy string safely + char* result = (char*)malloc(len + 1); + if (!result) return NULL; + + memcpy(result, found, len); + result[len] = '\0'; + return result; } -static f32 find_json_number(const byte* json, const byte* key) { - byte search_pattern[256]; - snprintf((char*)search_pattern, sizeof(search_pattern), "\"%s\":", (char*)key); +static double safe_find_json_number(const char* json, const char* key, size_t json_len) { + if (!json || !key) return 0.0; + + char search_pattern[128]; + int ret = snprintf(search_pattern, sizeof(search_pattern), "\"%s\":", key); + if (ret < 0 || ret >= (int)sizeof(search_pattern)) { + return 0.0; + } + + const char* found = strstr(json, search_pattern); + if (!found) return 0.0; - byte* found = (byte*)strstr((char*)json, (char*)search_pattern); - if (!found) return 0.0f; + size_t offset = found - json; + if (offset >= json_len) return 0.0; // Move past the key and find the number - found += strlen((char*)search_pattern); - while (*found == ' ' || *found == '\t') found++; // Skip whitespace + found += strlen(search_pattern); + while (*found == ' ' || *found == '\t') found++; - return (f32)atof((char*)found); + // Validate number format + const char* num_start = found; + if (*found == '-') found++; // Allow negative + + if (!isdigit(*found)) return 0.0; + + // Find end of number + while (isdigit(*found) || *found == '.') found++; + + // Extract number safely + size_t num_len = found - num_start; + if (num_len == 0 || num_len > 32) return 0.0; + + char num_str[64]; + memcpy(num_str, num_start, num_len); + num_str[num_len] = '\0'; + + return atof(num_str); } -// Load glasses from JSON file -b32 load_glasses_from_json(const byte* json_path, const byte* manufacturer_filter) { - FILE* file = fopen((char*)json_path, "r"); - if (!file) { - printf("Warning: Could not open JSON file: %s\n", json_path); - return 0; +// Safe memory management +static void cleanup_glass_context(GlassDataContext* ctx) { + if (ctx && ctx->glasses) { + free(ctx->glasses); + ctx->glasses = NULL; + ctx->count = 0; + ctx->capacity = 0; + ctx->using_json_data = 0; } +} + +static GlamacResult allocate_glasses(GlassDataContext* ctx, u32 count) { + if (!ctx || count == 0) return GLAMAC_ERROR_INVALID_ARGUMENT; - // Get file size - fseek(file, 0, SEEK_END); - long file_size = ftell(file); - fseek(file, 0, SEEK_SET); - - // Read entire file - byte* json_content = (byte*)malloc(file_size + 1); - if (!json_content) { - fclose(file); - return 0; + // Prevent integer overflow + if (count > (SIZE_MAX / sizeof(Glass))) { + return GLAMAC_ERROR_MEMORY; } - fread(json_content, 1, file_size, file); - json_content[file_size] = '\0'; - fclose(file); - - // Find the manufacturer section - byte manufacturer_key[256]; - snprintf((char*)manufacturer_key, sizeof(manufacturer_key), "\"%s\":", (char*)manufacturer_filter); + cleanup_glass_context(ctx); - byte* mfg_section = (byte*)strstr((char*)json_content, (char*)manufacturer_key); - if (!mfg_section) { - printf("Warning: Manufacturer '%s' not found in JSON\n", manufacturer_filter); - free(json_content); - return 0; + ctx->glasses = (Glass*)calloc(count, sizeof(Glass)); + if (!ctx->glasses) { + return GLAMAC_ERROR_MEMORY; } - // Find the glasses array within this manufacturer - byte* glasses_array = (byte*)strstr((char*)mfg_section, "\"glasses\":"); + ctx->capacity = count; + ctx->count = 0; + + return GLAMAC_SUCCESS; +} + +// Helper function to parse glasses from a manufacturer section +static GlamacResult parse_manufacturer_glasses(const char* manufacturer_section, size_t section_len, const char* manufacturer_name) { + // Find glasses array + const char* glasses_array = strstr(manufacturer_section, "\"glasses\":"); if (!glasses_array) { - printf("Warning: No glasses array found for manufacturer '%s'\n", manufacturer_filter); - free(json_content); - return 0; + return GLAMAC_ERROR_NO_GLASSES_FOUND; } - // Find opening bracket of glasses array - byte* array_start = (byte*)strchr((char*)glasses_array, '['); + // Find opening bracket + const char* array_start = strchr(glasses_array, '['); if (!array_start) { - free(json_content); - return 0; + return GLAMAC_ERROR_INVALID_JSON; } array_start++; - // Count glasses by counting glass objects - u32 count = 0; - byte* pos = array_start; - while ((pos = (byte*)strchr((char*)pos, '{')) != NULL) { - count++; - pos++; + // Parse glasses in this section + const char* pos = array_start; + while ((pos = strchr(pos, '{')) != NULL && pos < manufacturer_section + section_len) { + // Find matching closing brace for this glass object + const char* obj_start = pos; + const char* obj_end = NULL; + int brace_depth = 0; + const char* scan = pos; + + // Count braces to find the end of this glass object + while (*scan && scan < manufacturer_section + section_len) { + if (*scan == '{') { + brace_depth++; + } else if (*scan == '}') { + brace_depth--; + if (brace_depth == 0) { + obj_end = scan; + break; + } + } + scan++; + } + + if (!obj_end) break; + + // Extract glass data from this complete glass object + size_t obj_len = obj_end - obj_start + 1; + char* name = safe_find_json_string(obj_start, "name", obj_len); + double nd = safe_find_json_number(obj_start, "nd", obj_len); + double vd = safe_find_json_number(obj_start, "vd", obj_len); + char* glass_code = safe_find_json_string(obj_start, "glass_code", obj_len); + + if (name && nd > 0.0 && vd > 0.0 && nd < 10.0 && vd < 200.0) { + // Expand glasses array if needed + if (g_glass_context.count >= g_glass_context.capacity) { + u32 new_capacity = g_glass_context.capacity * 2; + if (new_capacity < 100) new_capacity = 100; + + Glass* new_glasses = (Glass*)realloc(g_glass_context.glasses, new_capacity * sizeof(Glass)); + if (!new_glasses) { + if (name) free(name); + if (glass_code) free(glass_code); + return GLAMAC_ERROR_MEMORY; + } + g_glass_context.glasses = new_glasses; + g_glass_context.capacity = new_capacity; + } + + // Copy glass data safely + size_t name_len = strlen(name); + if (name_len < sizeof(g_glass_context.glasses[g_glass_context.count].name)) { + strcpy((char*)g_glass_context.glasses[g_glass_context.count].name, name); + g_glass_context.glasses[g_glass_context.count].abbeNumber = (f32)vd; + g_glass_context.glasses[g_glass_context.count].refractiveIndex = (f32)nd; + + // Copy glass code safely + if (glass_code) { + size_t code_len = strlen(glass_code); + if (code_len < sizeof(g_glass_context.glasses[g_glass_context.count].glass_code)) { + strcpy((char*)g_glass_context.glasses[g_glass_context.count].glass_code, glass_code); + } else { + strncpy((char*)g_glass_context.glasses[g_glass_context.count].glass_code, glass_code, + sizeof(g_glass_context.glasses[g_glass_context.count].glass_code) - 1); + g_glass_context.glasses[g_glass_context.count].glass_code[sizeof(g_glass_context.glasses[g_glass_context.count].glass_code) - 1] = '\0'; + } + } else { + strcpy((char*)g_glass_context.glasses[g_glass_context.count].glass_code, "N/A"); + } + + // Copy manufacturer name + if (manufacturer_name) { + size_t mfg_len = strlen(manufacturer_name); + if (mfg_len < sizeof(g_glass_context.glasses[g_glass_context.count].manufacturer)) { + strcpy((char*)g_glass_context.glasses[g_glass_context.count].manufacturer, manufacturer_name); + } else { + strncpy((char*)g_glass_context.glasses[g_glass_context.count].manufacturer, manufacturer_name, + sizeof(g_glass_context.glasses[g_glass_context.count].manufacturer) - 1); + g_glass_context.glasses[g_glass_context.count].manufacturer[sizeof(g_glass_context.glasses[g_glass_context.count].manufacturer) - 1] = '\0'; + } + } else { + strcpy((char*)g_glass_context.glasses[g_glass_context.count].manufacturer, "Unknown"); + } + + g_glass_context.count++; + } + } + + if (name) { + free(name); + name = NULL; + } + + if (glass_code) { + free(glass_code); + glass_code = NULL; + } + + // Move to the next glass object + pos = obj_end + 1; } - if (count == 0) { - free(json_content); - return 0; + return GLAMAC_SUCCESS; +} + +// Load specific manufacturer +static GlamacResult load_single_manufacturer(const char* json_content, size_t json_len, const char* manufacturer_filter) { + char manufacturer_key[MAX_MANUFACTURER_NAME_LEN + 16]; + int ret = snprintf(manufacturer_key, sizeof(manufacturer_key), "\"%s\":", manufacturer_filter); + if (ret < 0 || ret >= (int)sizeof(manufacturer_key)) { + return GLAMAC_ERROR_INVALID_ARGUMENT; } - // Allocate glasses array - if (glasses) { - free(glasses); + const char* mfg_section = strstr(json_content, manufacturer_key); + if (!mfg_section) { + return GLAMAC_ERROR_MANUFACTURER_NOT_FOUND; } - glasses = (Glass*)malloc(count * sizeof(Glass)); - if (!glasses) { - free(json_content); - return 0; + + // Find the end of this manufacturer section (next manufacturer or end of object) + const char* section_end = json_content + json_len; + const char* next_mfg = mfg_section + strlen(manufacturer_key); + + // Look for the next manufacturer key or end of object + while (next_mfg < json_content + json_len) { + if (*next_mfg == '"' && next_mfg[1] != 'g') { // Not "glasses" + // Check if this looks like another manufacturer key + const char* colon = strchr(next_mfg, ':'); + if (colon && colon < next_mfg + 50) { // Reasonable manufacturer name length + section_end = next_mfg; + break; + } + } + next_mfg++; } - // Parse each glass - glass_count = 0; - pos = array_start; + size_t section_len = section_end - mfg_section; - while (glass_count < count && (pos = (byte*)strchr((char*)pos, '{')) != NULL) { - // Find end of this glass object - byte* obj_end = (byte*)strchr((char*)pos, '}'); - if (!obj_end) break; + // Initialize with estimated capacity + GlamacResult result = allocate_glasses(&g_glass_context, 1000); + if (result != GLAMAC_SUCCESS) { + return result; + } + + result = parse_manufacturer_glasses(mfg_section, section_len, manufacturer_filter); + if (result != GLAMAC_SUCCESS) { + return result; + } + + if (g_glass_context.count == 0) { + cleanup_glass_context(&g_glass_context); + return GLAMAC_ERROR_NO_GLASSES_FOUND; + } + + g_glass_context.using_json_data = 1; + return GLAMAC_SUCCESS; +} + +// Load all manufacturers +static GlamacResult load_all_manufacturers(const char* json_content, size_t json_len) { + const char* manufacturers[] = {"SCHOTT", "HOYA", "CDGM", "Ohara", NULL}; + + // Initialize with larger capacity for all manufacturers + GlamacResult result = allocate_glasses(&g_glass_context, 5000); + if (result != GLAMAC_SUCCESS) { + return result; + } + + b32 found_any = 0; + + for (int i = 0; manufacturers[i] != NULL; i++) { + char manufacturer_key[MAX_MANUFACTURER_NAME_LEN + 16]; + int ret = snprintf(manufacturer_key, sizeof(manufacturer_key), "\"%s\":", manufacturers[i]); + if (ret < 0 || ret >= (int)sizeof(manufacturer_key)) { + continue; // Skip this manufacturer + } + + const char* mfg_section = strstr(json_content, manufacturer_key); + if (!mfg_section) { + continue; // This manufacturer not found in JSON + } - // Extract glass data from this object - byte* name = find_json_string(pos, (byte*)"name"); - f32 nd = find_json_number(pos, (byte*)"nd"); - f32 vd = find_json_number(pos, (byte*)"vd"); + // Find the end of this manufacturer section + const char* section_end = json_content + json_len; + const char* next_mfg = mfg_section + strlen(manufacturer_key); - if (name && nd > 0.0f && vd > 0.0f) { - // Copy name (truncate if too long) - strncpy((char*)glasses[glass_count].name, (char*)name, sizeof(glasses[glass_count].name) - 1); - glasses[glass_count].name[sizeof(glasses[glass_count].name) - 1] = '\0'; - glasses[glass_count].abbeNumber = vd; - glasses[glass_count].refractiveIndex = nd; - glass_count++; + // Look for the next manufacturer key + for (int j = 0; manufacturers[j] != NULL; j++) { + if (j == i) continue; // Skip current manufacturer + + char next_key[MAX_MANUFACTURER_NAME_LEN + 16]; + ret = snprintf(next_key, sizeof(next_key), "\"%s\":", manufacturers[j]); + if (ret < 0 || ret >= (int)sizeof(next_key)) continue; + + const char* next_section = strstr(next_mfg, next_key); + if (next_section && next_section < section_end) { + section_end = next_section; + } } - if (name) free(name); - pos = obj_end + 1; + size_t section_len = section_end - mfg_section; + + GlamacResult parse_result = parse_manufacturer_glasses(mfg_section, section_len, manufacturers[i]); + if (parse_result == GLAMAC_SUCCESS) { + found_any = 1; + } } - free(json_content); - using_json_data = (glass_count > 0); + if (!found_any || g_glass_context.count == 0) { + cleanup_glass_context(&g_glass_context); + return GLAMAC_ERROR_NO_GLASSES_FOUND; + } + + g_glass_context.using_json_data = 1; + return GLAMAC_SUCCESS; +} + +// Secure JSON loading +static GlamacResult load_glasses_from_json_secure(const char* json_path, const char* manufacturer_filter) { + if (!json_path) { + return GLAMAC_ERROR_INVALID_ARGUMENT; + } + + // Validate inputs + GlamacResult result = validate_file_path(json_path); + if (result != GLAMAC_SUCCESS) return result; + + if (manufacturer_filter && strlen(manufacturer_filter) >= MAX_MANUFACTURER_NAME_LEN) { + return GLAMAC_ERROR_INVALID_ARGUMENT; + } + + // Open file safely + FILE* file = fopen(json_path, "r"); + if (!file) { + return GLAMAC_ERROR_FILE_NOT_FOUND; + } + + // Validate file size + long file_size; + result = validate_file_size(file, &file_size); + if (result != GLAMAC_SUCCESS) { + fclose(file); + return result; + } + + // Allocate buffer with bounds checking + char* json_content = (char*)malloc((size_t)file_size + 1); + if (!json_content) { + fclose(file); + return GLAMAC_ERROR_MEMORY; + } + + // Read file safely + size_t bytes_read = fread(json_content, 1, (size_t)file_size, file); + fclose(file); + + if (bytes_read != (size_t)file_size) { + free(json_content); + return GLAMAC_ERROR_INVALID_JSON; + } + + json_content[bytes_read] = '\0'; - printf("Loaded %u %s glasses from JSON\n", glass_count, manufacturer_filter); - return using_json_data; + // Handle both single manufacturer and all manufacturers cases + if (manufacturer_filter) { + // Load specific manufacturer + result = load_single_manufacturer(json_content, bytes_read, manufacturer_filter); + } else { + // Load all manufacturers + result = load_all_manufacturers(json_content, bytes_read); + } + + free(json_content); + return result; } -// Try multiple JSON file paths for cross-platform compatibility -static b32 try_load_json_with_fallbacks(const byte* manufacturer_filter) { - // List of possible JSON file paths to try - const byte* json_paths[] = { - (byte*)"data/json/glasses.json", // Linux style - (byte*)"data\\json\\glasses.json", // Windows style - (byte*)"glasses.json", // Current directory - (byte*)"..\\data\\json\\glasses.json", // Windows relative - (byte*)"../data/json/glasses.json", // Linux relative +// Cross-platform path handling +static GlamacResult try_load_json_with_fallbacks_secure(const char* manufacturer_filter) { + // manufacturer_filter can be NULL to load all manufacturers + + const char* json_paths[] = { + "data/json/glasses.json", // Linux style + "data\\json\\glasses.json", // Windows style + "glasses.json", // Current directory + "..\\data\\json\\glasses.json", // Windows relative + "../data/json/glasses.json", // Linux relative NULL }; for (int i = 0; json_paths[i] != NULL; i++) { printf("Trying JSON path: %s\n", json_paths[i]); - if (load_glasses_from_json(json_paths[i], manufacturer_filter)) { - return 1; // Success + GlamacResult result = load_glasses_from_json_secure(json_paths[i], manufacturer_filter); + if (result == GLAMAC_SUCCESS) { + return GLAMAC_SUCCESS; } + // Log specific error but continue trying + printf(" Failed: %s\n", glamac_error_string(result)); } - return 0; // All paths failed + return GLAMAC_ERROR_FILE_NOT_FOUND; +} + +// Public API implementation +u32 get_glass_count(void) { + return g_glass_context.count; +} + +const Glass* get_glass(u32 index) { + if (index < g_glass_context.count && g_glass_context.glasses) { + return &g_glass_context.glasses[index]; + } + return NULL; +} + +const byte* get_glass_name(u32 index) { + if (index < g_glass_context.count && g_glass_context.glasses) { + return g_glass_context.glasses[index].name; + } + return NULL; +} + +void find_glass_data_range(f32 *minAbbe, f32 *maxAbbe, f32 *minRI, f32 *maxRI) { + if (!minAbbe || !maxAbbe || !minRI || !maxRI) return; + + if (g_glass_context.count == 0 || !g_glass_context.glasses) { + *minAbbe = *maxAbbe = 50.0f; + *minRI = *maxRI = 1.5f; + return; + } + + *minAbbe = g_glass_context.glasses[0].abbeNumber; + *maxAbbe = g_glass_context.glasses[0].abbeNumber; + *minRI = g_glass_context.glasses[0].refractiveIndex; + *maxRI = g_glass_context.glasses[0].refractiveIndex; + + for (u32 i = 1; i < g_glass_context.count; i++) { + if (g_glass_context.glasses[i].abbeNumber < *minAbbe) *minAbbe = g_glass_context.glasses[i].abbeNumber; + if (g_glass_context.glasses[i].abbeNumber > *maxAbbe) *maxAbbe = g_glass_context.glasses[i].abbeNumber; + if (g_glass_context.glasses[i].refractiveIndex < *minRI) *minRI = g_glass_context.glasses[i].refractiveIndex; + if (g_glass_context.glasses[i].refractiveIndex > *maxRI) *maxRI = g_glass_context.glasses[i].refractiveIndex; + } + + // Add a small margin to the range + const f32 abbeMargin = (*maxAbbe - *minAbbe) * 0.1f; + const f32 riMargin = (*maxRI - *minRI) * 0.1f; + *minAbbe -= abbeMargin; + *maxAbbe += abbeMargin; + *minRI -= riMargin; + *maxRI += riMargin; +} + +b32 load_glasses_from_json(const byte* json_path, const byte* manufacturer_filter) { + GlamacResult result = load_glasses_from_json_secure((const char*)json_path, (const char*)manufacturer_filter); + return (result == GLAMAC_SUCCESS) ? 1 : 0; } -// Initialize glass data void initialize_glass_data(void) { - // Try to load SCHOTT glasses from JSON with multiple path attempts - if (!try_load_json_with_fallbacks((byte*)"SCHOTT")) { + // Try to load all manufacturers from JSON by default + GlamacResult result = try_load_json_with_fallbacks_secure(NULL); + + if (result != GLAMAC_SUCCESS) { // Fallback to default static data - printf("JSON loading failed, using fallback glass data\n"); + printf("JSON loading failed (%s), using fallback glass data\n", glamac_error_string(result)); - if (glasses) { - free(glasses); - } + cleanup_glass_context(&g_glass_context); - glass_count = DEFAULT_GLASS_COUNT; - glasses = (Glass*)malloc(glass_count * sizeof(Glass)); - if (glasses) { - memcpy(glasses, default_glasses, glass_count * sizeof(Glass)); - using_json_data = 0; + GlamacResult alloc_result = allocate_glasses(&g_glass_context, DEFAULT_GLASS_COUNT); + if (alloc_result == GLAMAC_SUCCESS) { + memcpy(g_glass_context.glasses, default_glasses, DEFAULT_GLASS_COUNT * sizeof(Glass)); + g_glass_context.count = DEFAULT_GLASS_COUNT; + g_glass_context.using_json_data = 0; + + // Set manufacturer for default glasses + for (u32 i = 0; i < DEFAULT_GLASS_COUNT; i++) { + strcpy((char*)g_glass_context.glasses[i].manufacturer, "SCHOTT"); + } } else { - glass_count = 0; + printf("Critical error: Failed to allocate fallback glass data\n"); + g_glass_context.count = 0; } } } + +// Cleanup function for proper resource management +void cleanup_glass_data(void) { + cleanup_glass_context(&g_glass_context); +} \ No newline at end of file diff --git a/src/glautils/fgla.c b/src/glautils/fgla.c new file mode 100644 index 0000000..68232fd --- /dev/null +++ b/src/glautils/fgla.c @@ -0,0 +1,521 @@ +/** + * fgla.c - Find Glass utility for searching optical glass database + * + * Copyright (C) 2025 https://optics-design.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the COPYING file for the full license text. + */ + +#include +#include +#include +#include +#include +#include "glass_data.h" +#include "glamacdef.h" + +// Security constants +#define MAX_SEARCH_TERM_LEN 256 +#define MAX_GLASS_NAME_LEN 256 +#define MAX_CATALOG_COUNT 10 +#define MAX_NORMALIZED_LEN 512 +#define GLASS_CODE_LEN 6 + +// Output format types +typedef enum { + OUTPUT_CSV = 0, + OUTPUT_TABLE = 1, + OUTPUT_JSON = 2 +} OutputFormat; + +// Function to safely convert string to lowercase with length limit +void to_lowercase_safe(char* str, size_t max_len) { + if (!str) return; + + for (size_t i = 0; i < max_len && str[i]; i++) { + str[i] = tolower(str[i]); + } +} + +// Function to safely normalize string by removing dashes and converting to lowercase +int normalize_string_safe(const char* input, char* output, size_t output_size) { + if (!input || !output || output_size < 2) return -1; + + size_t input_len = strnlen(input, MAX_GLASS_NAME_LEN); + size_t j = 0; + + for (size_t i = 0; i < input_len && j < output_size - 1; i++) { + if (input[i] != '-') { // Skip dashes + output[j++] = tolower(input[i]); + } + } + output[j] = '\0'; + return 0; +} + +// Function to safely check if needle is found in haystack (case-insensitive, dash-insensitive) +int contains_substring_safe(const char* haystack, const char* needle) { + if (!haystack || !needle) return 0; + + // Validate input lengths + size_t h_len = strnlen(haystack, MAX_GLASS_NAME_LEN); + size_t n_len = strnlen(needle, MAX_SEARCH_TERM_LEN); + + if (h_len == 0 || n_len == 0 || h_len >= MAX_GLASS_NAME_LEN || n_len >= MAX_SEARCH_TERM_LEN) { + return 0; + } + + // Allocate normalized buffers safely + char* haystack_normalized = malloc(h_len + 1); + char* needle_normalized = malloc(n_len + 1); + + if (!haystack_normalized || !needle_normalized) { + free(haystack_normalized); + free(needle_normalized); + return 0; + } + + // Create normalized copies (lowercase, no dashes) + if (normalize_string_safe(haystack, haystack_normalized, h_len + 1) != 0 || + normalize_string_safe(needle, needle_normalized, n_len + 1) != 0) { + free(haystack_normalized); + free(needle_normalized); + return 0; + } + + int result = strstr(haystack_normalized, needle_normalized) != NULL; + + free(haystack_normalized); + free(needle_normalized); + + return result; +} + +// Function to check if manufacturer matches any of the specified catalogs +int matches_catalog(const char* manufacturer, const char* catalog_list[], int catalog_count) { + if (catalog_count == 0) return 1; // No filter = show all + + for (int i = 0; i < catalog_count; i++) { + // Case-insensitive comparison + if (strcasecmp(manufacturer, catalog_list[i]) == 0) { + return 1; + } + } + return 0; +} + +// Function to validate search term input +int validate_search_term(const char* term) { + if (!term) return 0; + + size_t len = strnlen(term, MAX_SEARCH_TERM_LEN); + if (len == 0 || len >= MAX_SEARCH_TERM_LEN) return 0; + + // Check for dangerous characters + for (size_t i = 0; i < len; i++) { + char c = term[i]; + if (c == '\0') break; + // Allow alphanumeric, dash, space, and 'x' for glass codes + if (!isalnum(c) && c != '-' && c != ' ' && c != 'x' && c != 'X') { + return 0; + } + } + + return 1; +} + +// Function to check if a search term is a glass code pattern +// Glass code pattern: exactly 6 characters, contains only digits and/or 'x' +int is_glass_code_pattern(const char* term) { + if (!term) return 0; + + size_t len = strnlen(term, GLASS_CODE_LEN + 1); + if (len != GLASS_CODE_LEN) return 0; + + for (size_t i = 0; i < GLASS_CODE_LEN; i++) { + char c = term[i]; + if (c != 'x' && c != 'X' && !isdigit(c)) { + return 0; // Invalid character + } + } + + return 1; // All characters are digits or 'x' +} + +// Function to safely check if a glass code matches a pattern +// Pattern can contain 'x' as wildcards, or be an exact 6-digit match +int matches_glass_code_pattern_safe(const char* glass_code, const char* pattern) { + if (!glass_code || !pattern) return 0; + + size_t code_len = strnlen(glass_code, 20); // Glass codes can be longer than 6 + size_t pattern_len = strnlen(pattern, GLASS_CODE_LEN + 1); + + if (code_len < GLASS_CODE_LEN || pattern_len != GLASS_CODE_LEN) return 0; + + // Extract first 6 digits from glass_code safely + char code_digits[GLASS_CODE_LEN + 1]; + size_t digit_count = 0; + + for (size_t i = 0; i < code_len && digit_count < GLASS_CODE_LEN; i++) { + if (isdigit(glass_code[i])) { + code_digits[digit_count++] = glass_code[i]; + } + } + + if (digit_count < GLASS_CODE_LEN) return 0; // Not enough digits + code_digits[GLASS_CODE_LEN] = '\0'; + + // Check if pattern has any wildcards + int has_wildcards = 0; + for (size_t i = 0; i < GLASS_CODE_LEN; i++) { + if (tolower(pattern[i]) == 'x') { + has_wildcards = 1; + break; + } + } + + if (!has_wildcards) { + // Exact match comparison + return strcmp(code_digits, pattern) == 0; + } else { + // Wildcard pattern matching + for (size_t i = 0; i < GLASS_CODE_LEN; i++) { + char p = tolower(pattern[i]); + if (p != 'x' && p != code_digits[i]) { + return 0; + } + } + return 1; + } +} + +void print_usage(const char* program_name) { + printf("fgla - Find Glass utility\n"); + printf("Usage: %s [OPTIONS] \n", program_name); + printf("\n"); + printf("Search for optical glasses by name or glass code\n"); + printf("- Name search: case-insensitive, dash-insensitive partial matching\n"); + printf("- Glass code search: 6-digit exact match (e.g., '517642') or pattern with 'x' wildcards (e.g., '51x64x')\n"); + printf("\n"); + printf("Options:\n"); + printf(" -c Search only specified catalog(s) (case-insensitive)\n"); + printf(" Multiple catalogs: -c SCHOTT HOYA CDGM Ohara\n"); + printf(" If not specified, searches all catalogs\n"); + printf(" -f Output format: csv, table, json (default: csv)\n"); + printf("\n"); + printf("Examples:\n"); + printf(" %s BK # Find all glasses containing 'BK'\n", program_name); + printf(" %s 517642 # Find glasses with exact glass code\n", program_name); + printf(" %s 51x64x # Find glasses with glass code matching pattern\n", program_name); + printf(" %s -c SCHOTT \"N-SF\" # Find N-SF glasses in SCHOTT catalog only\n", program_name); + printf(" %s -f table BK7 # Display results in table format\n", program_name); + printf(" %s -f json -c HOYA nsf # Output HOYA NSF glasses as JSON\n", program_name); +} + +// Error handling with detailed messages +typedef enum { + FGLA_SUCCESS = 0, + FGLA_ERROR_INVALID_ARGS = 1, + FGLA_ERROR_INVALID_SEARCH_TERM = 2, + FGLA_ERROR_NO_DATABASE = 3, + FGLA_ERROR_NO_MATCHES = 4, + FGLA_ERROR_MEMORY = 5 +} FglaResult; + +void print_error_with_suggestion(FglaResult error, const char* context) { + switch (error) { + case FGLA_ERROR_INVALID_SEARCH_TERM: + fprintf(stderr, "Error: Invalid search term '%s'\n", context ? context : ""); + fprintf(stderr, "Search terms must be 1-%d characters and contain only letters, numbers, dashes, spaces, or 'x'\n", MAX_SEARCH_TERM_LEN - 1); + fprintf(stderr, "Examples: 'BK7', '517642', '51x64x'\n"); + break; + case FGLA_ERROR_NO_DATABASE: + fprintf(stderr, "Error: Could not load glass database.\n"); + fprintf(stderr, "Suggestions:\n"); + fprintf(stderr, " 1. Run 'python scripts/excel_to_json.py data/xlsx/ -o data/json/glasses.json'\n"); + fprintf(stderr, " 2. Check that data/json/glasses.json exists and is readable\n"); + fprintf(stderr, " 3. Verify current working directory with 'pwd'\n"); + break; + case FGLA_ERROR_NO_MATCHES: + fprintf(stderr, "No glasses found matching '%s'\n", context ? context : ""); + fprintf(stderr, "Try:\n"); + fprintf(stderr, " - A shorter or more general search term\n"); + fprintf(stderr, " - Checking spelling\n"); + fprintf(stderr, " - Using different manufacturers with -c option\n"); + break; + default: + fprintf(stderr, "Error: %s\n", context ? context : "Unknown error"); + break; + } +} + +// Output formatting functions +void print_header(OutputFormat format, u32 found_count, const char* search_term, int is_glass_code_search) { + switch (format) { + case OUTPUT_CSV: + if (is_glass_code_search) { + printf("Found %u glasses matching glass code pattern \"%s\"\n", found_count, search_term); + } else { + printf("Found %u glasses matching \"%s\"\n", found_count, search_term); + } + printf("Name,nd,vd,Glass Code,Manufacturer\n"); + break; + + case OUTPUT_TABLE: + if (is_glass_code_search) { + printf("Found %u glasses matching glass code pattern \"%s\"\n\n", found_count, search_term); + } else { + printf("Found %u glasses matching \"%s\"\n\n", found_count, search_term); + } + printf("%-20s %8s %8s %10s %12s\n", "Name", "nd", "vd", "Glass Code", "Manufacturer"); + printf("%-20s %8s %8s %10s %12s\n", "----", "--", "--", "----------", "------------"); + break; + + case OUTPUT_JSON: + printf("{\n"); + printf(" \"search_term\": \"%s\",\n", search_term); + printf(" \"search_type\": \"%s\",\n", is_glass_code_search ? "glass_code" : "name"); + printf(" \"found_count\": %u,\n", found_count); + printf(" \"glasses\": [\n"); + break; + } +} + +void print_glass_result(OutputFormat format, const Glass* glass, const char* glass_name, int is_first, int is_last) { + switch (format) { + case OUTPUT_CSV: + printf("%s,%.5f,%.2f,%s,%s\n", + glass_name, + glass->refractiveIndex, + glass->abbeNumber, + (const char*)glass->glass_code, + (const char*)glass->manufacturer); + break; + + case OUTPUT_TABLE: + printf("%-20s %8.5f %8.2f %10s %12s\n", + glass_name, + glass->refractiveIndex, + glass->abbeNumber, + (const char*)glass->glass_code, + (const char*)glass->manufacturer); + break; + + case OUTPUT_JSON: + printf("%s {\n", is_first ? "" : ",\n"); + printf(" \"name\": \"%s\",\n", glass_name); + printf(" \"nd\": %.5f,\n", glass->refractiveIndex); + printf(" \"vd\": %.2f,\n", glass->abbeNumber); + printf(" \"glass_code\": \"%s\",\n", (const char*)glass->glass_code); + printf(" \"manufacturer\": \"%s\"\n", (const char*)glass->manufacturer); + printf(" }"); + break; + } +} + +void print_footer(OutputFormat format) { + if (format == OUTPUT_JSON) { + printf("\n ]\n}\n"); + } +} + +int main(int argc, char* argv[]) { + const char* search_term = NULL; + const char* catalog_list[MAX_CATALOG_COUNT]; // Support up to MAX_CATALOG_COUNT catalogs + int catalog_count = 0; + OutputFormat output_format = OUTPUT_CSV; // Default format + + // Parse command line arguments + int i = 1; + while (i < argc) { + if (strcmp(argv[i], "-c") == 0) { + // Collect catalog names after -c until next option or unknown argument + i++; // Skip -c + const char* known_catalogs[] = {"SCHOTT", "HOYA", "CDGM", "Ohara", NULL}; + + while (i < argc && argv[i][0] != '-') { + // Check if this is a known catalog + int is_catalog = 0; + for (int j = 0; known_catalogs[j] != NULL; j++) { + if (strcasecmp(argv[i], known_catalogs[j]) == 0) { + is_catalog = 1; + break; + } + } + + if (is_catalog) { + if (catalog_count >= MAX_CATALOG_COUNT - 1) { // Leave room for NULL terminator + fprintf(stderr, "Error: Too many catalogs specified (maximum %d)\n", MAX_CATALOG_COUNT - 1); + return FGLA_ERROR_INVALID_ARGS; + } + catalog_list[catalog_count++] = argv[i]; + i++; + } else { + // Not a known catalog, stop collecting catalogs + break; + } + } + if (catalog_count == 0) { + fprintf(stderr, "Error: -c option requires at least one catalog name\n"); + print_usage(argv[0]); + return FGLA_ERROR_INVALID_ARGS; + } + } else if (strcmp(argv[i], "-f") == 0) { + // Format option + i++; // Skip -f + if (i >= argc) { + fprintf(stderr, "Error: -f option requires a format argument\n"); + print_usage(argv[0]); + return FGLA_ERROR_INVALID_ARGS; + } + + if (strcmp(argv[i], "csv") == 0) { + output_format = OUTPUT_CSV; + } else if (strcmp(argv[i], "table") == 0) { + output_format = OUTPUT_TABLE; + } else if (strcmp(argv[i], "json") == 0) { + output_format = OUTPUT_JSON; + } else { + fprintf(stderr, "Error: Unknown format '%s'. Supported formats: csv, table, json\n", argv[i]); + return FGLA_ERROR_INVALID_ARGS; + } + i++; + } else if (argv[i][0] == '-') { + fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); + print_usage(argv[0]); + return FGLA_ERROR_INVALID_ARGS; + } else { + if (search_term != NULL) { + fprintf(stderr, "Error: Multiple search terms not allowed\n"); + print_usage(argv[0]); + return FGLA_ERROR_INVALID_ARGS; + } + search_term = argv[i]; + i++; + } + } + + catalog_list[catalog_count] = NULL; // NULL-terminate the list + + if (search_term == NULL) { + print_usage(argv[0]); + return FGLA_ERROR_INVALID_ARGS; + } + + // Validate search term + if (!validate_search_term(search_term)) { + print_error_with_suggestion(FGLA_ERROR_INVALID_SEARCH_TERM, search_term); + return FGLA_ERROR_INVALID_SEARCH_TERM; + } + + // Try to load glass data from multiple possible locations + b32 loaded = 0; + const char* json_paths[] = { + "data/json/glasses.json", + "../data/json/glasses.json", + "glasses.json", + NULL + }; + + // Try to load JSON file + const char* successful_path = NULL; + for (int i = 0; json_paths[i] && !successful_path; i++) { + // Test if we can load from this path (always try all catalogs for path testing) + if (load_glasses_from_json((const byte*)json_paths[i], NULL)) { + successful_path = json_paths[i]; + } + } + + if (!successful_path) { + print_error_with_suggestion(FGLA_ERROR_NO_DATABASE, NULL); + fprintf(stderr, "Tried these locations:\n"); + for (int i = 0; json_paths[i]; i++) { + fprintf(stderr, " - %s\n", json_paths[i]); + } + return FGLA_ERROR_NO_DATABASE; + } + + // Load all glasses first, then filter by catalog during display + loaded = load_glasses_from_json((const byte*)successful_path, NULL); + + u32 glass_count = get_glass_count(); + u32 found_count = 0; + + // Determine search type + int is_glass_code_search = is_glass_code_pattern(search_term); + + // Pre-allocate matches array for performance (avoids double iteration) + u32* matching_indices = malloc(glass_count * sizeof(u32)); + if (!matching_indices) { + fprintf(stderr, "Error: Memory allocation failed\n"); + cleanup_glass_data(); + return FGLA_ERROR_MEMORY; + } + + // Single pass: find and store matches + found_count = 0; + for (u32 i = 0; i < glass_count; i++) { + const Glass* glass = get_glass(i); + if (!glass) continue; + + const char* glass_name = (const char*)get_glass_name(i); + if (!glass_name) continue; + + int matches = 0; + + if (is_glass_code_search) { + // Glass code pattern search + const char* glass_code = (const char*)glass->glass_code; + if (glass_code && matches_glass_code_pattern_safe(glass_code, search_term)) { + matches = 1; + } + } else { + // Name search + if (contains_substring_safe(glass_name, search_term)) { + matches = 1; + } + } + + // Check if this glass matches the search term and catalog filter + if (matches && matches_catalog((const char*)glass->manufacturer, catalog_list, catalog_count)) { + matching_indices[found_count++] = i; + } + } + + // Output results + if (found_count == 0) { + print_error_with_suggestion(FGLA_ERROR_NO_MATCHES, search_term); + free(matching_indices); + cleanup_glass_data(); + return FGLA_ERROR_NO_MATCHES; + } + + // Print header in requested format + print_header(output_format, found_count, search_term, is_glass_code_search); + + // Display matches from stored indices + for (u32 j = 0; j < found_count; j++) { + u32 i = matching_indices[j]; + const Glass* glass = get_glass(i); + const char* glass_name = (const char*)get_glass_name(i); + + if (glass && glass_name) { + print_glass_result(output_format, glass, glass_name, j == 0, j == found_count - 1); + } + } + + // Print footer in requested format + print_footer(output_format); + + free(matching_indices); + + // Cleanup + cleanup_glass_data(); + + return FGLA_SUCCESS; +} \ No newline at end of file -- cgit v1.2.3