/** * 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; }