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/glautils/fgla.c | 521 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 src/glautils/fgla.c (limited to 'src/glautils/fgla.c') 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