summaryrefslogtreecommitdiff
path: root/src/glautils/fgla.c
diff options
context:
space:
mode:
authoradmin <admin@optics-design.com>2025-08-04 00:35:10 +0200
committeradmin <admin@optics-design.com>2025-08-04 00:35:10 +0200
commit4910356db2585e55d2876001e40b21e9b148bcc4 (patch)
tree26474e58de023c6f3913a1e11ee928f630a8b814 /src/glautils/fgla.c
parent8332d652eac5b46d8b8b02b25d47aa5355ef31cd (diff)
added fgla search
Diffstat (limited to 'src/glautils/fgla.c')
-rw-r--r--src/glautils/fgla.c521
1 files changed, 521 insertions, 0 deletions
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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#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] <search_term>\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 <catalogs> 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 <format> 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
Back to https://optics-design.com