From 04b3fcb479f5aaae06d18b315a8bdc8c298f4eae Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 5 Aug 2025 11:28:41 +0200 Subject: removed clustering --- Makefile | 252 +------------------- Makefile.backup | 247 +++++++++++++++++++ build/Makefile | 45 ++++ build/common.mk | 60 +++++ build/cross-compile.mk | 94 ++++++++ build/dependencies.mk | 52 ++++ build/native.mk | 42 ++++ include/glamac/core/glamac_errors.h | 38 +++ include/glamac/core/glamacdef.h | 72 ++++++ include/glamac/core/security.h | 41 ++++ include/glamac/data/glass_data.h | 56 +++++ include/glamac/graphics/glamac_render.h | 50 ++++ include/glamac/graphics/glamac_view.h | 109 +++++++++ include/glamac/input/glamac_events.h | 27 +++ include/glamac_errors.h | 41 ---- include/glamac_events.h | 27 --- include/glamac_render.h | 50 ---- include/glamac_view.h | 175 -------------- include/glamacdef.h | 43 ---- include/glass_data.h | 56 ----- include/glautils/fgla.h | 113 +++++++++ src/glamac/glamac.c | 140 ++++++++--- src/glamac/glamac.d | 10 + src/glamac/glamac_errors.c | 6 +- src/glamac/glamac_errors.d | 3 + src/glamac/glamac_events.c | 142 ++++++++--- src/glamac/glamac_events.d | 10 + src/glamac/glamac_render.c | 364 ++++++++++++++++++++-------- src/glamac/glamac_render.d | 9 + src/glamac/glamac_view.c | 393 +------------------------------ src/glamac/glamac_view.d | 8 + src/glamac/glass_data.c | 280 ++++++++++++++++++++-- src/glamac/glass_data.d | 7 + src/glautils/fgla.c | 88 ++++--- src/glautils/fgla.d | 7 + test.txt | 302 ------------------------ tests/Makefile | 169 +++++++++++++ tests/data/test_glasses.json | 47 ++++ tests/fgla_test_internals.h | 28 +++ tests/integration/test_full_pipeline.c | 273 +++++++++++++++++++++ tests/integration/test_simple_pipeline.c | 221 +++++++++++++++++ tests/test_framework.h | 120 ++++++++++ tests/unit/test_fgla.c | 231 ++++++++++++++++++ tests/unit/test_glamac_view.c | 243 +++++++++++++++++++ tests/unit/test_glamac_view_simple.c | 168 +++++++++++++ tests/unit/test_glass_data.c | 174 ++++++++++++++ 46 files changed, 3570 insertions(+), 1563 deletions(-) create mode 100644 Makefile.backup create mode 100644 build/Makefile create mode 100644 build/common.mk create mode 100644 build/cross-compile.mk create mode 100644 build/dependencies.mk create mode 100644 build/native.mk create mode 100644 include/glamac/core/glamac_errors.h create mode 100644 include/glamac/core/glamacdef.h create mode 100644 include/glamac/core/security.h create mode 100644 include/glamac/data/glass_data.h create mode 100644 include/glamac/graphics/glamac_render.h create mode 100644 include/glamac/graphics/glamac_view.h create mode 100644 include/glamac/input/glamac_events.h delete mode 100644 include/glamac_errors.h delete mode 100644 include/glamac_events.h delete mode 100644 include/glamac_render.h delete mode 100644 include/glamac_view.h delete mode 100644 include/glamacdef.h delete mode 100644 include/glass_data.h create mode 100644 include/glautils/fgla.h create mode 100644 src/glamac/glamac.d create mode 100644 src/glamac/glamac_errors.d create mode 100644 src/glamac/glamac_events.d create mode 100644 src/glamac/glamac_render.d create mode 100644 src/glamac/glamac_view.d create mode 100644 src/glamac/glass_data.d create mode 100644 src/glautils/fgla.d delete mode 100644 test.txt create mode 100644 tests/Makefile create mode 100644 tests/data/test_glasses.json create mode 100644 tests/fgla_test_internals.h create mode 100644 tests/integration/test_full_pipeline.c create mode 100644 tests/integration/test_simple_pipeline.c create mode 100644 tests/test_framework.h create mode 100644 tests/unit/test_fgla.c create mode 100644 tests/unit/test_glamac_view.c create mode 100644 tests/unit/test_glamac_view_simple.c create mode 100644 tests/unit/test_glass_data.c diff --git a/Makefile b/Makefile index e2f0c54..f1a86e2 100644 --- a/Makefile +++ b/Makefile @@ -1,247 +1,11 @@ -# Simplified Makefile for glamac - SDL3 version -# Supports: Linux native, Windows cross-compilation +# Simplified root Makefile for glamac +# Delegates all build operations to the modular build system in build/ -# Detect OS and set platform-specific variables -UNAME_S := $(shell uname -s 2>/dev/null || echo Windows_NT) -ifeq ($(UNAME_S),Linux) - PLATFORM := linux - EXE_EXT := - MKDIR := mkdir -p - RM := rm -f - RMDIR := rm -rf - CC := gcc - MINGW_CC := x86_64-w64-mingw32-gcc - SDL3_LIBS := $(shell pkg-config --libs sdl3 SDL3_ttf 2>/dev/null || echo -lSDL3 -lSDL3_ttf) -lm - CROSS_PREFIX := /usr/x86_64-w64-mingw32 -else - PLATFORM := windows - EXE_EXT := .exe - MKDIR := mkdir - RM := del /Q - RMDIR := rmdir /s /q - CC := gcc - SDL3_LIBS := -lSDL3 -lSDL3_ttf -mwindows -endif +# Default target delegates to modular build system +%: + @$(MAKE) -C build $@ -# Directories -SRCDIR := src -BINDIR := bin -BINDIR_WIN := bin/win -INCDIR := include -DLL_CACHE := $(HOME)/.cache/glamac-dlls - -# Base flags -CFLAGS_BASE := -I$(INCDIR) -O2 -flto -# Security and warning flags -SECURITY_FLAGS := -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -WARNING_FLAGS := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wstack-protector -Wvla - -# Compiler flags -CFLAGS := $(CFLAGS_BASE) $(SECURITY_FLAGS) $(WARNING_FLAGS) -CFLAGS_NATIVE := $(CFLAGS) -march=native -# Windows cross-compilation flags (without stack protector to avoid libssp dependency) -WARNING_FLAGS_WIN := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wvla -CFLAGS_CROSS := $(CFLAGS_BASE) $(WARNING_FLAGS_WIN) -I$(CROSS_PREFIX)/include - -# Source files -GLAMAC_SRCS := $(wildcard $(SRCDIR)/glamac/*.c) -GLAUTILS_SRCS := $(wildcard $(SRCDIR)/glautils/*.c) -GLAUTILS_BINS := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR)/%$(EXE_EXT), $(GLAUTILS_SRCS)) -GLAUTILS_BINS_WIN := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR_WIN)/%.exe, $(GLAUTILS_SRCS)) - -# Glass data dependencies for fgla -GLASS_DATA_SRCS := $(SRCDIR)/glamac/glass_data.c $(SRCDIR)/glamac/glamac_errors.c - -# Default target -all: glamac glautils - -# Native build targets -glamac: $(BINDIR)/glamac$(EXE_EXT) - -glautils: $(GLAUTILS_BINS) - -$(BINDIR)/glamac$(EXE_EXT): $(GLAMAC_SRCS) | $(BINDIR) - @echo "Building glamac..." - $(CC) $^ $(CFLAGS_NATIVE) $(SDL3_LIBS) -o $@ - -# Special rule for fgla which needs glass_data dependencies -$(BINDIR)/fgla$(EXE_EXT): $(SRCDIR)/glautils/fgla.c $(GLASS_DATA_SRCS) | $(BINDIR) - $(CC) $^ $(CFLAGS_NATIVE) -o $@ - -# General rule for other glautils (excluding fgla) -$(BINDIR)/%$(EXE_EXT): $(SRCDIR)/glautils/%.c | $(BINDIR) - $(CC) $< $(CFLAGS_NATIVE) -o $@ - -$(BINDIR): - $(MKDIR) $(BINDIR) - -# Windows cross-compilation (Linux only) -ifeq ($(PLATFORM),linux) - -win: $(BINDIR_WIN)/glamac.exe win-dlls win-data - -win-all: win $(GLAUTILS_BINS_WIN) - -$(BINDIR_WIN)/glamac.exe: $(GLAMAC_SRCS) | $(BINDIR_WIN) - @echo "Cross-compiling glamac for Windows..." - @which $(MINGW_CC) >/dev/null 2>&1 || (echo "ERROR: Install mingw-w64-gcc first" && exit 1) - $(MINGW_CC) $^ $(CFLAGS_CROSS) -L$(CROSS_PREFIX)/lib -lmingw32 -lSDL3 -lSDL3_ttf -mwindows -static-libgcc -o $@ - -$(BINDIR_WIN)/%.exe: $(SRCDIR)/glautils/%.c | $(BINDIR_WIN) - $(MINGW_CC) $< $(CFLAGS_CROSS) -static-libgcc -o $@ - -$(BINDIR_WIN): - $(MKDIR) $(BINDIR_WIN) - -# Windows DLL management -win-dlls: | $(BINDIR_WIN) - @echo "Getting Windows DLLs..." - @$(MKDIR) $(DLL_CACHE) - @if [ ! -f "$(DLL_CACHE)/SDL3.dll" ]; then \ - echo "Downloading SDL3.dll..."; \ - cd $(DLL_CACHE) && \ - wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-3.2.10-win32-x64.zip && \ - unzip -j SDL3-3.2.10-win32-x64.zip SDL3.dll && \ - $(RM) SDL3-3.2.10-win32-x64.zip; \ - fi - @if [ ! -f "$(DLL_CACHE)/SDL3_ttf.dll" ]; then \ - echo "Downloading SDL3_ttf.dll..."; \ - cd $(DLL_CACHE) && \ - wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-3.2.2-win32-x64.zip && \ - unzip -j SDL3_ttf-3.2.2-win32-x64.zip SDL3_ttf.dll && \ - $(RM) SDL3_ttf-3.2.2-win32-x64.zip; \ - fi - @cp $(DLL_CACHE)/*.dll $(BINDIR_WIN)/ - @echo "Windows build ready in $(BINDIR_WIN)/" - -# Copy data files for Windows build -win-data: | $(BINDIR_WIN) - @echo "Copying data files for Windows..." - @$(MKDIR) $(BINDIR_WIN)/data/json - @if [ -f "data/json/glasses.json" ]; then \ - cp data/json/glasses.json $(BINDIR_WIN)/data/json/; \ - cp data/json/glasses.json $(BINDIR_WIN)/; \ - echo "Copied glasses.json to Windows build"; \ - else \ - echo "Warning: glasses.json not found, Windows build may use fallback data"; \ - fi - @echo "Copying font for Windows..." - @if [ -f "/usr/share/fonts/TTF/DejaVuSans.ttf" ]; then \ - cp /usr/share/fonts/TTF/DejaVuSans.ttf $(BINDIR_WIN)/; \ - echo "Copied DejaVuSans.ttf to Windows build"; \ - elif [ -f "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" ]; then \ - cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf $(BINDIR_WIN)/; \ - echo "Copied DejaVuSans.ttf to Windows build"; \ - else \ - echo "Warning: DejaVu font not found, Windows build may fail to start"; \ - fi - -# Cross-compilation setup -setup-cross: - @echo "Setting up cross-compilation..." - @which wget >/dev/null 2>&1 || (echo "Install wget first: sudo pacman -S wget" && exit 1) - @which $(MINGW_CC) >/dev/null 2>&1 || (echo "Install mingw-w64-gcc first: sudo pacman -S mingw-w64-gcc" && exit 1) - sudo $(MKDIR) $(CROSS_PREFIX)/include $(CROSS_PREFIX)/lib - @echo "Downloading SDL3 development libraries..." - cd /tmp && \ - wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-devel-3.2.10-mingw.tar.gz && \ - wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ - tar -xzf SDL3-devel-3.2.10-mingw.tar.gz && \ - tar -xzf SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ - sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ - sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ - sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ - sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ - $(RM) -rf SDL3-3.2.10* SDL3_ttf-3.2.2* - @echo "Cross-compilation setup complete!" - -else -# Windows host - disable cross-compilation -win win-all win-dlls setup-cross: - @echo "Cross-compilation not available on Windows. Use 'make all' instead." -endif - -# Excel to JSON conversion -EXCEL_FILES := $(wildcard data/Excel/*.xlsx) -JSON_FILES := $(patsubst data/Excel/%.xlsx, data/JSON/%.json, $(EXCEL_FILES)) - -convert-catalogs: $(JSON_FILES) - -data/JSON/%.json: data/Excel/%.xlsx scripts/excel_to_json.py - @echo "Converting $< to JSON..." - @$(MKDIR) data/JSON - @python3 scripts/excel_to_json.py $< -o $@ - -# Dependency management -deps: -ifeq ($(PLATFORM),linux) - @echo "Installing dependencies..." - sudo pacman -S --needed sdl3 git cmake pkgconf freetype2 python python-pandas python-openpyxl - @echo "Building SDL3_ttf from source..." - cd /tmp && $(RM) -rf SDL_ttf && \ - git clone https://github.com/libsdl-org/SDL_ttf.git && \ - cd SDL_ttf && git checkout release-3.2.2 && \ - cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && \ - make -C build -j$$(nproc) && sudo make -C build install && \ - sudo ldconfig - @echo "Dependencies installed!" -else - @echo "Install SDL3 development libraries manually on Windows." - @echo "Install Python with pandas and openpyxl for Excel conversion." -endif - -# Cleanup -clean: - $(RMDIR) $(BINDIR) 2>/dev/null || true - -clean-deps: -ifeq ($(PLATFORM),linux) - @echo "Removing SDL3 dependencies..." - sudo pacman -R sdl3 --noconfirm 2>/dev/null || true - sudo $(RM) -f /usr/lib/libSDL3_ttf.so* /usr/lib/libSDL3_ttf.a /usr/lib/pkgconfig/SDL3_ttf.pc - sudo $(RMDIR) /usr/include/SDL3_ttf/ 2>/dev/null || true - sudo ldconfig - @echo "Dependencies removed!" -endif - -clean-cache: - $(RMDIR) $(DLL_CACHE) 2>/dev/null || true - -clean-all: clean clean-cache - -rebuild: clean all - -# Help +# Special case for targets that need to be run from root directory +.PHONY: help help: - @echo "GlaMaC Build System" - @echo "" - @echo "Build targets:" - @echo " all - Build for current platform" - @echo " glamac - Build main application only" - @echo " glautils - Build utilities only" - @echo " clean - Clean build files" - @echo " rebuild - Clean and rebuild" -ifeq ($(PLATFORM),linux) - @echo "" - @echo "Cross-compilation (Linux → Windows):" - @echo " win - Build glamac for Windows" - @echo " win-all - Build everything for Windows" - @echo " setup-cross - Setup cross-compilation" -endif - @echo "" - @echo "Dependencies:" - @echo " deps - Install SDL3 dependencies" - @echo " clean-deps - Remove SDL3 dependencies" - @echo "" - @echo "Glass catalog conversion:" - @echo " convert-catalogs - Convert Excel files to JSON" - @echo "" - @echo "Quick start:" -ifeq ($(PLATFORM),linux) - @echo " make deps && make all # Native build" - @echo " make setup-cross && make win # Windows build" -else - @echo " Install SDL3, then: make all" -endif - -.PHONY: all glamac glautils win win-all win-dlls setup-cross convert-catalogs deps clean clean-deps clean-cache clean-all rebuild help \ No newline at end of file + @$(MAKE) -C build help \ No newline at end of file diff --git a/Makefile.backup b/Makefile.backup new file mode 100644 index 0000000..e2f0c54 --- /dev/null +++ b/Makefile.backup @@ -0,0 +1,247 @@ +# Simplified Makefile for glamac - SDL3 version +# Supports: Linux native, Windows cross-compilation + +# Detect OS and set platform-specific variables +UNAME_S := $(shell uname -s 2>/dev/null || echo Windows_NT) +ifeq ($(UNAME_S),Linux) + PLATFORM := linux + EXE_EXT := + MKDIR := mkdir -p + RM := rm -f + RMDIR := rm -rf + CC := gcc + MINGW_CC := x86_64-w64-mingw32-gcc + SDL3_LIBS := $(shell pkg-config --libs sdl3 SDL3_ttf 2>/dev/null || echo -lSDL3 -lSDL3_ttf) -lm + CROSS_PREFIX := /usr/x86_64-w64-mingw32 +else + PLATFORM := windows + EXE_EXT := .exe + MKDIR := mkdir + RM := del /Q + RMDIR := rmdir /s /q + CC := gcc + SDL3_LIBS := -lSDL3 -lSDL3_ttf -mwindows +endif + +# Directories +SRCDIR := src +BINDIR := bin +BINDIR_WIN := bin/win +INCDIR := include +DLL_CACHE := $(HOME)/.cache/glamac-dlls + +# Base flags +CFLAGS_BASE := -I$(INCDIR) -O2 -flto +# Security and warning flags +SECURITY_FLAGS := -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE +WARNING_FLAGS := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wstack-protector -Wvla + +# Compiler flags +CFLAGS := $(CFLAGS_BASE) $(SECURITY_FLAGS) $(WARNING_FLAGS) +CFLAGS_NATIVE := $(CFLAGS) -march=native +# Windows cross-compilation flags (without stack protector to avoid libssp dependency) +WARNING_FLAGS_WIN := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wvla +CFLAGS_CROSS := $(CFLAGS_BASE) $(WARNING_FLAGS_WIN) -I$(CROSS_PREFIX)/include + +# Source files +GLAMAC_SRCS := $(wildcard $(SRCDIR)/glamac/*.c) +GLAUTILS_SRCS := $(wildcard $(SRCDIR)/glautils/*.c) +GLAUTILS_BINS := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR)/%$(EXE_EXT), $(GLAUTILS_SRCS)) +GLAUTILS_BINS_WIN := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR_WIN)/%.exe, $(GLAUTILS_SRCS)) + +# Glass data dependencies for fgla +GLASS_DATA_SRCS := $(SRCDIR)/glamac/glass_data.c $(SRCDIR)/glamac/glamac_errors.c + +# Default target +all: glamac glautils + +# Native build targets +glamac: $(BINDIR)/glamac$(EXE_EXT) + +glautils: $(GLAUTILS_BINS) + +$(BINDIR)/glamac$(EXE_EXT): $(GLAMAC_SRCS) | $(BINDIR) + @echo "Building glamac..." + $(CC) $^ $(CFLAGS_NATIVE) $(SDL3_LIBS) -o $@ + +# Special rule for fgla which needs glass_data dependencies +$(BINDIR)/fgla$(EXE_EXT): $(SRCDIR)/glautils/fgla.c $(GLASS_DATA_SRCS) | $(BINDIR) + $(CC) $^ $(CFLAGS_NATIVE) -o $@ + +# General rule for other glautils (excluding fgla) +$(BINDIR)/%$(EXE_EXT): $(SRCDIR)/glautils/%.c | $(BINDIR) + $(CC) $< $(CFLAGS_NATIVE) -o $@ + +$(BINDIR): + $(MKDIR) $(BINDIR) + +# Windows cross-compilation (Linux only) +ifeq ($(PLATFORM),linux) + +win: $(BINDIR_WIN)/glamac.exe win-dlls win-data + +win-all: win $(GLAUTILS_BINS_WIN) + +$(BINDIR_WIN)/glamac.exe: $(GLAMAC_SRCS) | $(BINDIR_WIN) + @echo "Cross-compiling glamac for Windows..." + @which $(MINGW_CC) >/dev/null 2>&1 || (echo "ERROR: Install mingw-w64-gcc first" && exit 1) + $(MINGW_CC) $^ $(CFLAGS_CROSS) -L$(CROSS_PREFIX)/lib -lmingw32 -lSDL3 -lSDL3_ttf -mwindows -static-libgcc -o $@ + +$(BINDIR_WIN)/%.exe: $(SRCDIR)/glautils/%.c | $(BINDIR_WIN) + $(MINGW_CC) $< $(CFLAGS_CROSS) -static-libgcc -o $@ + +$(BINDIR_WIN): + $(MKDIR) $(BINDIR_WIN) + +# Windows DLL management +win-dlls: | $(BINDIR_WIN) + @echo "Getting Windows DLLs..." + @$(MKDIR) $(DLL_CACHE) + @if [ ! -f "$(DLL_CACHE)/SDL3.dll" ]; then \ + echo "Downloading SDL3.dll..."; \ + cd $(DLL_CACHE) && \ + wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-3.2.10-win32-x64.zip && \ + unzip -j SDL3-3.2.10-win32-x64.zip SDL3.dll && \ + $(RM) SDL3-3.2.10-win32-x64.zip; \ + fi + @if [ ! -f "$(DLL_CACHE)/SDL3_ttf.dll" ]; then \ + echo "Downloading SDL3_ttf.dll..."; \ + cd $(DLL_CACHE) && \ + wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-3.2.2-win32-x64.zip && \ + unzip -j SDL3_ttf-3.2.2-win32-x64.zip SDL3_ttf.dll && \ + $(RM) SDL3_ttf-3.2.2-win32-x64.zip; \ + fi + @cp $(DLL_CACHE)/*.dll $(BINDIR_WIN)/ + @echo "Windows build ready in $(BINDIR_WIN)/" + +# Copy data files for Windows build +win-data: | $(BINDIR_WIN) + @echo "Copying data files for Windows..." + @$(MKDIR) $(BINDIR_WIN)/data/json + @if [ -f "data/json/glasses.json" ]; then \ + cp data/json/glasses.json $(BINDIR_WIN)/data/json/; \ + cp data/json/glasses.json $(BINDIR_WIN)/; \ + echo "Copied glasses.json to Windows build"; \ + else \ + echo "Warning: glasses.json not found, Windows build may use fallback data"; \ + fi + @echo "Copying font for Windows..." + @if [ -f "/usr/share/fonts/TTF/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/TTF/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + elif [ -f "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + else \ + echo "Warning: DejaVu font not found, Windows build may fail to start"; \ + fi + +# Cross-compilation setup +setup-cross: + @echo "Setting up cross-compilation..." + @which wget >/dev/null 2>&1 || (echo "Install wget first: sudo pacman -S wget" && exit 1) + @which $(MINGW_CC) >/dev/null 2>&1 || (echo "Install mingw-w64-gcc first: sudo pacman -S mingw-w64-gcc" && exit 1) + sudo $(MKDIR) $(CROSS_PREFIX)/include $(CROSS_PREFIX)/lib + @echo "Downloading SDL3 development libraries..." + cd /tmp && \ + wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-devel-3.2.10-mingw.tar.gz && \ + wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ + tar -xzf SDL3-devel-3.2.10-mingw.tar.gz && \ + tar -xzf SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ + sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ + sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ + sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ + sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ + $(RM) -rf SDL3-3.2.10* SDL3_ttf-3.2.2* + @echo "Cross-compilation setup complete!" + +else +# Windows host - disable cross-compilation +win win-all win-dlls setup-cross: + @echo "Cross-compilation not available on Windows. Use 'make all' instead." +endif + +# Excel to JSON conversion +EXCEL_FILES := $(wildcard data/Excel/*.xlsx) +JSON_FILES := $(patsubst data/Excel/%.xlsx, data/JSON/%.json, $(EXCEL_FILES)) + +convert-catalogs: $(JSON_FILES) + +data/JSON/%.json: data/Excel/%.xlsx scripts/excel_to_json.py + @echo "Converting $< to JSON..." + @$(MKDIR) data/JSON + @python3 scripts/excel_to_json.py $< -o $@ + +# Dependency management +deps: +ifeq ($(PLATFORM),linux) + @echo "Installing dependencies..." + sudo pacman -S --needed sdl3 git cmake pkgconf freetype2 python python-pandas python-openpyxl + @echo "Building SDL3_ttf from source..." + cd /tmp && $(RM) -rf SDL_ttf && \ + git clone https://github.com/libsdl-org/SDL_ttf.git && \ + cd SDL_ttf && git checkout release-3.2.2 && \ + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && \ + make -C build -j$$(nproc) && sudo make -C build install && \ + sudo ldconfig + @echo "Dependencies installed!" +else + @echo "Install SDL3 development libraries manually on Windows." + @echo "Install Python with pandas and openpyxl for Excel conversion." +endif + +# Cleanup +clean: + $(RMDIR) $(BINDIR) 2>/dev/null || true + +clean-deps: +ifeq ($(PLATFORM),linux) + @echo "Removing SDL3 dependencies..." + sudo pacman -R sdl3 --noconfirm 2>/dev/null || true + sudo $(RM) -f /usr/lib/libSDL3_ttf.so* /usr/lib/libSDL3_ttf.a /usr/lib/pkgconfig/SDL3_ttf.pc + sudo $(RMDIR) /usr/include/SDL3_ttf/ 2>/dev/null || true + sudo ldconfig + @echo "Dependencies removed!" +endif + +clean-cache: + $(RMDIR) $(DLL_CACHE) 2>/dev/null || true + +clean-all: clean clean-cache + +rebuild: clean all + +# Help +help: + @echo "GlaMaC Build System" + @echo "" + @echo "Build targets:" + @echo " all - Build for current platform" + @echo " glamac - Build main application only" + @echo " glautils - Build utilities only" + @echo " clean - Clean build files" + @echo " rebuild - Clean and rebuild" +ifeq ($(PLATFORM),linux) + @echo "" + @echo "Cross-compilation (Linux → Windows):" + @echo " win - Build glamac for Windows" + @echo " win-all - Build everything for Windows" + @echo " setup-cross - Setup cross-compilation" +endif + @echo "" + @echo "Dependencies:" + @echo " deps - Install SDL3 dependencies" + @echo " clean-deps - Remove SDL3 dependencies" + @echo "" + @echo "Glass catalog conversion:" + @echo " convert-catalogs - Convert Excel files to JSON" + @echo "" + @echo "Quick start:" +ifeq ($(PLATFORM),linux) + @echo " make deps && make all # Native build" + @echo " make setup-cross && make win # Windows build" +else + @echo " Install SDL3, then: make all" +endif + +.PHONY: all glamac glautils win win-all win-dlls setup-cross convert-catalogs deps clean clean-deps clean-cache clean-all rebuild help \ No newline at end of file diff --git a/build/Makefile b/build/Makefile new file mode 100644 index 0000000..d77ff68 --- /dev/null +++ b/build/Makefile @@ -0,0 +1,45 @@ +# Main orchestrator Makefile for GlaMaC +# This file coordinates the modular build system + +# Include common configuration first +include common.mk + +# Include build modules +include native.mk +include cross-compile.mk +include dependencies.mk + +# Help system +help: + @echo "GlaMaC Build System" + @echo "" + @echo "Build targets:" + @echo " all - Build for current platform" + @echo " glamac - Build main application only" + @echo " glautils - Build utilities only" + @echo " clean - Clean build files" + @echo " rebuild - Clean and rebuild" +ifeq ($(PLATFORM),linux) + @echo "" + @echo "Cross-compilation (Linux → Windows):" + @echo " win - Build glamac for Windows" + @echo " win-all - Build everything for Windows" + @echo " setup-cross - Setup cross-compilation" +endif + @echo "" + @echo "Dependencies:" + @echo " deps - Install SDL3 dependencies" + @echo " clean-deps - Remove SDL3 dependencies" + @echo "" + @echo "Glass catalog conversion:" + @echo " convert-catalogs - Convert Excel files to JSON" + @echo "" + @echo "Quick start:" +ifeq ($(PLATFORM),linux) + @echo " make deps && make all # Native build" + @echo " make setup-cross && make win # Windows build" +else + @echo " Install SDL3, then: make all" +endif + +.PHONY: help \ No newline at end of file diff --git a/build/common.mk b/build/common.mk new file mode 100644 index 0000000..d1262c8 --- /dev/null +++ b/build/common.mk @@ -0,0 +1,60 @@ +# Common build variables and functions for GlaMaC +# This file contains shared configuration used by all build modules + +# Detect OS and set platform-specific variables +UNAME_S := $(shell uname -s 2>/dev/null || echo Windows_NT) +ifeq ($(UNAME_S),Linux) + PLATFORM := linux + EXE_EXT := + MKDIR := mkdir -p + RM := rm -f + RMDIR := rm -rf + CC := gcc + MINGW_CC := x86_64-w64-mingw32-gcc + SDL3_LIBS := $(shell pkg-config --libs sdl3 SDL3_ttf 2>/dev/null || echo -lSDL3 -lSDL3_ttf) -lm + CROSS_PREFIX := /usr/x86_64-w64-mingw32 +else + PLATFORM := windows + EXE_EXT := .exe + MKDIR := mkdir + RM := del /Q + RMDIR := rmdir /s /q + CC := gcc + SDL3_LIBS := -lSDL3 -lSDL3_ttf -mwindows +endif + +# Directories (relative to project root) +SRCDIR := ../src +BINDIR := ../bin +BINDIR_WIN := ../bin/win +INCDIR := ../include +DLL_CACHE := $(HOME)/.cache/glamac-dlls + +# Base flags +CFLAGS_BASE := -I$(INCDIR) -O2 -flto +# Security and warning flags +SECURITY_FLAGS := -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE +WARNING_FLAGS := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wstack-protector -Wvla + +# Compiler flags +CFLAGS := $(CFLAGS_BASE) $(SECURITY_FLAGS) $(WARNING_FLAGS) +CFLAGS_NATIVE := $(CFLAGS) -march=native +# Windows cross-compilation flags (without stack protector to avoid libssp dependency) +WARNING_FLAGS_WIN := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wvla +CFLAGS_CROSS := $(CFLAGS_BASE) $(WARNING_FLAGS_WIN) -I$(CROSS_PREFIX)/include + +# Source files +GLAMAC_SRCS := $(wildcard $(SRCDIR)/glamac/*.c) +GLAUTILS_SRCS := $(wildcard $(SRCDIR)/glautils/*.c) +GLAUTILS_BINS := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR)/%$(EXE_EXT), $(GLAUTILS_SRCS)) +GLAUTILS_BINS_WIN := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR_WIN)/%.exe, $(GLAUTILS_SRCS)) + +# Glass data dependencies for fgla +GLASS_DATA_SRCS := $(SRCDIR)/glamac/glass_data.c $(SRCDIR)/glamac/glamac_errors.c + +# Common directory creation rules +$(BINDIR): + $(MKDIR) $(BINDIR) + +$(BINDIR_WIN): + $(MKDIR) $(BINDIR_WIN) \ No newline at end of file diff --git a/build/cross-compile.mk b/build/cross-compile.mk new file mode 100644 index 0000000..5dc65f8 --- /dev/null +++ b/build/cross-compile.mk @@ -0,0 +1,94 @@ +# Cross-compilation rules for GlaMaC +# This file contains rules for Windows cross-compilation from Linux + +# Windows cross-compilation (Linux only) +ifeq ($(PLATFORM),linux) + +win: $(BINDIR_WIN)/glamac.exe win-dlls win-data + +win-all: win $(GLAUTILS_BINS_WIN) + +$(BINDIR_WIN)/glamac.exe: $(GLAMAC_SRCS) | $(BINDIR_WIN) + @echo "Cross-compiling glamac for Windows..." + @which $(MINGW_CC) >/dev/null 2>&1 || (echo "ERROR: Install mingw-w64-gcc first" && exit 1) + $(MINGW_CC) $^ $(CFLAGS_CROSS) -L$(CROSS_PREFIX)/lib -lmingw32 -lSDL3 -lSDL3_ttf -mwindows -static-libgcc -o $@ + +$(BINDIR_WIN)/%.exe: $(SRCDIR)/glautils/%.c | $(BINDIR_WIN) + $(MINGW_CC) $< $(CFLAGS_CROSS) -static-libgcc -o $@ + +# Windows DLL management +win-dlls: | $(BINDIR_WIN) + @echo "Getting Windows DLLs..." + @$(MKDIR) $(DLL_CACHE) + @if [ ! -f "$(DLL_CACHE)/SDL3.dll" ]; then \ + echo "Downloading SDL3.dll..."; \ + cd $(DLL_CACHE) && \ + wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-3.2.10-win32-x64.zip && \ + unzip -j SDL3-3.2.10-win32-x64.zip SDL3.dll && \ + $(RM) SDL3-3.2.10-win32-x64.zip; \ + fi + @if [ ! -f "$(DLL_CACHE)/SDL3_ttf.dll" ]; then \ + echo "Downloading SDL3_ttf.dll..."; \ + cd $(DLL_CACHE) && \ + wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-3.2.2-win32-x64.zip && \ + unzip -j SDL3_ttf-3.2.2-win32-x64.zip SDL3_ttf.dll && \ + $(RM) SDL3_ttf-3.2.2-win32-x64.zip; \ + fi + @cp $(DLL_CACHE)/*.dll $(BINDIR_WIN)/ + @echo "Windows build ready in $(BINDIR_WIN)/" + +# Copy data files for Windows build +win-data: | $(BINDIR_WIN) + @echo "Copying data files for Windows..." + @$(MKDIR) $(BINDIR_WIN)/data/json + @if [ -f "../data/json/glasses.json" ]; then \ + cp ../data/json/glasses.json $(BINDIR_WIN)/data/json/; \ + cp ../data/json/glasses.json $(BINDIR_WIN)/; \ + echo "Copied glasses.json to Windows build"; \ + else \ + echo "Warning: glasses.json not found, Windows build may use fallback data"; \ + fi + @echo "Copying font for Windows..." + @if [ -f "/usr/share/fonts/TTF/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/TTF/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + elif [ -f "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + else \ + echo "Warning: DejaVu font not found, Windows build may fail to start"; \ + fi + +# Cross-compilation setup +setup-cross: + @echo "Setting up cross-compilation..." + @which wget >/dev/null 2>&1 || (echo "Install wget first: sudo pacman -S wget" && exit 1) + @which $(MINGW_CC) >/dev/null 2>&1 || (echo "Install mingw-w64-gcc first: sudo pacman -S mingw-w64-gcc" && exit 1) + sudo $(MKDIR) $(CROSS_PREFIX)/include $(CROSS_PREFIX)/lib + @echo "Downloading SDL3 development libraries..." + cd /tmp && \ + wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-devel-3.2.10-mingw.tar.gz && \ + wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ + tar -xzf SDL3-devel-3.2.10-mingw.tar.gz && \ + tar -xzf SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ + sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ + sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ + sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ + sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ + $(RM) -rf SDL3-3.2.10* SDL3_ttf-3.2.2* + @echo "Cross-compilation setup complete!" + +else +# Windows host - disable cross-compilation +win win-all win-dlls setup-cross: + @echo "Cross-compilation not available on Windows. Use 'make all' instead." +endif + +# Cross-compilation cleanup +clean-cache: + $(RMDIR) $(DLL_CACHE) 2>/dev/null || true + +clean-all: clean clean-cache clean-deps-files + +# Cross-compilation phony targets +.PHONY: win win-all win-dlls setup-cross win-data clean-cache clean-all \ No newline at end of file diff --git a/build/dependencies.mk b/build/dependencies.mk new file mode 100644 index 0000000..fa10af4 --- /dev/null +++ b/build/dependencies.mk @@ -0,0 +1,52 @@ +# Dependency management for GlaMaC +# This file contains rules for installing and managing dependencies + +# Automatic dependency generation for source files +GLAMAC_DEPS := $(GLAMAC_SRCS:.c=.d) +GLAUTILS_DEPS := $(GLAUTILS_SRCS:.c=.d) +ALL_DEPS := $(GLAMAC_DEPS) $(GLAUTILS_DEPS) + +# Include generated dependencies (suppress errors if files don't exist yet) +-include $(ALL_DEPS) + +# Rule to generate dependency files +%.d: %.c + @$(CC) -MM $(CFLAGS) $< | sed 's|\(.*\)\.o[ ]*:|\1.o \1.d:|' > $@ + +# Clean dependency files +clean-deps-files: + $(RM) $(ALL_DEPS) + +# Add dependency files to clean targets +.PHONY: clean-deps-files + +# Dependency management +deps: +ifeq ($(PLATFORM),linux) + @echo "Installing dependencies..." + sudo pacman -S --needed sdl3 git cmake pkgconf freetype2 python python-pandas python-openpyxl + @echo "Building SDL3_ttf from source..." + cd /tmp && $(RM) -rf SDL_ttf && \ + git clone https://github.com/libsdl-org/SDL_ttf.git && \ + cd SDL_ttf && git checkout release-3.2.2 && \ + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && \ + make -C build -j$$(nproc) && sudo make -C build install && \ + sudo ldconfig + @echo "Dependencies installed!" +else + @echo "Install SDL3 development libraries manually on Windows." + @echo "Install Python with pandas and openpyxl for Excel conversion." +endif + +clean-deps: +ifeq ($(PLATFORM),linux) + @echo "Removing SDL3 dependencies..." + sudo pacman -R sdl3 --noconfirm 2>/dev/null || true + sudo $(RM) -f /usr/lib/libSDL3_ttf.so* /usr/lib/libSDL3_ttf.a /usr/lib/pkgconfig/SDL3_ttf.pc + sudo $(RMDIR) /usr/include/SDL3_ttf/ 2>/dev/null || true + sudo ldconfig + @echo "Dependencies removed!" +endif + +# Dependency-related phony targets +.PHONY: deps clean-deps \ No newline at end of file diff --git a/build/native.mk b/build/native.mk new file mode 100644 index 0000000..e4495d5 --- /dev/null +++ b/build/native.mk @@ -0,0 +1,42 @@ +# Native build rules for GlaMaC +# This file contains rules for building on the native platform (Linux/Windows) + +# Default target +all: glamac glautils + +# Native build targets +glamac: $(BINDIR)/glamac$(EXE_EXT) + +glautils: $(GLAUTILS_BINS) + +$(BINDIR)/glamac$(EXE_EXT): $(GLAMAC_SRCS) | $(BINDIR) + @echo "Building glamac..." + $(CC) $^ $(CFLAGS_NATIVE) $(SDL3_LIBS) -o $@ + +# Special rule for fgla which needs glass_data dependencies +$(BINDIR)/fgla$(EXE_EXT): $(SRCDIR)/glautils/fgla.c $(GLASS_DATA_SRCS) | $(BINDIR) + $(CC) $^ $(CFLAGS_NATIVE) -o $@ + +# General rule for other glautils (excluding fgla) +$(BINDIR)/%$(EXE_EXT): $(SRCDIR)/glautils/%.c | $(BINDIR) + $(CC) $< $(CFLAGS_NATIVE) -o $@ + +# Excel to JSON conversion +EXCEL_FILES := $(wildcard ../data/Excel/*.xlsx) +JSON_FILES := $(patsubst ../data/Excel/%.xlsx, ../data/JSON/%.json, $(EXCEL_FILES)) + +convert-catalogs: $(JSON_FILES) + +../data/JSON/%.json: ../data/Excel/%.xlsx ../scripts/excel_to_json.py + @echo "Converting $< to JSON..." + @$(MKDIR) ../data/JSON + @cd .. && python3 scripts/excel_to_json.py $< -o $@ + +# Cleanup +clean: clean-deps-files + $(RMDIR) $(BINDIR) 2>/dev/null || true + +rebuild: clean all + +# Build-related phony targets +.PHONY: all glamac glautils convert-catalogs clean rebuild \ No newline at end of file diff --git a/include/glamac/core/glamac_errors.h b/include/glamac/core/glamac_errors.h new file mode 100644 index 0000000..2065600 --- /dev/null +++ b/include/glamac/core/glamac_errors.h @@ -0,0 +1,38 @@ +/** + * glamac_errors.h - Unified error handling for GlaMaC + * + * 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. + */ +#ifndef GLAMAC_ERRORS_H +#define GLAMAC_ERRORS_H + +#include "glamacdef.h" +#include "security.h" + +// Error codes for GlaMaC operations +typedef enum { + GLAMAC_SUCCESS = 0, + GLAMAC_ERROR_MEMORY = -1, + GLAMAC_ERROR_FILE_NOT_FOUND = -2, + GLAMAC_ERROR_FILE_TOO_LARGE = -3, + GLAMAC_ERROR_INVALID_JSON = -4, + GLAMAC_ERROR_INVALID_PATH = -5, + GLAMAC_ERROR_BUFFER_OVERFLOW = -6, + GLAMAC_ERROR_MANUFACTURER_NOT_FOUND = -7, + GLAMAC_ERROR_NO_GLASSES_FOUND = -8, + GLAMAC_ERROR_INVALID_ARGUMENT = -9, + GLAMAC_ERROR_INVALID_GLASS_DATA = -10, + GLAMAC_ERROR_INVALID_MANUFACTURER = -11 +} GlamacResult; + +// Convert error code to human-readable string +const char* glamac_error_string(GlamacResult error); + +#endif /* GLAMAC_ERRORS_H */ \ No newline at end of file diff --git a/include/glamac/core/glamacdef.h b/include/glamac/core/glamacdef.h new file mode 100644 index 0000000..46cbf82 --- /dev/null +++ b/include/glamac/core/glamacdef.h @@ -0,0 +1,72 @@ +/** + * glamacdef.h - header file containing various definitions for the GlaMaC. + * + * 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. + */ +#ifndef OPDECDEF_H +#define OPDECDEF_H + +#include +#include +#include +#include +/* Type definitions for consistent sizing across platforms. Idea taken from https://nullprogram.com/blog/2023/10/08/ (archive link: ) */ +typedef uint8_t u8; +typedef char16_t c16; +typedef int32_t b32; +typedef int32_t i32; +typedef uint32_t u32; +typedef uint64_t u64; +typedef float f32; +typedef double f64; +typedef uintptr_t uptr; +#ifndef _WIN32 +typedef char byte; +#else +// On Windows, use unsigned char to match Windows headers +typedef unsigned char byte; +#endif +typedef ptrdiff_t size; +typedef size_t usize; +/* Utility macros */ +#define countof(a) (size)(sizeof(a) / sizeof(*(a))) +#define lengthof(s) (countof(s) - 1) + +/* Safe allocation macros with overflow protection */ +static inline void* safe_malloc(size_t element_size, size_t count) { + // Check for integer overflow in multiplication + if (count == 0 || element_size == 0) { + return NULL; + } + if (count > SIZE_MAX / element_size) { + return NULL; // Would overflow + } + return malloc(element_size * count); +} + +static inline void* safe_calloc(size_t element_size, size_t count) { + // Check for integer overflow - calloc handles this internally but be explicit + if (count == 0 || element_size == 0) { + return NULL; + } + if (count > SIZE_MAX / element_size) { + return NULL; // Would overflow + } + return calloc(count, element_size); +} + +// Safe allocation macros - use these instead of malloc directly +#define new(t, n) ((t*)safe_malloc(sizeof(t), (size_t)(n))) +#define new_zero(t, n) ((t*)safe_calloc(sizeof(t), (size_t)(n))) +#define new_arr(t, arr) ((t*)safe_malloc(sizeof(arr), 1)) + +// Legacy unsafe macro - deprecated, use new() or new_zero() instead +// #define old_new(t, n) (t *)malloc(n*sizeof(t)) // DEPRECATED +#endif /* OPDECDEF_H */ diff --git a/include/glamac/core/security.h b/include/glamac/core/security.h new file mode 100644 index 0000000..6b8c811 --- /dev/null +++ b/include/glamac/core/security.h @@ -0,0 +1,41 @@ +/** + * security.h - Centralized security constants and limits for GlaMaC + * + * 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. + */ +#ifndef GLAMAC_SECURITY_H +#define GLAMAC_SECURITY_H + +// File and data size limits +#define MAX_JSON_FILE_SIZE (10 * 1024 * 1024) // 10MB limit for JSON file size +#define MAX_JSON_STRING_LEN 1024 // Max string field length in JSON +#define MAX_PATH_LEN 4096 // Max file path length +#define MAX_MANUFACTURER_NAME_LEN 64 // Max manufacturer name length + +// Glass search utility limits +#define FGLA_MAX_SEARCH_TERM_LEN 256 // Max search term length +#define FGLA_MAX_GLASS_NAME_LEN 256 // Max glass name length +#define FGLA_MAX_CATALOG_COUNT 10 // Max number of catalogs to search +#define FGLA_MAX_NORMALIZED_LEN 512 // Max normalized string length +#define FGLA_GLASS_CODE_LEN 6 // Fixed glass code length + +// Buffer limits for input validation +#define MAX_INPUT_BUFFER_SIZE 8192 // General input buffer limit +#define MAX_COMMAND_LINE_ARGS 32 // Max command line arguments + +// Memory allocation limits +#define MAX_GLASS_ENTRIES 100000 // Max glass entries in database +#define MAX_TEXTURE_CACHE_SIZE 1000 // Max cached textures for rendering + +// Security validation constants +#define MIN_GLASS_CODE_DIGITS 6 // Minimum digits in glass code +#define MAX_GLASS_CODE_DIGITS 6 // Maximum digits in glass code + +#endif /* GLAMAC_SECURITY_H */ \ No newline at end of file diff --git a/include/glamac/data/glass_data.h b/include/glamac/data/glass_data.h new file mode 100644 index 0000000..00f30c0 --- /dev/null +++ b/include/glamac/data/glass_data.h @@ -0,0 +1,56 @@ +/** + * glass_data.h - header file from glass_data.c. + * + * 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. + */ +#ifndef GLASS_DATA_H +#define GLASS_DATA_H + +#include "../core/glamacdef.h" // For type definitions +#include "../core/glamac_errors.h" // For error handling + +// Structure to represent an optical glass +typedef struct { + byte name[50]; + f32 abbeNumber; // X-axis (vd) + f32 refractiveIndex; // Y-axis (nd) + byte glass_code[20]; // Glass code from manufacturer + byte manufacturer[20]; // Manufacturer name +} Glass; + +// Get number of glasses in the catalog +u32 get_glass_count(void); + +// Get glass at index +const Glass* get_glass(u32 index); + +// Get glass name +const byte* get_glass_name(u32 index); + +// Find data range in glass catalog +void find_glass_data_range(f32 *minAbbe, f32 *maxAbbe, f32 *minRI, f32 *maxRI); + +// Initialize glass data - call at program start +void initialize_glass_data(void); + +// Load glasses from JSON file +b32 load_glasses_from_json(const byte* json_path, const byte* manufacturer_filter); + +// Cleanup glass data resources +void cleanup_glass_data(void); + +// Catalog management +u32 get_catalog_count(void); +const char* get_catalog_name(u32 catalog_index); +const char* get_current_catalog_name(void); +void set_current_catalog(u32 catalog_index); +void cycle_catalog(i32 direction); // +1 for next, -1 for previous + +#endif /* GLASS_DATA_H */ diff --git a/include/glamac/graphics/glamac_render.h b/include/glamac/graphics/glamac_render.h new file mode 100644 index 0000000..4c20751 --- /dev/null +++ b/include/glamac/graphics/glamac_render.h @@ -0,0 +1,50 @@ +/** + * glamac_render.h - header file from glamac_render.c. + */ +#ifndef GLAMAC_RENDER_H +#define GLAMAC_RENDER_H + +#include +#include +#include "../core/glamacdef.h" +#include "glamac_view.h" + +// Drawing primitives +void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color); +void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 radius); + +// UI element rendering +void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); +void draw_grid(SDL_Renderer *renderer, const ViewState* view); +void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view); +void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); +void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); + +// Main render function +void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, ViewState* view); + +// Font management +typedef struct { + TTF_Font *regular; + TTF_Font *title; + TTF_Font *label; +} FontSet; + +// Load all required fonts +b32 load_fonts(FontSet *fonts); + +// Load fonts with DPI-aware sizing +b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi); + +// Free all fonts +void free_fonts(FontSet *fonts); +void clear_text_cache(void); + +// Simple glass property display helper +void calculate_smart_window_position(i32 glassX, i32 glassY, i32 windowWidth, i32 windowHeight, + const ViewState* view, i32* windowX, i32* windowY); + +// Debug mode check +b32 is_debug_mode(void); + +#endif /* GLAMAC_RENDER_H */ diff --git a/include/glamac/graphics/glamac_view.h b/include/glamac/graphics/glamac_view.h new file mode 100644 index 0000000..58de2b1 --- /dev/null +++ b/include/glamac/graphics/glamac_view.h @@ -0,0 +1,109 @@ +/** + * glamac_view.h - header file from glamac_view.c. + */ +#ifndef GLAMAC_VIEW_H +#define GLAMAC_VIEW_H + +#include +#include "../core/glamacdef.h" + +// Constants for view +#define MIN_PADDING 20 // Minimum padding in pixels +#define MAX_PADDING_PERCENT 0.04f // Maximum padding as percentage of window size +#define PAN_STEP 0.05f // Step size for keyboard panning +#define ZOOM_FACTOR 1.2f // Zoom factor for zoom operations +#define MIN_ZOOM 0.5f // Minimum zoom level +#define MAX_ZOOM 10.0f // Maximum zoom level + +// Glass label positioning constants (adjustable parameters) +#define LABEL_OFFSET_X 12 // Horizontal offset from glass point (pixels) +#define LABEL_OFFSET_Y -8 // Vertical offset from glass point (pixels) +#define LABEL_SIZE_SCALE 1.4f // Scale factor for glass name labels + + + +// State for zooming and panning +typedef struct { + f32 zoomLevel; + f32 offsetX; + f32 offsetY; + i32 windowWidth; + i32 windowHeight; + f32 minAbbe; + f32 maxAbbe; + f32 minRI; + f32 maxRI; + b32 showHelp; // Flag to show/hide help window + b32 gKeyPressed; // Flag to track if 'g' was pressed + u32 gKeyTime; // Time when 'g' was pressed for sequence timing + i32 selectedGlass; // Index of selected glass (-1 if none) +} ViewState; + +// Initialize a view state with default values +void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight); + +// Refresh view state data range when catalog changes +void refresh_view_data_range(ViewState* view); + +// Helper function to calculate adaptive padding +static inline i32 get_adaptive_padding(const ViewState* view) { + i32 padding = (i32)(view->windowWidth * MAX_PADDING_PERCENT); + return padding > MIN_PADDING ? padding : MIN_PADDING; +} + +// Convert glass data to screen coordinates with zoom and offset +static inline void data_to_screen_coords(f32 abbeNumber, f32 refractiveIndex, + const ViewState* view, i32 *x, i32 *y) { + const i32 padding = get_adaptive_padding(view); + + // Apply zoom and offset transformation + // FLIPPED: Use 1.0f - normalized to flip the Abbe number axis + f32 normalizedX = 1.0f - (abbeNumber - view->minAbbe) / (view->maxAbbe - view->minAbbe); + f32 normalizedY = (refractiveIndex - view->minRI) / (view->maxRI - view->minRI); + + // Transform with zoom and offset + normalizedX = (normalizedX - 0.5f) * view->zoomLevel + 0.5f + view->offsetX; + normalizedY = (normalizedY - 0.5f) * view->zoomLevel + 0.5f + view->offsetY; + + // Convert to screen coordinates + *x = padding + (i32)(normalizedX * (view->windowWidth - 2 * padding)); + *y = view->windowHeight - padding - (i32)(normalizedY * (view->windowHeight - 2 * padding)); +} + +// Convert screen coordinates to data values +static inline void screen_to_data_coords(i32 x, i32 y, const ViewState* view, + f32 *abbeNumber, f32 *refractiveIndex) { + const i32 padding = get_adaptive_padding(view); + + // Convert to normalized coordinates + f32 normalizedX = (f32)(x - padding) / (view->windowWidth - 2 * padding); + f32 normalizedY = (f32)(view->windowHeight - y - padding) / (view->windowHeight - 2 * padding); + + // Reverse transform with zoom and offset + normalizedX = (normalizedX - view->offsetX - 0.5f) / view->zoomLevel + 0.5f; + normalizedY = (normalizedY - view->offsetY - 0.5f) / view->zoomLevel + 0.5f; + + // Convert to data values - FLIPPED axis logic for Abbe + *abbeNumber = view->maxAbbe - normalizedX * (view->maxAbbe - view->minAbbe); + *refractiveIndex = view->minRI + normalizedY * (view->maxRI - view->minRI); +} + +// Find the nearest glass to a given screen position +i32 find_nearest_glass(i32 x, i32 y, const ViewState* view, f32 maxDistance); + +// Calculate visible data range based on current view +void get_visible_data_range(const ViewState* view, f32 *visibleMinAbbe, f32 *visibleMaxAbbe, + f32 *visibleMinRI, f32 *visibleMaxRI); + +// Handle mouse wheel zoom +void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view); + +// Toggle fullscreen +void toggle_fullscreen(SDL_Window* window); + +// Reset view to default +void reset_view(ViewState* view); + + + +#endif /* GLAMAC_VIEW_H */ diff --git a/include/glamac/input/glamac_events.h b/include/glamac/input/glamac_events.h new file mode 100644 index 0000000..f06a703 --- /dev/null +++ b/include/glamac/input/glamac_events.h @@ -0,0 +1,27 @@ +/** + * glamac_events.h - header file from glamac_events.c. + */ +#ifndef GLAMAC_EVENTS_H +#define GLAMAC_EVENTS_H + +#include +#include "../core/glamacdef.h" +#include "../graphics/glamac_view.h" + +// Process key event +b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *window, b32 *quit); + +// Process mouse button event +b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging); + +// Process mouse motion event +b32 process_mouse_motion(SDL_MouseMotionEvent *motion, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 dragging); + +// Process window event +b32 process_window_event(SDL_WindowEvent *window_event, ViewState *view); + +// Process all events on queue +b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, + i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit); + +#endif /* GLAMAC_EVENTS_H */ diff --git a/include/glamac_errors.h b/include/glamac_errors.h deleted file mode 100644 index 4e7b9d0..0000000 --- a/include/glamac_errors.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * glamac_errors.h - Unified error handling for GlaMaC - * - * 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. - */ -#ifndef GLAMAC_ERRORS_H -#define GLAMAC_ERRORS_H - -#include "glamacdef.h" - -// Error codes for GlaMaC operations -typedef enum { - GLAMAC_SUCCESS = 0, - GLAMAC_ERROR_MEMORY = -1, - GLAMAC_ERROR_FILE_NOT_FOUND = -2, - GLAMAC_ERROR_FILE_TOO_LARGE = -3, - GLAMAC_ERROR_INVALID_JSON = -4, - GLAMAC_ERROR_INVALID_PATH = -5, - GLAMAC_ERROR_BUFFER_OVERFLOW = -6, - GLAMAC_ERROR_MANUFACTURER_NOT_FOUND = -7, - GLAMAC_ERROR_NO_GLASSES_FOUND = -8, - GLAMAC_ERROR_INVALID_ARGUMENT = -9 -} GlamacResult; - -// Convert error code to human-readable string -const char* glamac_error_string(GlamacResult error); - -// Security limits -#define MAX_JSON_FILE_SIZE (10 * 1024 * 1024) // 10MB limit -#define MAX_JSON_STRING_LEN 1024 // Max string field length -#define MAX_PATH_LEN 4096 // Max file path length -#define MAX_MANUFACTURER_NAME_LEN 64 // Max manufacturer name length - -#endif /* GLAMAC_ERRORS_H */ \ No newline at end of file diff --git a/include/glamac_events.h b/include/glamac_events.h deleted file mode 100644 index c4145c0..0000000 --- a/include/glamac_events.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * glamac_events.h - header file from glamac_events.c. - */ -#ifndef GLAMAC_EVENTS_H -#define GLAMAC_EVENTS_H - -#include -#include "glamacdef.h" -#include "glamac_view.h" - -// Process key event -b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *window, b32 *quit); - -// Process mouse button event -b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging); - -// Process mouse motion event -b32 process_mouse_motion(SDL_MouseMotionEvent *motion, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 dragging); - -// Process window event -b32 process_window_event(SDL_WindowEvent *window_event, ViewState *view); - -// Process all events on queue -b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, - i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit); - -#endif /* GLAMAC_EVENTS_H */ diff --git a/include/glamac_render.h b/include/glamac_render.h deleted file mode 100644 index 899702d..0000000 --- a/include/glamac_render.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * glamac_render.h - header file from glamac_render.c. - */ -#ifndef GLAMAC_RENDER_H -#define GLAMAC_RENDER_H - -#include -#include -#include "glamacdef.h" -#include "glamac_view.h" - -// Drawing primitives -void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color); -void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 radius); - -// UI element rendering -void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); -void draw_grid(SDL_Renderer *renderer, const ViewState* view); -void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view); -void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); -void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); - -// Main render function -void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, ViewState* view); - -// Font management -typedef struct { - TTF_Font *regular; - TTF_Font *title; - TTF_Font *label; -} FontSet; - -// Load all required fonts -b32 load_fonts(FontSet *fonts); - -// Load fonts with DPI-aware sizing -b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi); - -// Free all fonts -void free_fonts(FontSet *fonts); -void clear_text_cache(void); - -// Simple glass property display helper -void calculate_smart_window_position(i32 glassX, i32 glassY, i32 windowWidth, i32 windowHeight, - const ViewState* view, i32* windowX, i32* windowY); - -// Debug mode check -b32 is_debug_mode(void); - -#endif /* GLAMAC_RENDER_H */ diff --git a/include/glamac_view.h b/include/glamac_view.h deleted file mode 100644 index 6ca4b9b..0000000 --- a/include/glamac_view.h +++ /dev/null @@ -1,175 +0,0 @@ -/** - * glamac_view.h - header file from glamac_view.c. - */ -#ifndef GLAMAC_VIEW_H -#define GLAMAC_VIEW_H - -#include -#include "glamacdef.h" - -// Constants for view -#define MIN_PADDING 20 // Minimum padding in pixels -#define MAX_PADDING_PERCENT 0.04f // Maximum padding as percentage of window size -#define PAN_STEP 0.05f // Step size for keyboard panning -#define ZOOM_FACTOR 1.2f // Zoom factor for zoom operations -#define MIN_ZOOM 0.5f // Minimum zoom level -#define MAX_ZOOM 10.0f // Maximum zoom level - -// Glass label positioning constants (adjustable parameters) -#define LABEL_OFFSET_X 12 // Horizontal offset from glass point (pixels) -#define LABEL_OFFSET_Y -8 // Vertical offset from glass point (pixels) -#define LABEL_SIZE_SCALE 1.4f // Scale factor for glass name labels - -// Tight clustering parameters -#define MAX_CLUSTER_SIZE 8 // Maximum glasses per cluster -#define DEFAULT_TIGHT_CLUSTER_ND_THRESHOLD 0.0005f // Default nd difference threshold -#define DEFAULT_TIGHT_CLUSTER_VD_THRESHOLD 0.15f // Default vd difference threshold - -// Loose clustering parameters (zoom-dependent) -#define DEFAULT_LOOSE_CLUSTER_ND_THRESHOLD 0.3f // Default nd base threshold -#define DEFAULT_LOOSE_CLUSTER_VD_THRESHOLD 0.55f // Default vd base threshold -#define DEFAULT_LOOSE_CLUSTER_ND_FRACTION 4.9f // Default nd fraction of visible range -#define DEFAULT_LOOSE_CLUSTER_VD_FRACTION 0.9f // Default vd fraction of visible range - -// Global tight cluster thresholds (adjustable parameters) -extern f32 g_tight_cluster_nd_threshold; -extern f32 g_tight_cluster_vd_threshold; - -// Global loose cluster parameters (adjustable parameters) -extern f32 g_loose_cluster_nd_threshold; // Base threshold -extern f32 g_loose_cluster_vd_threshold; // Base threshold -extern f32 g_loose_cluster_nd_fraction; // Zoom scaling fraction -extern f32 g_loose_cluster_vd_fraction; // Zoom scaling fraction - -// Tight cluster structure -typedef struct { - i32 glassIndices[MAX_CLUSTER_SIZE]; // Indices of glasses in this cluster - i32 count; // Number of glasses in cluster - i32 representativeIndex; // Index of glass with shortest name - f32 avgAbbeNumber; // Average position for reference - f32 avgRefractiveIndex; -} TightCluster; - -// Loose cluster structure (zoom-dependent) -typedef struct { - i32 glassIndices[MAX_CLUSTER_SIZE]; // Indices of glasses in this cluster - i32 count; // Number of glasses in cluster - i32 representativeIndex; // Index of glass with shortest name - f32 avgAbbeNumber; // Average position for reference - f32 avgRefractiveIndex; -} LooseCluster; - - -// State for zooming and panning -typedef struct { - f32 zoomLevel; - f32 offsetX; - f32 offsetY; - i32 windowWidth; - i32 windowHeight; - f32 minAbbe; - f32 maxAbbe; - f32 minRI; - f32 maxRI; - b32 showHelp; // Flag to show/hide help window - b32 gKeyPressed; // Flag to track if 'g' was pressed - u32 gKeyTime; // Time when 'g' was pressed for sequence timing - i32 selectedGlass; // Index of selected glass (-1 if none) - i32 selectedCluster; // Index of selected tight cluster (-1 if none) - - // Tight cluster data (per catalog) - TightCluster* tightClusters; // Array of tight clusters for current catalog - i32 tightClusterCount; // Number of tight clusters - - // Loose cluster data (per catalog, zoom-dependent) - LooseCluster* looseClusters; // Array of loose clusters for current catalog - i32 looseClusterCount; // Number of loose clusters - - // Label recalculation tracking - f32 lastZoomLevel; // Last zoom level for which labels were calculated - i32 lastWindowWidth; // Last window width for labels - i32 lastWindowHeight; // Last window height for labels - f32 lastOffsetX; // Last X offset for labels - f32 lastOffsetY; // Last Y offset for labels -} ViewState; - -// Initialize a view state with default values -void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight); - -// Refresh view state data range when catalog changes -void refresh_view_data_range(ViewState* view); - -// Helper function to calculate adaptive padding -static inline i32 get_adaptive_padding(const ViewState* view) { - i32 padding = (i32)(view->windowWidth * MAX_PADDING_PERCENT); - return padding > MIN_PADDING ? padding : MIN_PADDING; -} - -// Convert glass data to screen coordinates with zoom and offset -static inline void data_to_screen_coords(f32 abbeNumber, f32 refractiveIndex, - const ViewState* view, i32 *x, i32 *y) { - const i32 padding = get_adaptive_padding(view); - - // Apply zoom and offset transformation - // FLIPPED: Use 1.0f - normalized to flip the Abbe number axis - f32 normalizedX = 1.0f - (abbeNumber - view->minAbbe) / (view->maxAbbe - view->minAbbe); - f32 normalizedY = (refractiveIndex - view->minRI) / (view->maxRI - view->minRI); - - // Transform with zoom and offset - normalizedX = (normalizedX - 0.5f) * view->zoomLevel + 0.5f + view->offsetX; - normalizedY = (normalizedY - 0.5f) * view->zoomLevel + 0.5f + view->offsetY; - - // Convert to screen coordinates - *x = padding + (i32)(normalizedX * (view->windowWidth - 2 * padding)); - *y = view->windowHeight - padding - (i32)(normalizedY * (view->windowHeight - 2 * padding)); -} - -// Convert screen coordinates to data values -static inline void screen_to_data_coords(i32 x, i32 y, const ViewState* view, - f32 *abbeNumber, f32 *refractiveIndex) { - const i32 padding = get_adaptive_padding(view); - - // Convert to normalized coordinates - f32 normalizedX = (f32)(x - padding) / (view->windowWidth - 2 * padding); - f32 normalizedY = (f32)(view->windowHeight - y - padding) / (view->windowHeight - 2 * padding); - - // Reverse transform with zoom and offset - normalizedX = (normalizedX - view->offsetX - 0.5f) / view->zoomLevel + 0.5f; - normalizedY = (normalizedY - view->offsetY - 0.5f) / view->zoomLevel + 0.5f; - - // Convert to data values - FLIPPED axis logic for Abbe - *abbeNumber = view->maxAbbe - normalizedX * (view->maxAbbe - view->minAbbe); - *refractiveIndex = view->minRI + normalizedY * (view->maxRI - view->minRI); -} - -// Find the nearest glass to a given screen position -i32 find_nearest_glass(i32 x, i32 y, const ViewState* view, f32 maxDistance); - -// Calculate visible data range based on current view -void get_visible_data_range(const ViewState* view, f32 *visibleMinAbbe, f32 *visibleMaxAbbe, - f32 *visibleMinRI, f32 *visibleMaxRI); - -// Handle mouse wheel zoom -void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view); - -// Toggle fullscreen -void toggle_fullscreen(SDL_Window* window); - -// Reset view to default -void reset_view(ViewState* view); - -// Tight clustering functions -void create_tight_clusters(ViewState* view); -void free_tight_clusters(ViewState* view); -i32 find_tight_cluster_for_glass(i32 glassIndex, const ViewState* view); - -// Loose clustering functions (zoom-dependent) -void create_loose_clusters(ViewState* view); -void free_loose_clusters(ViewState* view); -i32 find_loose_cluster_for_glass(i32 glassIndex, const ViewState* view); - -// Combined clustering logic -b32 should_show_glass_label(i32 glassIndex, const ViewState* view); - - -#endif /* GLAMAC_VIEW_H */ diff --git a/include/glamacdef.h b/include/glamacdef.h deleted file mode 100644 index aeba926..0000000 --- a/include/glamacdef.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * glamacdef.h - header file containing various definitions for the GlaMaC. - * - * 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. - */ -#ifndef OPDECDEF_H -#define OPDECDEF_H - -#include -#include -#include -/* Type definitions for consistent sizing across platforms. Idea taken from https://nullprogram.com/blog/2023/10/08/ (archive link: ) */ -typedef uint8_t u8; -typedef char16_t c16; -typedef int32_t b32; -typedef int32_t i32; -typedef uint32_t u32; -typedef uint64_t u64; -typedef float f32; -typedef double f64; -typedef uintptr_t uptr; -#ifndef _WIN32 -typedef char byte; -#else -// On Windows, use unsigned char to match Windows headers -typedef unsigned char byte; -#endif -typedef ptrdiff_t size; -typedef size_t usize; -/* Utility macros */ -#define countof(a) (size)(sizeof(a) / sizeof(*(a))) -#define lengthof(s) (countof(s) - 1) -// #define new(a, t, n) (t *)alloc(a, sizeof(t), _Alignof(t), n) //From Nullprogram, not using currently -#define new(t, n) (t *)malloc(n*sizeof(t)) -#define new_arr(t,arr) (t)malloc(sizeof(arr)) -#endif /* OPDECDEF_H */ diff --git a/include/glass_data.h b/include/glass_data.h deleted file mode 100644 index 56c2c7f..0000000 --- a/include/glass_data.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * glass_data.h - header file from glass_data.c. - * - * 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. - */ -#ifndef GLASS_DATA_H -#define GLASS_DATA_H - -#include "glamacdef.h" // For type definitions -#include "glamac_errors.h" // For error handling - -// Structure to represent an optical glass -typedef struct { - byte name[50]; - f32 abbeNumber; // X-axis (vd) - f32 refractiveIndex; // Y-axis (nd) - byte glass_code[20]; // Glass code from manufacturer - byte manufacturer[20]; // Manufacturer name -} Glass; - -// Get number of glasses in the catalog -u32 get_glass_count(void); - -// Get glass at index -const Glass* get_glass(u32 index); - -// Get glass name -const byte* get_glass_name(u32 index); - -// Find data range in glass catalog -void find_glass_data_range(f32 *minAbbe, f32 *maxAbbe, f32 *minRI, f32 *maxRI); - -// Initialize glass data - call at program start -void initialize_glass_data(void); - -// Load glasses from JSON file -b32 load_glasses_from_json(const byte* json_path, const byte* manufacturer_filter); - -// Cleanup glass data resources -void cleanup_glass_data(void); - -// Catalog management -u32 get_catalog_count(void); -const char* get_catalog_name(u32 catalog_index); -const char* get_current_catalog_name(void); -void set_current_catalog(u32 catalog_index); -void cycle_catalog(i32 direction); // +1 for next, -1 for previous - -#endif /* GLASS_DATA_H */ diff --git a/include/glautils/fgla.h b/include/glautils/fgla.h new file mode 100644 index 0000000..d9a946b --- /dev/null +++ b/include/glautils/fgla.h @@ -0,0 +1,113 @@ +/** + * fgla.h - Find Glass utility header + * + * 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. + */ + +#ifndef FGLA_H +#define FGLA_H + +#include "glamac/core/glamacdef.h" +#include "glamac/core/security.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Output format types +typedef enum { + FGLA_OUTPUT_CSV = 0, + FGLA_OUTPUT_TABLE = 1, + FGLA_OUTPUT_JSON = 2 +} FglaOutputFormat; + +// Error handling +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; + +/** + * @brief Safely converts string to lowercase with length limit + * @param str String to convert (modified in place) + * @param max_len Maximum length to process + */ +void fgla_to_lowercase_safe(char* str, size_t max_len); + +/** + * @brief Safely normalizes string by removing dashes and converting to lowercase + * @param input Input string to normalize + * @param output Buffer for normalized output + * @param output_size Size of output buffer + * @return 0 on success, -1 on error + */ +int fgla_normalize_string_safe(const char* input, char* output, size_t output_size); + +/** + * @brief Safely checks if needle is found in haystack (case-insensitive, dash-insensitive) + * @param haystack String to search in + * @param needle String to search for + * @return 1 if found, 0 if not found or error + */ +int fgla_contains_substring_safe(const char* haystack, const char* needle); + +/** + * @brief Checks if manufacturer matches any of the specified catalogs + * @param manufacturer Manufacturer name to check + * @param catalog_list Array of catalog names to match against + * @param catalog_count Number of catalogs in the list + * @return 1 if matches, 0 if no match + */ +int fgla_matches_catalog(const char* manufacturer, const char* catalog_list[], int catalog_count); + +/** + * @brief Validates search term input for security and format + * @param term Search term to validate + * @return 1 if valid, 0 if invalid + */ +int fgla_validate_search_term(const char* term); + +/** + * @brief Checks if a search term is a glass code pattern + * @param term Search term to check + * @return 1 if glass code pattern, 0 otherwise + */ +int fgla_is_glass_code_pattern(const char* term); + +/** + * @brief Safely checks if a glass code matches a pattern + * @param glass_code Glass code to check + * @param pattern Pattern to match (can contain 'x' wildcards) + * @return 1 if matches, 0 if no match + */ +int fgla_matches_glass_code_pattern_safe(const char* glass_code, const char* pattern); + +/** + * @brief Prints usage information for fgla utility + * @param program_name Name of the program (argv[0]) + */ +void fgla_print_usage(const char* program_name); + +/** + * @brief Prints error message with helpful suggestions + * @param error Error code from FglaResult enum + * @param context Optional context string for the error + */ +void fgla_print_error_with_suggestion(FglaResult error, const char* context); + +#ifdef __cplusplus +} +#endif + +#endif /* FGLA_H */ \ No newline at end of file diff --git a/src/glamac/glamac.c b/src/glamac/glamac.c index dba2fa1..dd06626 100644 --- a/src/glamac/glamac.c +++ b/src/glamac/glamac.c @@ -5,39 +5,121 @@ #include #include #include -#include "glamacdef.h" // Type definitions -#include "glass_data.h" // Glass catalog -#include "glamac_view.h" // View management -#include "glamac_render.h" // Rendering +#include "glamac/core/glamacdef.h" // Type definitions +#include "glamac/data/glass_data.h" // Glass catalog +#include "glamac/graphics/glamac_view.h" // View management +#include "glamac/graphics/glamac_render.h" // Rendering // External function declarations from glamac_events.c extern b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit); -// Function to reload fonts on window resize +// Function to reload fonts on window resize with improved error handling and hysteresis b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { static i32 lastWidth = 0; static i32 lastHeight = 0; + static u32 lastReloadTime = 0; + static i32 pendingWidth = 0; + static i32 pendingHeight = 0; - // Check if window size changed significantly (more than 50% change) - if (abs(view->windowWidth - lastWidth) > view->windowWidth * 0.5 || - abs(view->windowHeight - lastHeight) > view->windowHeight * 0.5) { - - // Clear text cache first - clear_text_cache(); - - // Free existing fonts - free_fonts(fonts); + // Validate inputs + if (!fonts || !view) { + printf("Error: Invalid parameters for font reload\n"); + return 0; + } + + // Validate window dimensions + if (view->windowWidth <= 0 || view->windowHeight <= 0) { + printf("Error: Invalid window dimensions for font reload: %dx%d\n", + view->windowWidth, view->windowHeight); + return 0; + } + + // Hysteresis parameters + const f32 threshold = 0.25f; // 25% change threshold + const u32 minReloadInterval = 500; // Minimum 500ms between font reloads + const u32 maxPendingTime = 2000; // Maximum time to wait for pending reload + + u32 currentTime = SDL_GetTicks(); + + // Check if window size changed significantly + i32 widthDiff = abs(view->windowWidth - lastWidth); + i32 heightDiff = abs(view->windowHeight - lastHeight); + b32 significantChange = (widthDiff > (i32)(view->windowWidth * threshold) || + heightDiff > (i32)(view->windowHeight * threshold)); + + if (significantChange) { + // Update pending dimensions + pendingWidth = view->windowWidth; + pendingHeight = view->windowHeight; - // Reload with new size - if (!load_adaptive_fonts(fonts, view->windowWidth, view->windowHeight, dpi)) { - printf("Failed to reload fonts after window resize!\n"); - return 0; + // Check if enough time has passed since last reload + if (currentTime - lastReloadTime >= minReloadInterval) { + printf("Window size changed significantly: %dx%d -> %dx%d, reloading fonts...\n", + lastWidth, lastHeight, view->windowWidth, view->windowHeight); + + // Clear text cache first + clear_text_cache(); + + // Store current fonts as backup + FontSet backup_fonts = *fonts; + + // Try to reload with new size + if (!load_adaptive_fonts(fonts, view->windowWidth, view->windowHeight, dpi)) { + printf("Error: Failed to reload fonts after window resize, restoring backup\n"); + + // Restore backup fonts + *fonts = backup_fonts; + + // If backup is also invalid, try loading basic fonts + if (!fonts->regular || !fonts->title || !fonts->label) { + printf("Warning: Backup fonts invalid, attempting basic font loading\n"); + if (!load_fonts(fonts)) { + printf("Critical error: All font loading attempts failed\n"); + return 0; + } + } + + // Don't update lastWidth/lastHeight on failure to trigger retry later + return 1; // Continue running with backup fonts + } + + // Free backup fonts if new loading succeeded + if (backup_fonts.regular != fonts->regular && backup_fonts.regular) { + TTF_CloseFont(backup_fonts.regular); + } + if (backup_fonts.title != fonts->title && backup_fonts.title) { + TTF_CloseFont(backup_fonts.title); + } + if (backup_fonts.label != fonts->label && backup_fonts.label) { + TTF_CloseFont(backup_fonts.label); + } + + // Update state after successful reload + lastWidth = view->windowWidth; + lastHeight = view->windowHeight; + lastReloadTime = currentTime; + pendingWidth = pendingHeight = 0; // Clear pending state + + printf("Fonts successfully reloaded for window size: %dx%d\n", + view->windowWidth, view->windowHeight); + } else { + // Can't reload yet due to rate limiting, check if we should force it + if (pendingWidth != 0 && pendingHeight != 0 && + currentTime - lastReloadTime >= maxPendingTime) { + printf("Forcing delayed font reload after %ums\n", maxPendingTime); + // Recursive call will now pass the time check + return reload_fonts_if_needed(fonts, view, dpi); + } + printf("Font reload rate limited, pending: %dx%d\n", pendingWidth, pendingHeight); } - - lastWidth = view->windowWidth; - lastHeight = view->windowHeight; - printf("Fonts reloaded for new window size: %dx%d\n", view->windowWidth, view->windowHeight); + } else if (pendingWidth != 0 && pendingHeight != 0 && + currentTime - lastReloadTime >= minReloadInterval) { + // Handle pending resize that wasn't significant enough initially + // but has been waiting for the rate limit + view->windowWidth = pendingWidth; + view->windowHeight = pendingHeight; + return reload_fonts_if_needed(fonts, view, dpi); } return 1; @@ -136,11 +218,7 @@ int main(int argc, char* argv[]) { printf("Loaded %u glasses from %u catalogs\n", get_glass_count(), get_catalog_count()); - // Create tight clusters for the initial catalog - create_tight_clusters(&view); - // Create loose clusters for the initial catalog - create_loose_clusters(&view); // Main loop flag @@ -165,7 +243,13 @@ int main(int argc, char* argv[]) { // Reload fonts if window size changed significantly if (!reload_fonts_if_needed(&fonts, &view, dpi)) { - quit = 1; // Exit if font reloading fails + printf("Warning: Font reloading failed, continuing with current fonts\n"); + // Don't quit immediately - try to continue with current fonts + // Only quit if fonts are completely invalid + if (!fonts.regular || !fonts.title || !fonts.label) { + printf("Critical error: No valid fonts available, exiting\n"); + quit = 1; + } } // Render everything @@ -176,8 +260,6 @@ int main(int argc, char* argv[]) { SDL_StopTextInput(window); // Clean up resources - free_tight_clusters(&view); - free_loose_clusters(&view); cleanup_glass_data(); free_fonts(&fonts); clear_text_cache(); diff --git a/src/glamac/glamac.d b/src/glamac/glamac.d new file mode 100644 index 0000000..596c63f --- /dev/null +++ b/src/glamac/glamac.d @@ -0,0 +1,10 @@ +glamac.o glamac.d: ../src/glamac/glamac.c ../include/glamac/core/glamacdef.h \ + ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h \ + ../include/glamac/graphics/glamac_view.h \ + ../include/glamac/graphics/../core/glamacdef.h \ + ../include/glamac/graphics/glamac_render.h \ + ../include/glamac/graphics/glamac_view.h diff --git a/src/glamac/glamac_errors.c b/src/glamac/glamac_errors.c index d32d369..854404d 100644 --- a/src/glamac/glamac_errors.c +++ b/src/glamac/glamac_errors.c @@ -1,7 +1,7 @@ /** * glamac_errors.c - Unified error handling implementation */ -#include "glamac_errors.h" +#include "glamac/core/glamac_errors.h" const char* glamac_error_string(GlamacResult error) { switch (error) { @@ -25,6 +25,10 @@ const char* glamac_error_string(GlamacResult error) { return "No glasses found for manufacturer"; case GLAMAC_ERROR_INVALID_ARGUMENT: return "Invalid argument provided"; + case GLAMAC_ERROR_INVALID_GLASS_DATA: + return "Invalid glass data - failed validation checks"; + case GLAMAC_ERROR_INVALID_MANUFACTURER: + return "Invalid manufacturer name or data"; default: return "Unknown error"; } diff --git a/src/glamac/glamac_errors.d b/src/glamac/glamac_errors.d new file mode 100644 index 0000000..04a5576 --- /dev/null +++ b/src/glamac/glamac_errors.d @@ -0,0 +1,3 @@ +glamac_errors.o glamac_errors.d: ../src/glamac/glamac_errors.c \ + ../include/glamac/core/glamac_errors.h \ + ../include/glamac/core/glamacdef.h ../include/glamac/core/security.h diff --git a/src/glamac/glamac_events.c b/src/glamac/glamac_events.c index 51d1e9d..5966efa 100644 --- a/src/glamac/glamac_events.c +++ b/src/glamac/glamac_events.c @@ -4,9 +4,9 @@ #include #include #include -#include "glamac_view.h" -#include "glass_data.h" -#include "glamac_render.h" // For clear_text_cache +#include "glamac/graphics/glamac_view.h" +#include "glamac/data/glass_data.h" +#include "glamac/graphics/glamac_render.h" // For clear_text_cache // External debug mode function extern b32 is_debug_mode(void); @@ -35,10 +35,9 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo break; case SDLK_ESCAPE: - // ESC closes glass/cluster selection first, then help window, finally quits - if (view->selectedGlass >= 0 || view->selectedCluster >= 0) { + // ESC closes glass selection first, then help window, finally quits + if (view->selectedGlass >= 0) { view->selectedGlass = -1; // Clear selection - view->selectedCluster = -1; } else if (view->showHelp) { view->showHelp = 0; } else { @@ -62,14 +61,12 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo // Zoom in at center view->zoomLevel *= ZOOM_FACTOR; if (view->zoomLevel > MAX_ZOOM) view->zoomLevel = MAX_ZOOM; - create_loose_clusters(view); // Recreate loose clusters on zoom return 1; case SDLK_MINUS: // Zoom out at center view->zoomLevel /= ZOOM_FACTOR; if (view->zoomLevel < MIN_ZOOM) view->zoomLevel = MIN_ZOOM; - create_loose_clusters(view); // Recreate loose clusters on zoom view->gKeyPressed = 0; // Reset g key state return 1; @@ -92,10 +89,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo cycle_catalog(-1); refresh_view_data_range(view); // Update view for new catalog view->selectedGlass = -1; // Clear selection - view->selectedCluster = -1; // Clear cluster selection clear_text_cache(); // Clear text cache when switching catalogs - create_tight_clusters(view); // Recreate tight clusters for new catalog - create_loose_clusters(view); // Recreate loose clusters for new catalog printf("Switched to catalog: %s (%u glasses)\n", get_current_catalog_name(), get_glass_count()); } else { @@ -111,10 +105,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo cycle_catalog(1); refresh_view_data_range(view); // Update view for new catalog view->selectedGlass = -1; // Clear selection - view->selectedCluster = -1; // Clear cluster selection clear_text_cache(); // Clear text cache when switching catalogs - create_tight_clusters(view); // Recreate tight clusters for new catalog - create_loose_clusters(view); // Recreate loose clusters for new catalog printf("Switched to catalog: %s (%u glasses)\n", get_current_catalog_name(), get_glass_count()); } else { // Move right @@ -152,22 +143,11 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las i32 nearestGlass = find_nearest_glass((i32)mouseX, (i32)mouseY, view, 15); if (nearestGlass >= 0) { - // Check if clicked glass is part of a tight cluster - i32 clusterIndex = find_tight_cluster_for_glass(nearestGlass, view); - - if (clusterIndex >= 0 && view->tightClusters[clusterIndex].count > 1) { - // Clicked on a multi-glass tight cluster - view->selectedCluster = clusterIndex; - view->selectedGlass = -1; // Clear individual glass selection - } else { - // Clicked on individual glass or single-glass cluster - view->selectedGlass = nearestGlass; - view->selectedCluster = -1; // Clear cluster selection - } + // Found a glass within click tolerance - select it + view->selectedGlass = nearestGlass; } else { - // No glass clicked + // No glass clicked - clear selection view->selectedGlass = -1; - view->selectedCluster = -1; } if (nearestGlass < 0) { @@ -187,17 +167,48 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las return 0; } -// Process mouse motion event +// Process mouse motion event with bounds checking b32 process_mouse_motion(SDL_MouseMotionEvent *motion __attribute__((unused)), ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 dragging) { + if (!view || !lastMouseX || !lastMouseY) { + return 0; + } + if (dragging) { // Get current mouse position f32 mouseX, mouseY; SDL_GetMouseState(&mouseX, &mouseY); - // Calculate normalized delta + // Validate window dimensions to prevent division by zero + if (view->windowWidth <= 0 || view->windowHeight <= 0) { + printf("Warning: Invalid window dimensions in mouse motion: %dx%d\n", + view->windowWidth, view->windowHeight); + return 0; + } + + // Calculate normalized delta with bounds checking const i32 padding = get_adaptive_padding(view); - const f32 dx = (mouseX - *lastMouseX) / (view->windowWidth - 2 * padding); - const f32 dy = (mouseY - *lastMouseY) / (view->windowHeight - 2 * padding); + const i32 effectiveWidth = view->windowWidth - 2 * padding; + const i32 effectiveHeight = view->windowHeight - 2 * padding; + + // Prevent division by zero or very small values + if (effectiveWidth <= 10 || effectiveHeight <= 10) { + printf("Warning: Window too small for mouse interaction: effective size %dx%d\n", + effectiveWidth, effectiveHeight); + return 0; + } + + const f32 dx = (mouseX - *lastMouseX) / (f32)effectiveWidth; + const f32 dy = (mouseY - *lastMouseY) / (f32)effectiveHeight; + + // Validate delta values to prevent extreme jumps + const f32 maxDelta = 2.0f; // Maximum reasonable delta per frame + if (fabsf(dx) > maxDelta || fabsf(dy) > maxDelta) { + printf("Warning: Extreme mouse delta detected: dx=%.3f, dy=%.3f\n", dx, dy); + // Update position but don't apply the movement + *lastMouseX = (i32)mouseX; + *lastMouseY = (i32)mouseY; + return 1; + } // Update offset - inverted movement for natural feel view->offsetX += dx; @@ -213,13 +224,44 @@ b32 process_mouse_motion(SDL_MouseMotionEvent *motion __attribute__((unused)), V return 0; } -// Process window event +// Process window event with validation b32 process_window_event(SDL_WindowEvent *window_event, ViewState *view) { + if (!window_event || !view) { + return 0; + } + switch (window_event->type) { case SDL_EVENT_WINDOW_RESIZED: - view->windowWidth = (i32)window_event->data1; - view->windowHeight = (i32)window_event->data2; - return 1; + { + i32 newWidth = (i32)window_event->data1; + i32 newHeight = (i32)window_event->data2; + + // Validate new dimensions + const i32 minWidth = 200; // Minimum usable width + const i32 minHeight = 150; // Minimum usable height + const i32 maxWidth = 8192; // Maximum reasonable width + const i32 maxHeight = 8192; // Maximum reasonable height + + if (newWidth < minWidth || newHeight < minHeight) { + printf("Warning: Window size too small: %dx%d, clamping to minimum\n", + newWidth, newHeight); + newWidth = (newWidth < minWidth) ? minWidth : newWidth; + newHeight = (newHeight < minHeight) ? minHeight : newHeight; + } + + if (newWidth > maxWidth || newHeight > maxHeight) { + printf("Warning: Window size too large: %dx%d, clamping to maximum\n", + newWidth, newHeight); + newWidth = (newWidth > maxWidth) ? maxWidth : newWidth; + newHeight = (newHeight > maxHeight) ? maxHeight : newHeight; + } + + view->windowWidth = newWidth; + view->windowHeight = newHeight; + + printf("Window resized to: %dx%d\n", newWidth, newHeight); + return 1; + } default: // Ignore other window events break; @@ -254,9 +296,31 @@ b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, return process_mouse_motion(&event->motion, view, lastMouseX, lastMouseY, *dragging); case SDL_EVENT_WINDOW_RESIZED: - view->windowWidth = (i32)event->window.data1; - view->windowHeight = (i32)event->window.data2; - return 1; + { + i32 newWidth = (i32)event->window.data1; + i32 newHeight = (i32)event->window.data2; + + // Validate new dimensions + const i32 minWidth = 200; // Minimum usable width + const i32 minHeight = 150; // Minimum usable height + const i32 maxWidth = 8192; // Maximum reasonable width + const i32 maxHeight = 8192; // Maximum reasonable height + + if (newWidth < minWidth || newHeight < minHeight || + newWidth > maxWidth || newHeight > maxHeight) { + printf("Warning: Invalid window resize dimensions: %dx%d\n", + newWidth, newHeight); + // Still update but within bounds + newWidth = (newWidth < minWidth) ? minWidth : + (newWidth > maxWidth) ? maxWidth : newWidth; + newHeight = (newHeight < minHeight) ? minHeight : + (newHeight > maxHeight) ? maxHeight : newHeight; + } + + view->windowWidth = newWidth; + view->windowHeight = newHeight; + return 1; + } } return 0; diff --git a/src/glamac/glamac_events.d b/src/glamac/glamac_events.d new file mode 100644 index 0000000..11d79c7 --- /dev/null +++ b/src/glamac/glamac_events.d @@ -0,0 +1,10 @@ +glamac_events.o glamac_events.d: ../src/glamac/glamac_events.c \ + ../include/glamac/graphics/glamac_view.h \ + ../include/glamac/graphics/../core/glamacdef.h \ + ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h \ + ../include/glamac/graphics/glamac_render.h \ + ../include/glamac/graphics/glamac_view.h diff --git a/src/glamac/glamac_render.c b/src/glamac/glamac_render.c index 5230f1b..1fdaf30 100644 --- a/src/glamac/glamac_render.c +++ b/src/glamac/glamac_render.c @@ -7,29 +7,123 @@ #include #include #include -#include "glamac_render.h" -#include "glass_data.h" +#include "glamac/graphics/glamac_render.h" +#include "glamac/data/glass_data.h" + +// Font path constants and validation +#define MAX_FONT_PATHS 8 +#define MAX_FONT_PATH_LEN 512 +#define MIN_FONT_SIZE 8 +#define MAX_FONT_SIZE 72 +#define DEFAULT_FONT_SIZE 16 +#define AXIS_FONT_SIZE 12 +#define TITLE_FONT_SIZE 20 +#define LABEL_FONT_SIZE 11 + +// Grid and rendering constants +#define GRID_DIVISIONS 10 +#define CIRCLE_RADIUS 5 +#define LABEL_OFFSET_X 12 +// LABEL_OFFSET_Y is defined in glamac_view.h +#define AXIS_LABEL_OFFSET 15 +#define AXIS_MARGIN 45 + +// Safe font paths (in order of preference) +static const char* SAFE_FONT_PATHS[] = { + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/System/Library/Fonts/Arial.ttf", // macOS + "C:\\Windows\\Fonts\\arial.ttf", // Windows + "fonts/DejaVuSans.ttf", // Local fallback + "DejaVuSans.ttf", // Current directory + NULL +}; + +static const char* SAFE_BOLD_FONT_PATHS[] = { + "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/System/Library/Fonts/Arial Bold.ttf", // macOS + "C:\\Windows\\Fonts\\arialbd.ttf", // Windows + "fonts/DejaVuSans-Bold.ttf", // Local fallback + "DejaVuSans-Bold.ttf", // Current directory + NULL +}; + +// Text rendering cache with LRU eviction +#define TEXT_CACHE_SIZE 512 +#define MAX_TEXT_LENGTH 128 -// Text rendering cache typedef struct { - char text[128]; + char text[MAX_TEXT_LENGTH]; SDL_Texture* texture; i32 width, height; SDL_Color color; TTF_Font* font; + u32 last_used_time; // For LRU eviction + b32 in_use; // Slot availability flag } CachedText; -static CachedText textCache[512]; +static CachedText textCache[TEXT_CACHE_SIZE]; static i32 cacheSize = 0; +static u32 cache_access_counter = 0; // Global counter for LRU + +// Find least recently used cache entry for eviction +static i32 find_lru_cache_entry(void) { + i32 lru_index = 0; + u32 oldest_time = textCache[0].last_used_time; + + for (i32 i = 1; i < TEXT_CACHE_SIZE; i++) { + if (!textCache[i].in_use) { + return i; // Found empty slot + } + if (textCache[i].last_used_time < oldest_time) { + oldest_time = textCache[i].last_used_time; + lru_index = i; + } + } + + return lru_index; +} -// Find cached text texture +// Evict cache entry and free its resources +static void evict_cache_entry(i32 index) { + if (index >= 0 && index < TEXT_CACHE_SIZE && textCache[index].in_use) { + if (textCache[index].texture) { + SDL_DestroyTexture(textCache[index].texture); + textCache[index].texture = NULL; + } + textCache[index].in_use = 0; + textCache[index].text[0] = '\0'; + textCache[index].font = NULL; + textCache[index].last_used_time = 0; + } +} + +// Find cached text texture with LRU management static SDL_Texture* get_cached_text(SDL_Renderer* renderer, TTF_Font* font, const char* text, SDL_Color color, i32* width, i32* height) { - for (i32 i = 0; i < cacheSize; i++) { - if (textCache[i].font == font && + if (!renderer || !font || !text || !width || !height) { + return NULL; + } + + // Validate text length + size_t text_len = strlen(text); + if (text_len == 0 || text_len >= MAX_TEXT_LENGTH) { + return NULL; + } + + cache_access_counter++; + + // Search for existing cache entry + for (i32 i = 0; i < TEXT_CACHE_SIZE; i++) { + if (textCache[i].in_use && + textCache[i].font == font && textCache[i].color.r == color.r && textCache[i].color.g == color.g && textCache[i].color.b == color.b && strcmp(textCache[i].text, text) == 0) { + + // Update LRU timestamp + textCache[i].last_used_time = cache_access_counter; *width = textCache[i].width; *height = textCache[i].height; return textCache[i].texture; @@ -37,37 +131,60 @@ static SDL_Texture* get_cached_text(SDL_Renderer* renderer, TTF_Font* font, cons } // Create new cached texture - if (cacheSize < 512) { - SDL_Surface* surface = TTF_RenderText_Blended(font, text, strlen(text), color); - if (surface) { - SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); - if (texture) { - strncpy(textCache[cacheSize].text, text, sizeof(textCache[cacheSize].text) - 1); - textCache[cacheSize].text[sizeof(textCache[cacheSize].text) - 1] = '\0'; - textCache[cacheSize].texture = texture; - textCache[cacheSize].width = surface->w; - textCache[cacheSize].height = surface->h; - textCache[cacheSize].color = color; - textCache[cacheSize].font = font; - *width = surface->w; - *height = surface->h; - SDL_DestroySurface(surface); - cacheSize++; - return texture; - } - SDL_DestroySurface(surface); - } + SDL_Surface* surface = TTF_RenderText_Blended(font, text, text_len, color); + if (!surface) { + return NULL; } - return NULL; + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + if (!texture) { + SDL_DestroySurface(surface); + return NULL; + } + + // Find cache slot (either empty or LRU) + i32 cache_index; + if (cacheSize < TEXT_CACHE_SIZE) { + // Use next available slot + cache_index = cacheSize; + cacheSize++; + } else { + // Cache is full, evict LRU entry + cache_index = find_lru_cache_entry(); + evict_cache_entry(cache_index); + } + + // Store in cache + strncpy(textCache[cache_index].text, text, sizeof(textCache[cache_index].text) - 1); + textCache[cache_index].text[sizeof(textCache[cache_index].text) - 1] = '\0'; + textCache[cache_index].texture = texture; + textCache[cache_index].width = surface->w; + textCache[cache_index].height = surface->h; + textCache[cache_index].color = color; + textCache[cache_index].font = font; + textCache[cache_index].last_used_time = cache_access_counter; + textCache[cache_index].in_use = 1; + + *width = surface->w; + *height = surface->h; + SDL_DestroySurface(surface); + + return texture; } void clear_text_cache(void) { - for (i32 i = 0; i < cacheSize; i++) { - if (textCache[i].texture) { + for (i32 i = 0; i < TEXT_CACHE_SIZE; i++) { + if (textCache[i].in_use && textCache[i].texture) { SDL_DestroyTexture(textCache[i].texture); + textCache[i].texture = NULL; } + textCache[i].in_use = 0; + textCache[i].text[0] = '\0'; + textCache[i].font = NULL; + textCache[i].last_used_time = 0; } cacheSize = 0; + cache_access_counter = 0; } // Draw text helper @@ -126,12 +243,48 @@ void draw_grid(SDL_Renderer *renderer, const ViewState* view) { } } +// Safe font loading helper +static TTF_Font* load_font_safe(const char* const* font_paths, int size) { + if (!font_paths || size <= 0) return NULL; + + // Validate size range + if (size < 8 || size > 72) { + printf("Warning: Font size %d outside safe range (8-72), clamping\n", size); + size = (size < 8) ? 8 : 72; + } + + for (int i = 0; font_paths[i] != NULL; i++) { + const char* path = font_paths[i]; + + // Basic path validation + if (!path || strlen(path) == 0 || strlen(path) >= MAX_FONT_PATH_LEN) { + continue; + } + + // Check for directory traversal attempts + if (strstr(path, "..") || strstr(path, "//")) { + printf("Warning: Skipping potentially unsafe font path: %s\n", path); + continue; + } + + TTF_Font* font = TTF_OpenFont(path, size); + if (font) { + printf("Successfully loaded font: %s (size %d)\n", path, size); + return font; + } + } + + printf("Warning: Failed to load any font from provided paths\n"); + return NULL; +} + // Draw axes void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - // Create smaller font for axis scales - TTF_Font *axisFont = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 12); + // Create smaller font for axis scales with safe loading + TTF_Font *axisFont = load_font_safe(SAFE_FONT_PATHS, 12); if (!axisFont) { axisFont = font; // Fallback to regular font if loading fails + printf("Warning: Using fallback font for axes\n"); } const i32 padding = get_adaptive_padding(view); const i32 plotWidth = view->windowWidth - 2 * padding; @@ -252,11 +405,9 @@ void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewSt // Draw glass point (larger) draw_filled_circle(renderer, x, y, 5); - // Draw label only if it should be shown (tight clustering logic) - if (should_show_glass_label((i32)i, view)) { - const char* glassName = (const char*)glass->name; - draw_text(renderer, labelFont, glassName, x + 12, y - 12, labelColor); - } + // Show all glass labels (clustering removed) + const char* glassName = (const char*)glass->name; + draw_text(renderer, labelFont, glassName, x + 12, y - 12, labelColor); } } @@ -282,52 +433,6 @@ void calculate_smart_window_position(i32 glassX, i32 glassY, i32 windowWidth, i3 } } -// Draw cluster properties window (shows all glasses in tight cluster) -void draw_cluster_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - if (view->selectedCluster < 0 || !view->tightClusters) return; - - const TightCluster* cluster = &view->tightClusters[view->selectedCluster]; - if (cluster->count == 0) return; - - // Calculate cluster position on screen using representative glass - const Glass* repGlass = get_glass(cluster->representativeIndex); - if (!repGlass) return; - - i32 clusterX, clusterY; - data_to_screen_coords(repGlass->abbeNumber, repGlass->refractiveIndex, view, &clusterX, &clusterY); - - // Window properties - const i32 windowWidth = 320; - const i32 windowHeight = 20 + cluster->count * 25; - - i32 windowX, windowY; - calculate_smart_window_position(clusterX, clusterY, windowWidth, windowHeight, view, &windowX, &windowY); - - // Draw background - SDL_SetRenderDrawColor(renderer, 255, 255, 255, 220); - SDL_FRect bgRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; - SDL_RenderFillRect(renderer, &bgRect); - - // Draw border - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderRect(renderer, &bgRect); - - // Draw text - SDL_Color black = {0, 0, 0, 255}; - char buffer[256]; - - // Draw each glass in cluster directly (no title) - for (i32 i = 0; i < cluster->count; i++) { - const Glass* glass = get_glass(cluster->glassIndices[i]); - if (glass) { - i32 yPos = windowY + 10 + i * 25; - - snprintf(buffer, sizeof(buffer), "%s: nd=%.4f, vd=%.2f", - (const char*)glass->name, glass->refractiveIndex, glass->abbeNumber); - draw_text(renderer, font, buffer, windowX + 10, yPos, black); - } - } -} // Draw glass properties window void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { @@ -439,8 +544,6 @@ void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Fon // Draw glass properties if selected draw_glass_properties(renderer, font, titleFont, view); - // Draw cluster properties if selected - draw_cluster_properties(renderer, font, titleFont, view); // Draw help window if needed draw_help_window(renderer, font, titleFont, view); @@ -451,33 +554,96 @@ void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Fon // Font management functions b32 load_fonts(FontSet *fonts) { - fonts->regular = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 16); - fonts->title = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", 20); - fonts->label = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 14); + if (!fonts) return 0; + + // Initialize to NULL for safe cleanup + fonts->regular = NULL; + fonts->title = NULL; + fonts->label = NULL; - return fonts->regular && fonts->title && fonts->label; + fonts->regular = load_font_safe(SAFE_FONT_PATHS, 16); + fonts->title = load_font_safe(SAFE_BOLD_FONT_PATHS, 20); + fonts->label = load_font_safe(SAFE_FONT_PATHS, 11); + + // Check if at least one font loaded successfully + if (!fonts->regular && !fonts->title && !fonts->label) { + printf("Critical error: No fonts could be loaded\n"); + return 0; + } + + // Use fallbacks if specific fonts failed + if (!fonts->title && fonts->regular) { + fonts->title = fonts->regular; + printf("Warning: Using regular font as title font fallback\n"); + } + if (!fonts->label && fonts->regular) { + fonts->label = fonts->regular; + printf("Warning: Using regular font as label font fallback\n"); + } + if (!fonts->regular && fonts->title) { + fonts->regular = fonts->title; + printf("Warning: Using title font as regular font fallback\n"); + } + + return (fonts->regular && fonts->title && fonts->label) ? 1 : 0; } b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi) { (void)windowHeight; // Suppress unused parameter warning - // Calculate font sizes based on window size and DPI (increased sizes) + + if (!fonts) return 0; + + // Validate input parameters + if (windowWidth <= 0 || dpi <= 0) { + printf("Warning: Invalid window dimensions or DPI, using defaults\n"); + windowWidth = 800; + dpi = 96.0f; + } + + // Calculate font sizes based on window size and DPI i32 regularSize = (i32)(16.0f * dpi / 96.0f * windowWidth / 800.0f); i32 titleSize = (i32)(20.0f * dpi / 96.0f * windowWidth / 800.0f); - i32 labelSize = (i32)(14.0f * dpi / 96.0f * windowWidth / 800.0f); + // Reduce label size for better overview visibility (was 14.0f) + i32 labelSize = (i32)(11.0f * dpi / 96.0f * windowWidth / 800.0f); - // Clamp font sizes + // Clamp font sizes to safe ranges if (regularSize < 12) regularSize = 12; if (regularSize > 32) regularSize = 32; if (titleSize < 16) titleSize = 16; if (titleSize > 36) titleSize = 36; - if (labelSize < 10) labelSize = 10; - if (labelSize > 24) labelSize = 24; + if (labelSize < 8) labelSize = 8; + if (labelSize > 20) labelSize = 20; + + // Initialize to NULL for safe cleanup + fonts->regular = NULL; + fonts->title = NULL; + fonts->label = NULL; + + fonts->regular = load_font_safe(SAFE_FONT_PATHS, regularSize); + fonts->title = load_font_safe(SAFE_BOLD_FONT_PATHS, titleSize); + fonts->label = load_font_safe(SAFE_FONT_PATHS, labelSize); + + // Check if at least one font loaded successfully + if (!fonts->regular && !fonts->title && !fonts->label) { + printf("Critical error: No adaptive fonts could be loaded\n"); + return 0; + } - fonts->regular = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", regularSize); - fonts->title = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", titleSize); - fonts->label = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", labelSize); + // Use fallbacks if specific fonts failed + if (!fonts->title && fonts->regular) { + fonts->title = fonts->regular; + printf("Warning: Using regular font as title font fallback\n"); + } + if (!fonts->label && fonts->regular) { + fonts->label = fonts->regular; + printf("Warning: Using regular font as label font fallback\n"); + } + if (!fonts->regular && fonts->title) { + fonts->regular = fonts->title; + printf("Warning: Using title font as regular font fallback\n"); + } - return fonts->regular && fonts->title && fonts->label; + return (fonts->regular && fonts->title && fonts->label) ? 1 : 0; } void free_fonts(FontSet *fonts) { diff --git a/src/glamac/glamac_render.d b/src/glamac/glamac_render.d new file mode 100644 index 0000000..738174c --- /dev/null +++ b/src/glamac/glamac_render.d @@ -0,0 +1,9 @@ +glamac_render.o glamac_render.d: ../src/glamac/glamac_render.c \ + ../include/glamac/graphics/glamac_render.h \ + ../include/glamac/graphics/../core/glamacdef.h \ + ../include/glamac/graphics/glamac_view.h \ + ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h diff --git a/src/glamac/glamac_view.c b/src/glamac/glamac_view.c index ff85b71..244523e 100644 --- a/src/glamac/glamac_view.c +++ b/src/glamac/glamac_view.c @@ -6,21 +6,12 @@ #include #include #include -#include "glamac_view.h" -#include "glass_data.h" +#include "glamac/graphics/glamac_view.h" +#include "glamac/data/glass_data.h" // External debug mode function extern b32 is_debug_mode(void); -// Global tight cluster threshold parameters -f32 g_tight_cluster_nd_threshold = DEFAULT_TIGHT_CLUSTER_ND_THRESHOLD; -f32 g_tight_cluster_vd_threshold = DEFAULT_TIGHT_CLUSTER_VD_THRESHOLD; - -// Global loose cluster parameters (zoom-dependent) -f32 g_loose_cluster_nd_threshold = DEFAULT_LOOSE_CLUSTER_ND_THRESHOLD; // Base threshold -f32 g_loose_cluster_vd_threshold = DEFAULT_LOOSE_CLUSTER_VD_THRESHOLD; // Base threshold -f32 g_loose_cluster_nd_fraction = DEFAULT_LOOSE_CLUSTER_ND_FRACTION; // Zoom scaling fraction -f32 g_loose_cluster_vd_fraction = DEFAULT_LOOSE_CLUSTER_VD_FRACTION; // Zoom scaling fraction // Initialize a view state with default values void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { @@ -33,22 +24,6 @@ void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { view->gKeyPressed = 0; view->gKeyTime = 0; view->selectedGlass = -1; // No glass selected initially - view->selectedCluster = -1; // No cluster selected initially - - // Initialize tight cluster data - view->tightClusters = NULL; - view->tightClusterCount = 0; - - // Initialize loose cluster data - view->looseClusters = NULL; - view->looseClusterCount = 0; - - // Initialize label recalculation tracking (force initial calculation) - view->lastZoomLevel = -1.0f; - view->lastWindowWidth = -1; - view->lastWindowHeight = -1; - view->lastOffsetX = -999.0f; - view->lastOffsetY = -999.0f; // Calculate data range find_glass_data_range(&view->minAbbe, &view->maxAbbe, &view->minRI, &view->maxRI); @@ -58,8 +33,6 @@ void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { void refresh_view_data_range(ViewState* view) { find_glass_data_range(&view->minAbbe, &view->maxAbbe, &view->minRI, &view->maxRI); - // Force label recalculation on catalog change - view->lastZoomLevel = -1.0f; // Invalid values to force recalc } // Calculate visible data range @@ -133,8 +106,6 @@ void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view view->offsetX += (f32)(mouseX - newMouseScreenX) / (view->windowWidth - 2 * padding); view->offsetY += (f32)(newMouseScreenY - mouseY) / (view->windowHeight - 2 * padding); - // Recreate loose clusters since zoom changed (they are zoom-dependent) - create_loose_clusters(view); } // Toggle fullscreen @@ -155,373 +126,13 @@ void reset_view(ViewState* view) { view->offsetY = 0.0f; view->selectedGlass = -1; - // Recreate loose clusters since zoom changed - create_loose_clusters(view); } -// Check if two glasses should be clustered based on separate nd and vd thresholds -static inline b32 should_cluster_glasses(const Glass* glass1, const Glass* glass2) { - f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); - f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); - - return (nd_diff <= g_tight_cluster_nd_threshold) && (vd_diff <= g_tight_cluster_vd_threshold); -} -// Find glass with shortest name in a list -static i32 find_shortest_name_glass(const i32* glassIndices, i32 count) { - if (count == 0) return -1; - - i32 shortestIndex = glassIndices[0]; - size_t shortestLength = strlen((const char*)get_glass(shortestIndex)->name); - - for (i32 i = 1; i < count; i++) { - const Glass* glass = get_glass(glassIndices[i]); - if (glass) { - size_t nameLength = strlen((const char*)glass->name); - if (nameLength < shortestLength) { - shortestLength = nameLength; - shortestIndex = glassIndices[i]; - } - } - } - - return shortestIndex; -} -// Create tight clusters for current catalog -void create_tight_clusters(ViewState* view) { - // Free existing clusters - free_tight_clusters(view); - - const u32 glassCount = get_glass_count(); - if (glassCount == 0) return; - - // Allocate temporary arrays - TightCluster* clusters = (TightCluster*)calloc(glassCount, sizeof(TightCluster)); - b32* processed = (b32*)calloc(glassCount, sizeof(b32)); - i32 clusterCount = 0; - - if (!clusters || !processed) { - free(clusters); - free(processed); - return; - } - - printf("Creating tight clusters for %s catalog (%u glasses, nd_threshold=%.6f, vd_threshold=%.3f)...\n", - get_current_catalog_name(), glassCount, g_tight_cluster_nd_threshold, g_tight_cluster_vd_threshold); - - // Simple clustering algorithm - for (u32 i = 0; i < glassCount; i++) { - if (processed[i]) continue; - - const Glass* glass1 = get_glass(i); - if (!glass1) continue; - - // Start new cluster with this glass - TightCluster* cluster = &clusters[clusterCount]; - cluster->glassIndices[0] = (i32)i; - cluster->count = 1; - cluster->avgAbbeNumber = glass1->abbeNumber; - cluster->avgRefractiveIndex = glass1->refractiveIndex; - processed[i] = 1; - - // Find nearby glasses within tight clustering distance - for (u32 j = i + 1; j < glassCount && cluster->count < MAX_CLUSTER_SIZE; j++) { - if (processed[j]) continue; - - const Glass* glass2 = get_glass(j); - if (!glass2) continue; - - if (should_cluster_glasses(glass1, glass2)) { - // Add to cluster - cluster->glassIndices[cluster->count] = (i32)j; - cluster->count++; - - // Update average position - cluster->avgAbbeNumber = (cluster->avgAbbeNumber * (cluster->count - 1) + glass2->abbeNumber) / cluster->count; - cluster->avgRefractiveIndex = (cluster->avgRefractiveIndex * (cluster->count - 1) + glass2->refractiveIndex) / cluster->count; - - processed[j] = 1; - } - } - - // Set representative glass (shortest name) - cluster->representativeIndex = find_shortest_name_glass(cluster->glassIndices, cluster->count); - - clusterCount++; - } - - // Count multi-glass clusters - i32 multiGlassClusters = 0; - for (i32 i = 0; i < clusterCount; i++) { - if (clusters[i].count > 1) { - multiGlassClusters++; - } - } - - printf("Created %d tight clusters (%d with multiple glasses)\n", clusterCount, multiGlassClusters); - - // Debug output: show detailed cluster information - if (is_debug_mode()) { - printf("\n=== TIGHT CLUSTER DEBUG INFO ===\n"); - printf("Catalog: %s, nd_threshold: %.6f, vd_threshold: %.3f\n", - get_current_catalog_name(), g_tight_cluster_nd_threshold, g_tight_cluster_vd_threshold); - - for (i32 i = 0; i < clusterCount; i++) { - const TightCluster* cluster = &clusters[i]; - if (cluster->count > 1) { - printf("\nCluster %d (%d glasses):\n", i, cluster->count); - for (i32 j = 0; j < cluster->count; j++) { - const Glass* glass = get_glass(cluster->glassIndices[j]); - if (glass) { - printf(" %s: nd=%.6f, vd=%.3f", - (const char*)glass->name, glass->refractiveIndex, glass->abbeNumber); - if (cluster->glassIndices[j] == cluster->representativeIndex) { - printf(" [REPRESENTATIVE]"); - } - printf("\n"); - } - } - - // Calculate and display pairwise distances within cluster - printf(" Pairwise distances:\n"); - for (i32 j = 0; j < cluster->count; j++) { - for (i32 k = j + 1; k < cluster->count; k++) { - const Glass* glass1 = get_glass(cluster->glassIndices[j]); - const Glass* glass2 = get_glass(cluster->glassIndices[k]); - if (glass1 && glass2) { - f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); - f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); - printf(" %s <-> %s: nd_diff=%.6f, vd_diff=%.3f\n", - (const char*)glass1->name, (const char*)glass2->name, nd_diff, vd_diff); - } - } - } - } - } - printf("=== END CLUSTER DEBUG INFO ===\n\n"); - } - - // Resize to actual number of clusters and store - if (clusterCount > 0) { - TightCluster* resizedClusters = (TightCluster*)realloc(clusters, clusterCount * sizeof(TightCluster)); - if (!resizedClusters) { - free(clusters); - view->tightClusters = NULL; - view->tightClusterCount = 0; - } else { - view->tightClusters = resizedClusters; - view->tightClusterCount = clusterCount; - } - } else { - free(clusters); - view->tightClusters = NULL; - view->tightClusterCount = 0; - } - - free(processed); -} - -// Free tight clusters -void free_tight_clusters(ViewState* view) { - if (view->tightClusters) { - free(view->tightClusters); - view->tightClusters = NULL; - } - view->tightClusterCount = 0; - view->selectedCluster = -1; -} -// Create loose clusters for current catalog (zoom-dependent) -void create_loose_clusters(ViewState* view) { - // Free existing clusters - free_loose_clusters(view); - - const u32 glassCount = get_glass_count(); - if (glassCount == 0) return; - - // Calculate zoom-dependent thresholds that reach ZERO at maximum zoom - f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; - get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - - // Calculate zoom factor based on zoom level (0.0 at max zoom, 1.0 at min zoom) - // At MAX_ZOOM, factor = 0; At MIN_ZOOM, factor = 1 - f32 zoomRange = MAX_ZOOM - MIN_ZOOM; - f32 currentZoomNormalized = (view->zoomLevel - MIN_ZOOM) / zoomRange; - - // Invert so that factor = 0 at max zoom, factor = 1 at min zoom - f32 zoomFactor = 1.0f - (currentZoomNormalized > 1.0f ? 1.0f : currentZoomNormalized); - - // Ensure we reach exactly zero at maximum zoom - if (view->zoomLevel >= MAX_ZOOM) { - zoomFactor = 0.0f; - } - - // Calculate thresholds: ZERO at max zoom, base*fraction at min zoom - f32 ndThreshold = zoomFactor * g_loose_cluster_nd_threshold * g_loose_cluster_nd_fraction; - f32 vdThreshold = zoomFactor * g_loose_cluster_vd_threshold * g_loose_cluster_vd_fraction; - - // Allocate temporary arrays - LooseCluster* clusters = (LooseCluster*)calloc(glassCount, sizeof(LooseCluster)); - b32* processed = (b32*)calloc(glassCount, sizeof(b32)); - i32 clusterCount = 0; - - if (!clusters || !processed) { - free(clusters); - free(processed); - return; - } - - printf("Creating loose clusters for %s catalog (%u glasses)...\n", get_current_catalog_name(), glassCount); - printf(" zoom level: %.2f, zoom factor: %.4f\n", view->zoomLevel, zoomFactor); - printf(" nd: %.3f * %.1f * %.4f = %.6f\n", - g_loose_cluster_nd_threshold, g_loose_cluster_nd_fraction, zoomFactor, ndThreshold); - printf(" vd: %.2f * %.1f * %.4f = %.3f\n", - g_loose_cluster_vd_threshold, g_loose_cluster_vd_fraction, zoomFactor, vdThreshold); - - // Simple clustering algorithm (same as tight clustering but with zoom-dependent thresholds) - for (u32 i = 0; i < glassCount; i++) { - if (processed[i]) continue; - - const Glass* glass1 = get_glass(i); - if (!glass1) continue; - - // Start new cluster with this glass - LooseCluster* cluster = &clusters[clusterCount]; - cluster->glassIndices[0] = (i32)i; - cluster->count = 1; - cluster->avgAbbeNumber = glass1->abbeNumber; - cluster->avgRefractiveIndex = glass1->refractiveIndex; - processed[i] = 1; - - // Find nearby glasses within loose clustering distance - for (u32 j = i + 1; j < glassCount && cluster->count < MAX_CLUSTER_SIZE; j++) { - if (processed[j]) continue; - - const Glass* glass2 = get_glass(j); - if (!glass2) continue; - - // Check if glasses should be clustered (separate nd/vd thresholds) - f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); - f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); - - if (nd_diff <= ndThreshold && vd_diff <= vdThreshold) { - // Add to cluster - cluster->glassIndices[cluster->count] = (i32)j; - cluster->count++; - - // Update average position - cluster->avgAbbeNumber = (cluster->avgAbbeNumber * (cluster->count - 1) + glass2->abbeNumber) / cluster->count; - cluster->avgRefractiveIndex = (cluster->avgRefractiveIndex * (cluster->count - 1) + glass2->refractiveIndex) / cluster->count; - - processed[j] = 1; - } - } - - // Set representative glass (shortest name) - cluster->representativeIndex = find_shortest_name_glass(cluster->glassIndices, cluster->count); - - clusterCount++; - } - - // Count multi-glass clusters - i32 multiGlassClusters = 0; - for (i32 i = 0; i < clusterCount; i++) { - if (clusters[i].count > 1) { - multiGlassClusters++; - } - } - - printf("Created %d loose clusters (%d with multiple glasses)\n", clusterCount, multiGlassClusters); - - // Resize to actual number of clusters and store - if (clusterCount > 0) { - LooseCluster* resizedClusters = (LooseCluster*)realloc(clusters, clusterCount * sizeof(LooseCluster)); - if (!resizedClusters) { - free(clusters); - view->looseClusters = NULL; - view->looseClusterCount = 0; - } else { - view->looseClusters = resizedClusters; - view->looseClusterCount = clusterCount; - } - } else { - free(clusters); - view->looseClusters = NULL; - view->looseClusterCount = 0; - } - - free(processed); -} -// Free loose clusters -void free_loose_clusters(ViewState* view) { - if (view->looseClusters) { - free(view->looseClusters); - view->looseClusters = NULL; - } - view->looseClusterCount = 0; -} -// Find tight cluster that contains a specific glass -i32 find_tight_cluster_for_glass(i32 glassIndex, const ViewState* view) { - if (!view->tightClusters) return -1; - - for (i32 i = 0; i < view->tightClusterCount; i++) { - for (i32 j = 0; j < view->tightClusters[i].count; j++) { - if (view->tightClusters[i].glassIndices[j] == glassIndex) { - return i; // Return cluster index - } - } - } - - return -1; // Not in any cluster -} -// Find loose cluster that contains a specific glass -i32 find_loose_cluster_for_glass(i32 glassIndex, const ViewState* view) { - if (!view->looseClusters) return -1; - - for (i32 i = 0; i < view->looseClusterCount; i++) { - for (i32 j = 0; j < view->looseClusters[i].count; j++) { - if (view->looseClusters[i].glassIndices[j] == glassIndex) { - return i; // Return cluster index - } - } - } - - return -1; // Not in any cluster -} -// Check if a glass label should be shown (combined tight and loose clustering logic) -b32 should_show_glass_label(i32 glassIndex, const ViewState* view) { - // First check tight clusters (they have priority) - i32 tightClusterIndex = find_tight_cluster_for_glass(glassIndex, view); - - if (tightClusterIndex >= 0) { - // Glass is in a tight cluster - const TightCluster* cluster = &view->tightClusters[tightClusterIndex]; - - if (cluster->count > 1) { - // Multi-glass tight cluster: only show representative (shortest name) - return glassIndex == cluster->representativeIndex; - } - } - - // Then check loose clusters (only if not in tight cluster) - i32 looseClusterIndex = find_loose_cluster_for_glass(glassIndex, view); - - if (looseClusterIndex >= 0) { - // Glass is in a loose cluster - const LooseCluster* cluster = &view->looseClusters[looseClusterIndex]; - - if (cluster->count > 1) { - // Multi-glass loose cluster: only show representative (shortest name) - return glassIndex == cluster->representativeIndex; - } - } - - // Single glass or not in any cluster: always show - return 1; -} diff --git a/src/glamac/glamac_view.d b/src/glamac/glamac_view.d new file mode 100644 index 0000000..81ffa71 --- /dev/null +++ b/src/glamac/glamac_view.d @@ -0,0 +1,8 @@ +glamac_view.o glamac_view.d: ../src/glamac/glamac_view.c \ + ../include/glamac/graphics/glamac_view.h \ + ../include/glamac/graphics/../core/glamacdef.h \ + ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h diff --git a/src/glamac/glass_data.c b/src/glamac/glass_data.c index 32077de..c6e9e10 100644 --- a/src/glamac/glass_data.c +++ b/src/glamac/glass_data.c @@ -10,9 +10,9 @@ * * See the COPYING file for the full license text. */ -#include "glamacdef.h" -#include "glass_data.h" -#include "glamac_errors.h" +#include "glamac/core/glamacdef.h" +#include "glamac/data/glass_data.h" +#include "glamac/core/glamac_errors.h" #include #include #include @@ -55,7 +55,157 @@ static const Glass default_glasses[] = { #define DEFAULT_GLASS_COUNT (sizeof(default_glasses) / sizeof(default_glasses[0])) -// Security validation functions +// Enhanced glass data validation +static GlamacResult validate_glass_properties(const char* name, double nd, double vd, const char* glass_code) { + // Validate glass name + if (!name || strlen(name) == 0 || strlen(name) >= 50) { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + + // Check for valid characters in glass name (alphanumeric, dash, underscore, space) + for (const char* p = name; *p; p++) { + if (!isalnum(*p) && *p != '-' && *p != '_' && *p != ' ' && *p != '+') { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + } + + // Validate refractive index (nd) - typical range for optical glasses + if (nd < 1.0 || nd > 4.0) { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + + // Validate Abbe number (vd) - typical range for optical glasses + if (vd < 10.0 || vd > 100.0) { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + + // Additional physics-based validation: check for reasonable nd/vd relationship + // High refractive index glasses typically have lower Abbe numbers + if (nd > 2.0 && vd > 80.0) { + return GLAMAC_ERROR_INVALID_GLASS_DATA; // Unusual combination + } + + // Validate glass code format if provided + if (glass_code && strlen(glass_code) > 0) { + size_t code_len = strlen(glass_code); + if (code_len >= 20) { // Glass code field size limit + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + + // Glass code should be primarily numeric (allowing some special chars) + int digit_count = 0; + for (const char* p = glass_code; *p; p++) { + if (isdigit(*p)) { + digit_count++; + } else if (!isalnum(*p) && *p != '-' && *p != '.') { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + } + + // Glass code should have at least some digits + if (digit_count == 0) { + return GLAMAC_ERROR_INVALID_GLASS_DATA; + } + } + + return GLAMAC_SUCCESS; +} + +static GlamacResult validate_json_structure(const char* json, size_t json_len) { + if (!json || json_len == 0) { + return GLAMAC_ERROR_INVALID_JSON; + } + + // Basic JSON structure validation + int brace_count = 0; + int bracket_count = 0; + int quote_count = 0; + bool in_string = false; + bool escaped = false; + + for (size_t i = 0; i < json_len; i++) { + char c = json[i]; + + if (escaped) { + escaped = false; + continue; + } + + if (c == '\\' && in_string) { + escaped = true; + continue; + } + + if (c == '"') { + in_string = !in_string; + quote_count++; + } + + if (!in_string) { + if (c == '{') brace_count++; + else if (c == '}') brace_count--; + else if (c == '[') bracket_count++; + else if (c == ']') bracket_count--; + + // Check for balanced braces/brackets + if (brace_count < 0 || bracket_count < 0) { + return GLAMAC_ERROR_INVALID_JSON; + } + } + } + + // Final balance check + if (brace_count != 0 || bracket_count != 0 || (quote_count % 2) != 0) { + return GLAMAC_ERROR_INVALID_JSON; + } + + // Check for required top-level structure + if (!strstr(json, "\"manufacturers\"") && !strstr(json, "\"glasses\"")) { + return GLAMAC_ERROR_INVALID_JSON; + } + + return GLAMAC_SUCCESS; +} + +static GlamacResult validate_manufacturer_data(const char* manufacturer_name) { + if (!manufacturer_name || strlen(manufacturer_name) == 0) { + return GLAMAC_ERROR_INVALID_MANUFACTURER; + } + + size_t name_len = strlen(manufacturer_name); + if (name_len >= MAX_MANUFACTURER_NAME_LEN) { + return GLAMAC_ERROR_INVALID_MANUFACTURER; + } + + // Validate manufacturer name characters + for (const char* p = manufacturer_name; *p; p++) { + if (!isalnum(*p) && *p != '-' && *p != '_' && *p != ' ' && *p != '&') { + return GLAMAC_ERROR_INVALID_MANUFACTURER; + } + } + + // Check against known manufacturers list for additional validation + const char* known_manufacturers[] = { + "SCHOTT", "HOYA", "CDGM", "Ohara", "SUMITA", "HIKARI", NULL + }; + + bool is_known = false; + for (int i = 0; known_manufacturers[i]; i++) { + if (strcasecmp(manufacturer_name, known_manufacturers[i]) == 0) { + is_known = true; + break; + } + } + + // For now, just log unknown manufacturers but don't reject them + if (!is_known) { + printf("Warning: Unknown manufacturer '%s' - data may be incorrect\n", manufacturer_name); + } + + return GLAMAC_SUCCESS; +} + +// Enhanced security validation functions static GlamacResult validate_file_path(const char* path) { if (!path) return GLAMAC_ERROR_INVALID_ARGUMENT; @@ -64,9 +214,58 @@ static GlamacResult validate_file_path(const char* path) { return GLAMAC_ERROR_INVALID_PATH; } - // Check for directory traversal attempts - if (strstr(path, "..") || strstr(path, "//") || - strchr(path, '|') || strchr(path, ';') || strchr(path, '&')) { + // Check for various directory traversal attempts + const char* dangerous_patterns[] = { + "..", // Basic traversal + "./.", // Current directory traversal + "/..", // Unix traversal + "\\..", // Windows traversal + "//", // Double slash + "\\\\", // Double backslash + "%2e%2e", // URL-encoded .. + "%2f", // URL-encoded / + "%5c", // URL-encoded backslash + "<", // XML/HTML injection + ">", // XML/HTML injection + "|", // Shell pipe + ";", // Command separator + "&", // Command separator + "`", // Command substitution + "$", // Variable expansion + "~", // Home directory expansion + NULL + }; + + // Check for dangerous patterns + for (int i = 0; dangerous_patterns[i]; i++) { + if (strstr(path, dangerous_patterns[i])) { + printf("Security warning: Dangerous pattern '%s' found in path: %s\n", + dangerous_patterns[i], path); + return GLAMAC_ERROR_INVALID_PATH; + } + } + + // Additional checks for path structure + const char* p = path; + while (*p) { + // Check for control characters + if (*p < 32 && *p != '\t') { + printf("Security warning: Control character found in path\n"); + return GLAMAC_ERROR_INVALID_PATH; + } + + // Check for high-bit characters that might be encoding attempts + if ((unsigned char)*p > 127) { + printf("Security warning: Non-ASCII character found in path\n"); + return GLAMAC_ERROR_INVALID_PATH; + } + + p++; + } + + // Path should end with .json for this application + if (len < 5 || strcmp(path + len - 5, ".json") != 0) { + printf("Security warning: Path does not end with .json extension\n"); return GLAMAC_ERROR_INVALID_PATH; } @@ -265,7 +464,9 @@ static GlamacResult parse_manufacturer_glasses(const char* manufacturer_section, 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) { + // Enhanced validation using the new validation functions + GlamacResult validation_result = validate_glass_properties(name, nd, vd, glass_code); + if (name && validation_result == GLAMAC_SUCCESS) { // Expand glasses array if needed if (catalog->count >= catalog->capacity) { u32 new_capacity = catalog->capacity * 2; @@ -281,31 +482,55 @@ static GlamacResult parse_manufacturer_glasses(const char* manufacturer_section, catalog->capacity = new_capacity; } - // Copy glass data safely + // Copy glass data safely with proper bounds checking size_t name_len = strlen(name); - if (name_len < sizeof(catalog->glasses[catalog->count].name)) { - strcpy((char*)catalog->glasses[catalog->count].name, name); + if (name_len > 0 && name_len < sizeof(catalog->glasses[catalog->count].name)) { + // Use strncpy with explicit null termination for safety + strncpy((char*)catalog->glasses[catalog->count].name, name, + sizeof(catalog->glasses[catalog->count].name) - 1); + catalog->glasses[catalog->count].name[sizeof(catalog->glasses[catalog->count].name) - 1] = '\0'; + catalog->glasses[catalog->count].abbeNumber = (f32)vd; catalog->glasses[catalog->count].refractiveIndex = (f32)nd; - // Copy glass code safely - if (glass_code) { + // Copy glass code safely with bounds checking + if (glass_code && strlen(glass_code) > 0) { size_t code_len = strlen(glass_code); if (code_len < sizeof(catalog->glasses[catalog->count].glass_code)) { - strcpy((char*)catalog->glasses[catalog->count].glass_code, glass_code); + strncpy((char*)catalog->glasses[catalog->count].glass_code, glass_code, + sizeof(catalog->glasses[catalog->count].glass_code) - 1); + catalog->glasses[catalog->count].glass_code[sizeof(catalog->glasses[catalog->count].glass_code) - 1] = '\0'; } else { + // Truncate if too long strncpy((char*)catalog->glasses[catalog->count].glass_code, glass_code, sizeof(catalog->glasses[catalog->count].glass_code) - 1); catalog->glasses[catalog->count].glass_code[sizeof(catalog->glasses[catalog->count].glass_code) - 1] = '\0'; + printf("Warning: Glass code truncated for %s\n", name); } } else { - strcpy((char*)catalog->glasses[catalog->count].glass_code, "N/A"); + // Safe copy of N/A string + strncpy((char*)catalog->glasses[catalog->count].glass_code, "N/A", + sizeof(catalog->glasses[catalog->count].glass_code) - 1); + catalog->glasses[catalog->count].glass_code[sizeof(catalog->glasses[catalog->count].glass_code) - 1] = '\0'; } - // Copy manufacturer name - strcpy((char*)catalog->glasses[catalog->count].manufacturer, catalog->name); + // Copy manufacturer name with bounds checking + size_t mfg_len = strlen(catalog->name); + if (mfg_len < sizeof(catalog->glasses[catalog->count].manufacturer)) { + strncpy((char*)catalog->glasses[catalog->count].manufacturer, catalog->name, + sizeof(catalog->glasses[catalog->count].manufacturer) - 1); + catalog->glasses[catalog->count].manufacturer[sizeof(catalog->glasses[catalog->count].manufacturer) - 1] = '\0'; + } else { + // Fallback to "Unknown" if manufacturer name is too long + strncpy((char*)catalog->glasses[catalog->count].manufacturer, "Unknown", + sizeof(catalog->glasses[catalog->count].manufacturer) - 1); + catalog->glasses[catalog->count].manufacturer[sizeof(catalog->glasses[catalog->count].manufacturer) - 1] = '\0'; + printf("Warning: Manufacturer name too long, using 'Unknown'\n"); + } catalog->count++; + } else { + printf("Warning: Glass name invalid or too long: %s\n", name ? name : "NULL"); } } @@ -329,6 +554,12 @@ static GlamacResult parse_manufacturer_glasses(const char* manufacturer_section, // Load single manufacturer into a specific catalog static GlamacResult load_single_manufacturer_to_catalog(const char* json_content, size_t json_len, const char* manufacturer_name, GlassCatalog* catalog) { + // Validate manufacturer name first + GlamacResult result = validate_manufacturer_data(manufacturer_name); + if (result != GLAMAC_SUCCESS) { + return result; + } + char manufacturer_key[MAX_MANUFACTURER_NAME_LEN + 16]; int ret = snprintf(manufacturer_key, sizeof(manufacturer_key), "\"%s\":", manufacturer_name); if (ret < 0 || ret >= (int)sizeof(manufacturer_key)) { @@ -344,7 +575,7 @@ static GlamacResult load_single_manufacturer_to_catalog(const char* json_content size_t section_len = json_len - (mfg_section - json_content); // Initialize catalog with full capacity to avoid realloc during parsing - GlamacResult result = allocate_catalog_glasses(catalog, 2000, manufacturer_name); + result = allocate_catalog_glasses(catalog, 2000, manufacturer_name); if (result != GLAMAC_SUCCESS) { return result; } @@ -440,6 +671,13 @@ static GlamacResult load_glasses_from_json_secure(const char* json_path, const c json_content[bytes_read] = '\0'; + // Validate JSON structure before parsing + result = validate_json_structure(json_content, bytes_read); + if (result != GLAMAC_SUCCESS) { + free(json_content); + return result; + } + // Handle both single manufacturer and all manufacturers cases if (manufacturer_filter) { // Load specific manufacturer into first catalog @@ -566,9 +804,11 @@ void initialize_glass_data(void) { g_glass_context.current_catalog = 0; g_glass_context.using_json_data = 0; - // Set manufacturer for default glasses + // Set manufacturer for default glasses with bounds checking for (u32 i = 0; i < DEFAULT_GLASS_COUNT; i++) { - strcpy((char*)g_glass_context.catalogs[0].glasses[i].manufacturer, "SCHOTT"); + strncpy((char*)g_glass_context.catalogs[0].glasses[i].manufacturer, "SCHOTT", + sizeof(g_glass_context.catalogs[0].glasses[i].manufacturer) - 1); + g_glass_context.catalogs[0].glasses[i].manufacturer[sizeof(g_glass_context.catalogs[0].glasses[i].manufacturer) - 1] = '\0'; } } else { printf("Critical error: Failed to allocate fallback glass data\n"); diff --git a/src/glamac/glass_data.d b/src/glamac/glass_data.d new file mode 100644 index 0000000..6ecf97d --- /dev/null +++ b/src/glamac/glass_data.d @@ -0,0 +1,7 @@ +glass_data.o glass_data.d: ../src/glamac/glass_data.c \ + ../include/glamac/core/glamacdef.h ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h \ + ../include/glamac/core/glamac_errors.h diff --git a/src/glautils/fgla.c b/src/glautils/fgla.c index baf2fef..c8cd139 100644 --- a/src/glautils/fgla.c +++ b/src/glautils/fgla.c @@ -15,25 +15,26 @@ #include #include #include -#include "glass_data.h" -#include "glamacdef.h" +#include "glamac/data/glass_data.h" +#include "glamac/core/glamacdef.h" +#include "glautils/fgla.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 +// Use constants from header +#define MAX_SEARCH_TERM_LEN FGLA_MAX_SEARCH_TERM_LEN +#define MAX_GLASS_NAME_LEN FGLA_MAX_GLASS_NAME_LEN +#define MAX_CATALOG_COUNT FGLA_MAX_CATALOG_COUNT +#define MAX_NORMALIZED_LEN FGLA_MAX_NORMALIZED_LEN +#define GLASS_CODE_LEN FGLA_GLASS_CODE_LEN -// Output format types -typedef enum { - OUTPUT_CSV = 0, - OUTPUT_TABLE = 1, - OUTPUT_JSON = 2 -} OutputFormat; +// Use output format types from header +typedef FglaOutputFormat OutputFormat; +#define OUTPUT_CSV FGLA_OUTPUT_CSV +#define OUTPUT_TABLE FGLA_OUTPUT_TABLE +#define OUTPUT_JSON FGLA_OUTPUT_JSON // Function to safely convert string to lowercase with length limit -void to_lowercase_safe(char* str, size_t max_len) { +// Functions are now always non-static since they're declared in the header +void fgla_to_lowercase_safe(char* str, size_t max_len) { if (!str) return; for (size_t i = 0; i < max_len && str[i]; i++) { @@ -42,7 +43,7 @@ void to_lowercase_safe(char* str, size_t max_len) { } // 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) { +int fgla_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); @@ -58,7 +59,7 @@ int normalize_string_safe(const char* input, char* output, size_t output_size) { } // Function to safely check if needle is found in haystack (case-insensitive, dash-insensitive) -int contains_substring_safe(const char* haystack, const char* needle) { +int fgla_contains_substring_safe(const char* haystack, const char* needle) { if (!haystack || !needle) return 0; // Validate input lengths @@ -80,8 +81,8 @@ int contains_substring_safe(const char* haystack, const char* needle) { } // 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) { + if (fgla_normalize_string_safe(haystack, haystack_normalized, h_len + 1) != 0 || + fgla_normalize_string_safe(needle, needle_normalized, n_len + 1) != 0) { free(haystack_normalized); free(needle_normalized); return 0; @@ -96,7 +97,7 @@ int contains_substring_safe(const char* haystack, const char* needle) { } // Function to check if manufacturer matches any of the specified catalogs -int matches_catalog(const char* manufacturer, const char* catalog_list[], int catalog_count) { +int fgla_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++) { @@ -109,7 +110,7 @@ int matches_catalog(const char* manufacturer, const char* catalog_list[], int ca } // Function to validate search term input -int validate_search_term(const char* term) { +int fgla_validate_search_term(const char* term) { if (!term) return 0; size_t len = strnlen(term, MAX_SEARCH_TERM_LEN); @@ -130,7 +131,7 @@ int validate_search_term(const char* term) { // 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) { +int fgla_is_glass_code_pattern(const char* term) { if (!term) return 0; size_t len = strnlen(term, GLASS_CODE_LEN + 1); @@ -148,7 +149,7 @@ int is_glass_code_pattern(const char* term) { // 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) { +int fgla_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 @@ -193,7 +194,7 @@ int matches_glass_code_pattern_safe(const char* glass_code, const char* pattern) } } -void print_usage(const char* program_name) { +void fgla_print_usage(const char* program_name) { printf("fgla - Find Glass utility\n"); printf("Usage: %s [OPTIONS] \n", program_name); printf("\n"); @@ -217,16 +218,9 @@ void print_usage(const char* 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; +// Use FglaResult from header -void print_error_with_suggestion(FglaResult error, const char* context) { +void fgla_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 : ""); @@ -323,6 +317,7 @@ void print_footer(OutputFormat format) { } } +#ifndef TEST_BUILD 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 @@ -361,7 +356,7 @@ int main(int argc, char* argv[]) { } if (catalog_count == 0) { fprintf(stderr, "Error: -c option requires at least one catalog name\n"); - print_usage(argv[0]); + fgla_print_usage(argv[0]); return FGLA_ERROR_INVALID_ARGS; } } else if (strcmp(argv[i], "-f") == 0) { @@ -369,7 +364,7 @@ int main(int argc, char* argv[]) { i++; // Skip -f if (i >= argc) { fprintf(stderr, "Error: -f option requires a format argument\n"); - print_usage(argv[0]); + fgla_print_usage(argv[0]); return FGLA_ERROR_INVALID_ARGS; } @@ -386,12 +381,12 @@ int main(int argc, char* argv[]) { i++; } else if (argv[i][0] == '-') { fprintf(stderr, "Error: Unknown option '%s'\n", argv[i]); - print_usage(argv[0]); + fgla_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]); + fgla_print_usage(argv[0]); return FGLA_ERROR_INVALID_ARGS; } search_term = argv[i]; @@ -402,13 +397,13 @@ int main(int argc, char* argv[]) { catalog_list[catalog_count] = NULL; // NULL-terminate the list if (search_term == NULL) { - print_usage(argv[0]); + fgla_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); + if (!fgla_validate_search_term(search_term)) { + fgla_print_error_with_suggestion(FGLA_ERROR_INVALID_SEARCH_TERM, search_term); return FGLA_ERROR_INVALID_SEARCH_TERM; } @@ -431,7 +426,7 @@ int main(int argc, char* argv[]) { } if (!successful_path) { - print_error_with_suggestion(FGLA_ERROR_NO_DATABASE, NULL); + fgla_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]); @@ -447,7 +442,7 @@ int main(int argc, char* argv[]) { u32 found_count = 0; // Determine search type - int is_glass_code_search = is_glass_code_pattern(search_term); + int is_glass_code_search = fgla_is_glass_code_pattern(search_term); // Pre-allocate matches array for performance - estimate max size across all catalogs u32* matching_indices = malloc(10000 * sizeof(u32)); // Generous allocation for all glasses @@ -478,18 +473,18 @@ int main(int argc, char* argv[]) { 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)) { + if (glass_code && fgla_matches_glass_code_pattern_safe(glass_code, search_term)) { matches = 1; } } else { // Name search - if (contains_substring_safe(glass_name, search_term)) { + if (fgla_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)) { + if (matches && fgla_matches_catalog((const char*)glass->manufacturer, catalog_list, catalog_count)) { matching_indices[found_count] = i; matching_catalogs[found_count] = catalog_idx; found_count++; @@ -499,7 +494,7 @@ int main(int argc, char* argv[]) { // Output results if (found_count == 0) { - print_error_with_suggestion(FGLA_ERROR_NO_MATCHES, search_term); + fgla_print_error_with_suggestion(FGLA_ERROR_NO_MATCHES, search_term); free(matching_indices); free(matching_catalogs); cleanup_glass_data(); @@ -536,3 +531,4 @@ int main(int argc, char* argv[]) { return FGLA_SUCCESS; } +#endif /* TEST_BUILD */ diff --git a/src/glautils/fgla.d b/src/glautils/fgla.d new file mode 100644 index 0000000..69b10cd --- /dev/null +++ b/src/glautils/fgla.d @@ -0,0 +1,7 @@ +fgla.o fgla.d: ../src/glautils/fgla.c ../include/glamac/data/glass_data.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/glamac_errors.h \ + ../include/glamac/data/../core/glamacdef.h \ + ../include/glamac/data/../core/security.h \ + ../include/glamac/core/glamacdef.h ../include/glautils/fgla.h \ + ../include/glamac/core/security.h diff --git a/test.txt b/test.txt deleted file mode 100644 index df03815..0000000 --- a/test.txt +++ /dev/null @@ -1,302 +0,0 @@ -Debug mode enabled -Running in debug mode (no GUI) -Trying JSON path: data/json/glasses.json -Loaded 156 glasses from SCHOTT -Loaded 211 glasses from HOYA -Loaded 324 glasses from CDGM -Loaded 136 glasses from Ohara -Using catalog: CDGM -Creating clusters for CDGM catalog (324 glasses)... - → ADDING [2] H-FK61B to TIGHT cluster 1 (distance=0.000000 from [1] H-FK61) - → ADDING [243] D-FK61 to TIGHT cluster 1 (distance=0.000000 from [1] H-FK61) - → ADDING [245] D-FK61A to TIGHT cluster 1 (distance=0.000000 from [1] H-FK61) -Cluster 1 (TIGHT, 4 members): H-FK61, H-FK61B, D-FK61... - → ADDING [247] D-FK95 to TIGHT cluster 3 (distance=0.000000 from [4] H-FK95N) -Cluster 3 (TIGHT, 2 members): H-FK95N, D-FK95 - → ADDING [249] D-QK3L to TIGHT cluster 5 (distance=0.000000 from [6] H-QK3L) - → ADDING [322] FC5 to TIGHT cluster 5 (distance=0.000000 from [6] H-QK3L) -Cluster 5 (TIGHT, 3 members): H-QK3L, D-QK3L, FC5 - → ADDING D-ZK3 to LOOSE cluster (max distance=0.128761) - → ADDING D-ZK3A to LOOSE cluster (max distance=0.095283) - → ADDING D-ZK3A-25 to LOOSE cluster (max distance=0.140006) -Cluster 9 (LOOSE, 4 members): K4A, D-ZK3, D-ZK3A... - → ADDING H-ZPK1A to LOOSE cluster (max distance=0.112022) - → ADDING H-BaK4 to LOOSE cluster (max distance=0.068505) - → ADDING D-K59 to LOOSE cluster (max distance=0.148930) -Cluster 10 (LOOSE, 4 members): H-K5, H-ZPK1A, H-BaK4... - → ADDING H-BaK1 to LOOSE cluster (max distance=0.021614) - → ADDING H-ZK7A to LOOSE cluster (max distance=0.142820) -Cluster 11 (LOOSE, 3 members): H-K6, H-BaK1, H-ZK7A - → ADDING H-BaK6 to LOOSE cluster (max distance=0.138961) - → REJECTING H-ZK7 from LOOSE cluster (distance=0.186602 from H-BaK6 > 0.150000) - → REJECTING H-ZK14 from LOOSE cluster (distance=0.164739 from H-BaK6 > 0.150000) - → ADDING D-ZK3L to LOOSE cluster (max distance=0.104003) - → ADDING D-ZK3L-25 to LOOSE cluster (max distance=0.142583) -Cluster 12 (LOOSE, 4 members): H-K7, H-BaK6, D-ZK3L... - → ADDING H-ZK10 to LOOSE cluster (max distance=0.132866) -Cluster 13 (LOOSE, 2 members): H-K8, H-ZK10 - → ADDING [16] H-K9LGT to TIGHT cluster 14 (distance=0.000000 from [15] H-K9L) - → ADDING [17] H-K9L* to TIGHT cluster 14 (distance=0.000000 from [15] H-K9L) - → ADDING [18] H-K9LA to TIGHT cluster 14 (distance=0.000000 from [15] H-K9L) -Cluster 14 (TIGHT, 4 members): H-K9L, H-K9LGT, H-K9L*... - → ADDING H-ZK4 to LOOSE cluster (max distance=0.127726) -Cluster 15 (LOOSE, 2 members): H-K10, H-ZK4 - → ADDING H-LaK4L to LOOSE cluster (max distance=0.120456) -Cluster 16 (LOOSE, 2 members): H-K11, H-LaK4L - → ADDING H-ZK11 to LOOSE cluster (max distance=0.106839) -Cluster 17 (LOOSE, 2 members): H-K12, H-ZK11 - → ADDING H-ZK2 to LOOSE cluster (max distance=0.063853) - → ADDING D-ZK2 to LOOSE cluster (max distance=0.116948) - → REJECTING D-ZK2A from LOOSE cluster (distance=0.160000 from D-ZK2 > 0.150000) - → ADDING D-ZK2A-25 to LOOSE cluster (max distance=0.070005) -Cluster 18 (LOOSE, 4 members): H-K50, H-ZK2, D-ZK2... - → ADDING H-ZK6 to LOOSE cluster (max distance=0.107874) -Cluster 19 (LOOSE, 2 members): H-K51, H-ZK6 - → ADDING D-ZPK3 to LOOSE cluster (max distance=0.010027) - → ADDING D-ZPK3-25 to LOOSE cluster (max distance=0.070006) -Cluster 21 (LOOSE, 3 members): H-ZPK3, D-ZPK3, D-ZPK3-25 - → ADDING [263] D-ZPK5 to TIGHT cluster 22 (distance=0.000000 from [27] H-ZPK5) -Cluster 22 (TIGHT, 2 members): H-ZPK5, D-ZPK5 - → ADDING [265] D-ZPK7 to TIGHT cluster 23 (distance=0.000000 from [28] H-ZPK7) -Cluster 23 (TIGHT, 2 members): H-ZPK7, D-ZPK7 - → ADDING D-ZK2L to LOOSE cluster (max distance=0.128893) -Cluster 24 (LOOSE, 2 members): H-BaK2, D-ZK2L - → ADDING H-LaK50A to LOOSE cluster (max distance=0.108926) - → REJECTING D-ZK21 from LOOSE cluster (distance=0.191931 from H-LaK50A > 0.150000) -Cluster 26 (LOOSE, 2 members): H-BaK5, H-LaK50A - → ADDING [36] H-BaK7GT to TIGHT cluster 27 (distance=0.000000 from [35] H-BaK7) -Cluster 27 (TIGHT, 2 members): H-BaK7, H-BaK7GT - → ADDING H-LaK12 to LOOSE cluster (max distance=0.141338) -Cluster 28 (LOOSE, 2 members): H-BaK7A, H-LaK12 - → ADDING [42] H-ZK3A to TIGHT cluster 31 (distance=0.000000 from [41] H-ZK3) -Cluster 31 (TIGHT, 2 members): H-ZK3, H-ZK3A - → ADDING H-LaK10 to LOOSE cluster (max distance=0.126477) -Cluster 32 (LOOSE, 2 members): H-ZK5, H-LaK10 - → ADDING H-ZK14 to LOOSE cluster (max distance=0.022349) -Cluster 33 (LOOSE, 2 members): H-ZK7, H-ZK14 - → ADDING [50] H-ZK9A to TIGHT cluster 35 (distance=0.000000 from [49] H-ZK9B) -Cluster 35 (TIGHT, 2 members): H-ZK9B, H-ZK9A - → ADDING H-LaK7A to LOOSE cluster (max distance=0.124809) - → ADDING D-LaK52 to LOOSE cluster (max distance=0.117204) - → ADDING D-LaK52-25 to LOOSE cluster (max distance=0.127095) -Cluster 37 (LOOSE, 4 members): H-ZK20, H-LaK7A, D-LaK52... - → ADDING D-ZK21 to LOOSE cluster (max distance=0.090012) - → ADDING [280] D-ZK21-25 to TIGHT cluster 38 (distance=0.000470 from [56] H-ZK21) -Cluster 38 (TIGHT, 3 members): H-ZK21, D-ZK21, D-ZK21-25 - → ADDING [58] H-ZK50GT to TIGHT cluster 39 (distance=0.000000 from [57] H-ZK50) -Cluster 39 (TIGHT, 2 members): H-ZK50, H-ZK50GT - → ADDING H-LaK11 to LOOSE cluster (max distance=0.075208) - → ADDING H-LaK52 to LOOSE cluster (max distance=0.144819) -Cluster 41 (LOOSE, 3 members): H-LaK2A, H-LaK11, H-LaK52 - → ADDING H-ZBaF50 to LOOSE cluster (max distance=0.133533) -Cluster 42 (LOOSE, 2 members): H-LaK3, H-ZBaF50 - → ADDING H-LaK51A to LOOSE cluster (max distance=0.021382) - → ADDING D-LaK70 to LOOSE cluster (max distance=0.142713) -Cluster 43 (LOOSE, 3 members): H-LaK5A, H-LaK51A, D-LaK70 - → ADDING H-LaK8A to LOOSE cluster (max distance=0.060001) -Cluster 45 (LOOSE, 2 members): H-LaK8B, H-LaK8A - → ADDING [75] H-LaK53A to TIGHT cluster 46 (distance=0.000000 from [74] H-LaK53B) -Cluster 46 (TIGHT, 2 members): H-LaK53B, H-LaK53A - → ADDING D-LaK5 to LOOSE cluster (max distance=0.071215) - → ADDING D-LaK5-25 to LOOSE cluster (max distance=0.080005) -Cluster 48 (LOOSE, 3 members): H-LaK59A, D-LaK5, D-LaK5-25 - → ADDING H-LaK72 to LOOSE cluster (max distance=0.082152) -Cluster 50 (LOOSE, 2 members): H-LaK67, H-LaK72 - → ADDING QF1 to LOOSE cluster (max distance=0.049999) -Cluster 54 (LOOSE, 2 members): H-QF1, QF1 - → ADDING ZBaF17 to LOOSE cluster (max distance=0.131502) -Cluster 57 (LOOSE, 2 members): QF5, ZBaF17 - → ADDING QF6 to LOOSE cluster (max distance=0.080002) -Cluster 58 (LOOSE, 2 members): H-QF6A, QF6 - → ADDING QF8 to LOOSE cluster (max distance=0.029999) - → REJECTING H-ZBaF5 from LOOSE cluster (distance=0.158369 from QF8 > 0.150000) - → ADDING H-ZBaF52 to LOOSE cluster (max distance=0.132744) -Cluster 59 (LOOSE, 3 members): H-QF8, QF8, H-ZBaF52 - → ADDING H-QF50A to LOOSE cluster (max distance=0.139999) - → ADDING [96] QF50 to TIGHT cluster 61 (distance=0.000000 from [94] H-QF50) -Cluster 61 (TIGHT, 3 members): H-QF50, H-QF50A, QF50 - → ADDING [99] F1 to TIGHT cluster 63 (distance=0.000000 from [98] H-F1) -Cluster 63 (TIGHT, 2 members): H-F1, F1 - → ADDING F2 to LOOSE cluster (max distance=0.040001) -Cluster 64 (LOOSE, 2 members): H-F2, F2 - → ADDING F3 to LOOSE cluster (max distance=0.040001) -Cluster 65 (LOOSE, 2 members): H-F3, F3 - → ADDING [105] F4 to TIGHT cluster 66 (distance=0.000000 from [104] H-F4) -Cluster 66 (TIGHT, 2 members): H-F4, F4 - → ADDING [107] F5 to TIGHT cluster 67 (distance=0.000000 from [106] H-F5) -Cluster 67 (TIGHT, 2 members): H-F5, F5 - → ADDING F6 to LOOSE cluster (max distance=0.020000) - → ADDING H-F13 to LOOSE cluster (max distance=0.130004) - → ADDING F13 to LOOSE cluster (max distance=0.130004) -Cluster 68 (LOOSE, 4 members): H-F6, F6, H-F13... - → ADDING H-ZBaF4 to LOOSE cluster (max distance=0.132961) -Cluster 69 (LOOSE, 2 members): F7, H-ZBaF4 - → ADDING H-F52 to LOOSE cluster (max distance=0.142384) -Cluster 70 (LOOSE, 2 members): H-F51, H-F52 - → ADDING BaF4 to LOOSE cluster (max distance=0.129997) -Cluster 73 (LOOSE, 2 members): H-BaF4, BaF4 - → ADDING BaF7 to LOOSE cluster (max distance=0.039997) -Cluster 76 (LOOSE, 2 members): H-BaF7, BaF7 - → ADDING D-LaK6 to LOOSE cluster (max distance=0.081962) - → ADDING D-LaK6-25 to LOOSE cluster (max distance=0.139181) -Cluster 78 (LOOSE, 3 members): H-ZBaF1, D-LaK6, D-LaK6-25 ->>> Starting cluster 79 with glass [125] ZBaF2 (nd=1.63962, vd=48.27) - → ADDING H-ZF1A to LOOSE cluster (max distance=0.049999) - → ADDING [138] ZF1 to TIGHT cluster 86 (distance=0.000000 from [136] H-ZF1) -Cluster 86 (TIGHT, 3 members): H-ZF1, H-ZF1A, ZF1 - → ADDING [140] ZF2 to TIGHT cluster 87 (distance=0.000000 from [139] H-ZF2) -Cluster 87 (TIGHT, 2 members): H-ZF2, ZF2 - → ADDING [142] ZF3 to TIGHT cluster 88 (distance=0.000000 from [141] H-ZF3) -Cluster 88 (TIGHT, 2 members): H-ZF3, ZF3 - → ADDING [144] H-ZF4AGT to TIGHT cluster 89 (distance=0.000000 from [143] H-ZF4A) - → ADDING [145] ZF4 to TIGHT cluster 89 (distance=0.000000 from [143] H-ZF4A) -Cluster 89 (TIGHT, 3 members): H-ZF4A, H-ZF4AGT, ZF4 - → ADDING ZF5 to LOOSE cluster (max distance=0.059999) -Cluster 90 (LOOSE, 2 members): H-ZF5, ZF5 - → ADDING [149] ZF6 to TIGHT cluster 91 (distance=0.000000 from [148] H-ZF6) -Cluster 91 (TIGHT, 2 members): H-ZF6, ZF6 - → ADDING [151] H-ZF7LAGT to TIGHT cluster 92 (distance=0.000000 from [150] H-ZF7LA) - → ADDING [153] ZF7L to TIGHT cluster 92 (distance=0.000000 from [150] H-ZF7LA) - → ADDING [154] ZF7LGT to TIGHT cluster 92 (distance=0.000000 from [150] H-ZF7LA) -Cluster 92 (TIGHT, 4 members): H-ZF7LA, H-ZF7LAGT, ZF7L... - → ADDING ZF7LTT to LOOSE cluster (max distance=0.090005) -Cluster 93 (LOOSE, 2 members): ZF7, ZF7LTT - → ADDING ZF10 to LOOSE cluster (max distance=0.020000) - → ADDING D-ZF10 to LOOSE cluster (max distance=0.100000) - → ADDING D-ZF10-25 to LOOSE cluster (max distance=0.100014) -Cluster 95 (LOOSE, 4 members): H-ZF10, ZF10, D-ZF10... - → ADDING ZF11 to LOOSE cluster (max distance=0.020000) -Cluster 96 (LOOSE, 2 members): H-ZF11, ZF11 - → ADDING ZF12 to LOOSE cluster (max distance=0.060001) -Cluster 97 (LOOSE, 2 members): H-ZF12, ZF12 - → ADDING [164] H-ZF13GT to TIGHT cluster 98 (distance=0.000000 from [163] H-ZF13) -Cluster 98 (TIGHT, 2 members): H-ZF13, H-ZF13GT - → ADDING [168] ZF50 to TIGHT cluster 101 (distance=0.000000 from [167] H-ZF50) -Cluster 101 (TIGHT, 2 members): H-ZF50, ZF50 - → ADDING [171] H-ZF52GT to TIGHT cluster 103 (distance=0.000000 from [170] H-ZF52) - → ADDING [172] H-ZF52TT to TIGHT cluster 103 (distance=0.000000 from [170] H-ZF52) - → ADDING [173] H-ZF52A to TIGHT cluster 103 (distance=0.000000 from [170] H-ZF52) -Cluster 103 (TIGHT, 4 members): H-ZF52, H-ZF52GT, H-ZF52TT... - → ADDING [176] H-ZF62GT to TIGHT cluster 105 (distance=0.000000 from [175] H-ZF62) -Cluster 105 (TIGHT, 2 members): H-ZF62, H-ZF62GT - → ADDING [178] H-ZF71GT to TIGHT cluster 106 (distance=0.000000 from [177] H-ZF71) -Cluster 106 (TIGHT, 2 members): H-ZF71, H-ZF71GT - → ADDING [180] H-ZF72AGT to TIGHT cluster 107 (distance=0.000000 from [179] H-ZF72A) -Cluster 107 (TIGHT, 2 members): H-ZF72A, H-ZF72AGT - → ADDING [182] H-ZF73GT to TIGHT cluster 108 (distance=0.000000 from [181] H-ZF73) -Cluster 108 (TIGHT, 2 members): H-ZF73, H-ZF73GT - → ADDING [184] H-ZF88GT to TIGHT cluster 109 (distance=0.000000 from [183] H-ZF88) -Cluster 109 (TIGHT, 2 members): H-ZF88, H-ZF88GT - → ADDING H-LaF53 to LOOSE cluster (max distance=0.058037) - → ADDING D-LaF050 to LOOSE cluster (max distance=0.117026) - → ADDING D-LaF050-25 to LOOSE cluster (max distance=0.080009) -Cluster 110 (LOOSE, 4 members): H-LaF1, H-LaF53, D-LaF050... - → ADDING [189] H-LaF4GT to TIGHT cluster 113 (distance=0.000000 from [188] H-LaF4) -Cluster 113 (TIGHT, 2 members): H-LaF4, H-LaF4GT - → ADDING H-ZLaF53B to LOOSE cluster (max distance=0.130865) - → ADDING H-ZLaF53BGT to LOOSE cluster (max distance=0.130865) - → REJECTING H-ZLaF78B from LOOSE cluster (distance=0.173342 from H-ZLaF53B > 0.150000) - → ADDING D-ZLaF67-25 to LOOSE cluster (max distance=0.141987) -Cluster 116 (LOOSE, 4 members): H-LaF7, H-ZLaF53B, H-ZLaF53BGT... - → ADDING D-LaF50 to LOOSE cluster (max distance=0.010110) - → ADDING D-LaF50-25 to LOOSE cluster (max distance=0.079999) -Cluster 118 (LOOSE, 3 members): H-LaF50B, D-LaF50, D-LaF50-25 - → ADDING H-ZLaF1 to LOOSE cluster (max distance=0.071752) -Cluster 120 (LOOSE, 2 members): H-LaF52, H-ZLaF1 - → ADDING H-ZLaF73 to LOOSE cluster (max distance=0.129077) - → ADDING D-ZLaF85A to LOOSE cluster (max distance=0.090220) - → ADDING D-ZLaF85A-25 to LOOSE cluster (max distance=0.104373) -Cluster 122 (LOOSE, 4 members): H-LaF55, H-ZLaF73, D-ZLaF85A... - → ADDING H-ZLaF4LB to LOOSE cluster (max distance=0.029999) -Cluster 126 (LOOSE, 2 members): H-ZLaF4LA, H-ZLaF4LB - → ADDING H-ZLaF50D to LOOSE cluster (max distance=0.010002) - → ADDING H-ZLaF69 to LOOSE cluster (max distance=0.032313) - → ADDING H-ZLaF69A to LOOSE cluster (max distance=0.023324) -Cluster 127 (LOOSE, 4 members): H-ZLaF50E, H-ZLaF50D, H-ZLaF69... - → ADDING H-ZLaF52 to LOOSE cluster (max distance=0.070000) - → ADDING D-ZLaF52LA to LOOSE cluster (max distance=0.040191) - → ADDING D-ZLaF52LA-25 to LOOSE cluster (max distance=0.050073) -Cluster 129 (LOOSE, 4 members): H-ZLaF52A, H-ZLaF52, D-ZLaF52LA... - → ADDING H-ZLaF55C to LOOSE cluster (max distance=0.020000) - → ADDING D-ZLaF61 to LOOSE cluster (max distance=0.023533) - → ADDING D-ZLaF61-25 to LOOSE cluster (max distance=0.061501) -Cluster 130 (LOOSE, 4 members): H-ZLaF55D, H-ZLaF55C, D-ZLaF61... - → ADDING [217] H-ZLaF66GT to TIGHT cluster 132 (distance=0.000000 from [216] H-ZLaF66) -Cluster 132 (TIGHT, 2 members): H-ZLaF66, H-ZLaF66GT - → ADDING H-ZLaF68B to LOOSE cluster (max distance=0.059998) - → REJECTING D-ZLaF81-25 from LOOSE cluster (distance=0.195166 from H-ZLaF68B > 0.150000) -Cluster 133 (LOOSE, 2 members): H-ZLaF68C, H-ZLaF68B - → ADDING H-ZLaF71AGT to LOOSE cluster (max distance=0.079998) - → ADDING H-ZLaF89L to LOOSE cluster (max distance=0.137150) - → ADDING H-ZLaF89LA to LOOSE cluster (max distance=0.119624) -Cluster 135 (LOOSE, 4 members): H-ZLaF71, H-ZLaF71AGT, H-ZLaF89L... - → ADDING H-ZLaF75B to LOOSE cluster (max distance=0.100000) - → ADDING H-ZLaF75C to LOOSE cluster (max distance=0.059999) -Cluster 136 (LOOSE, 3 members): H-ZLaF75A, H-ZLaF75B, H-ZLaF75C - → ADDING H-ZLaF76A to LOOSE cluster (max distance=0.049999) -Cluster 137 (LOOSE, 2 members): H-ZLaF76, H-ZLaF76A - → ADDING H-ZLaF90A to LOOSE cluster (max distance=0.029999) -Cluster 139 (LOOSE, 2 members): H-ZLaF90, H-ZLaF90A - → ADDING H-ZLaF92A to LOOSE cluster (max distance=0.039999) -Cluster 141 (LOOSE, 2 members): H-ZLaF92, H-ZLaF92A - → ADDING TF3 to LOOSE cluster (max distance=0.020024) -Cluster 143 (LOOSE, 2 members): H-TF3L, TF3 - → ADDING D-FK61A-25 to LOOSE cluster (max distance=0.020005) - → ADDING S-FPL51 to LOOSE cluster (max distance=0.070001) -Cluster 145 (LOOSE, 3 members): D-FK61-25, D-FK61A-25, S-FPL51 - → ADDING D-PK3 to LOOSE cluster (max distance=0.062876) -Cluster 147 (LOOSE, 2 members): D-QK3L-25, D-PK3 - → ADDING D-K9-25 to LOOSE cluster (max distance=0.069998) - → ADDING [253] D-K9GT to TIGHT cluster 148 (distance=0.000000 from [251] D-K9) -Cluster 148 (TIGHT, 3 members): D-K9, D-K9-25, D-K9GT - → ADDING D-ZPK1A-25 to LOOSE cluster (max distance=0.070009) -Cluster 152 (LOOSE, 2 members): D-ZPK1A, D-ZPK1A-25 - → ADDING D-ZK2L-25 to LOOSE cluster (max distance=0.020242) -Cluster 156 (LOOSE, 2 members): D-ZK2A, D-ZK2L-25 - → ADDING D-ZK79-25 to LOOSE cluster (max distance=0.080006) -Cluster 158 (LOOSE, 2 members): D-ZK79, D-ZK79-25 - → ADDING D-ZF93-25 to LOOSE cluster (max distance=0.010179) -Cluster 160 (LOOSE, 2 members): D-ZF93, D-ZF93-25 - → ADDING D-LaF53-25 to LOOSE cluster (max distance=0.080011) -Cluster 161 (LOOSE, 2 members): D-LaF53, D-LaF53-25 - → ADDING D-LaF79-25 to LOOSE cluster (max distance=0.010040) - → ADDING D-ZLaF85L to LOOSE cluster (max distance=0.147434) - → ADDING D-ZLaF85L-25 to LOOSE cluster (max distance=0.126353) -Cluster 162 (LOOSE, 4 members): D-LaF79, D-LaF79-25, D-ZLaF85L... - → ADDING D-ZLaF50-25 to LOOSE cluster (max distance=0.040017) -Cluster 163 (LOOSE, 2 members): D-ZLaF50, D-ZLaF50-25 - → ADDING D-ZLaF81-25 to LOOSE cluster (max distance=0.030024) -Cluster 165 (LOOSE, 2 members): D-ZLaF81, D-ZLaF81-25 - → ADDING D-ZLaF85LN-25 to LOOSE cluster (max distance=0.020033) -Cluster 166 (LOOSE, 2 members): D-ZLaF85LN, D-ZLaF85LN-25 - → ADDING D-ZLaF85LS-25 to LOOSE cluster (max distance=0.030018) -Cluster 167 (LOOSE, 2 members): D-ZLaF85LS, D-ZLaF85LS-25 -Created 33 tight clusters, 68 loose clusters (169 total) - -Final cluster assignments: ->>> H-ZK2 is in cluster 18 (LOOSE, 4 members) ->>> ZBaF2 is in cluster 79 (LOOSE, 1 members) -Glass data loaded: 324 glasses -Created 169 clusters -Glass 0: H-FK55 (nd=1.5502, vd=75.23, mfg=CDGM) -Glass 1: H-FK61 (nd=1.4970, vd=81.61, mfg=CDGM) -Glass 2: H-FK61B (nd=1.4970, vd=81.61, mfg=CDGM) -Glass 3: H-FK71 (nd=1.4565, vd=90.27, mfg=CDGM) -Glass 4: H-FK95N (nd=1.4378, vd=94.52, mfg=CDGM) -Glass 5: H-QK1 (nd=1.4705, vd=66.88, mfg=CDGM) -Glass 6: H-QK3L (nd=1.4875, vd=70.44, mfg=CDGM) -Glass 7: H-K1 (nd=1.4997, vd=62.07, mfg=CDGM) -Glass 8: H-K2 (nd=1.5005, vd=66.02, mfg=CDGM) -Glass 9: H-K3 (nd=1.5046, vd=64.72, mfg=CDGM) - -Checking problematic glass indices: -Glass [40]: H-ZK2 (nd=1.58313, vd=59.46) -Glass [125]: ZBaF2 (nd=1.63962, vd=48.27) - -Testing collision detection... -Rect1 vs Rect2 overlap: YES -Rect1 vs Rect3 overlap: NO -Label collision with point test: -Label at point collision: YES -Label far from point collision: NO -Debug mode complete diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..84e64dc --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,169 @@ +# Test Makefile for GlaMaC +# Builds and runs unit tests and integration tests + +# Include common build configuration +include ../build/common.mk + +# Test-specific directories +TESTDIR := . +UNIT_TESTDIR := $(TESTDIR)/unit +INTEGRATION_TESTDIR := $(TESTDIR)/integration +TEST_BINDIR := $(TESTDIR)/bin + +# Test source files +UNIT_TESTS := $(wildcard $(UNIT_TESTDIR)/*.c) +INTEGRATION_TESTS := $(wildcard $(INTEGRATION_TESTDIR)/*.c) + +# Test binaries (specific ones we have rules for) +UNIT_TEST_BINS := $(TEST_BINDIR)/test_glass_data $(TEST_BINDIR)/test_fgla $(TEST_BINDIR)/test_glamac_view +INTEGRATION_TEST_BINS := $(TEST_BINDIR)/test_simple_pipeline + +# Glass data dependencies (needed for tests) +GLASS_DATA_DEPS := ../src/glamac/glass_data.c ../src/glamac/glamac_errors.c + +# View system dependencies +VIEW_DEPS := ../src/glamac/glamac_view.c ../src/glamac/glamac_errors.c + +# Test-specific compiler flags +TEST_CFLAGS := $(CFLAGS_NATIVE) -I$(TESTDIR) -DTEST_BUILD + +# Default target +all: unit-tests integration-tests + +# Create test bin directory +$(TEST_BINDIR): + $(MKDIR) $(TEST_BINDIR) + +# Unit test rules +unit-tests: $(UNIT_TEST_BINS) + +# Unit test for glass_data +$(TEST_BINDIR)/test_glass_data: $(UNIT_TESTDIR)/test_glass_data.c $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Unit test for fgla (needs fgla.c for internal function testing) +$(TEST_BINDIR)/test_fgla: $(UNIT_TESTDIR)/test_fgla.c ../src/glautils/fgla.c $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Unit test for glamac_view (simplified version) +$(TEST_BINDIR)/test_glamac_view: $(UNIT_TESTDIR)/test_glamac_view_simple.c $(VIEW_DEPS) $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Integration tests +integration-tests: $(INTEGRATION_TEST_BINS) + +# Integration test for simple pipeline +$(TEST_BINDIR)/test_simple_pipeline: $(INTEGRATION_TESTDIR)/test_simple_pipeline.c ../src/glautils/fgla.c $(VIEW_DEPS) $(GLASS_DATA_DEPS) | $(TEST_BINDIR) + $(CC) $^ $(TEST_CFLAGS) -o $@ + +# Run all tests +test: unit-tests integration-tests + @echo "" + @echo "$(CYAN)Running Unit Tests$(RESET)" + @echo "==================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + @echo "" + @echo "$(CYAN)Running Integration Tests$(RESET)" + @echo "=========================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run only unit tests +test-unit: unit-tests + @echo "$(CYAN)Running Unit Tests$(RESET)" + @echo "==================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run only integration tests +test-integration: integration-tests + @echo "$(CYAN)Running Integration Tests$(RESET)" + @echo "=========================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test; \ + echo ""; \ + fi; \ + done + +# Run specific test +test-%: $(TEST_BINDIR)/% + @echo "$(CYAN)Running specific test: $*$(RESET)" + @./$(TEST_BINDIR)/$* + +# Clean test binaries +clean: + $(RMDIR) $(TEST_BINDIR) 2>/dev/null || true + +# Verbose test runs (for debugging) +test-verbose: SHELL:=/bin/bash +test-verbose: unit-tests integration-tests + @echo "" + @echo "$(CYAN)Running Unit Tests (Verbose)$(RESET)" + @echo "============================" + @for test in $(UNIT_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test 2>&1 | tee "$$test.log"; \ + echo ""; \ + fi; \ + done + @echo "" + @echo "$(CYAN)Running Integration Tests (Verbose)$(RESET)" + @echo "==================================" + @for test in $(INTEGRATION_TEST_BINS); do \ + if [ -f "$$test" ]; then \ + echo "$(BLUE)Running $$test$(RESET)"; \ + ./$$test 2>&1 | tee "$$test.log"; \ + echo ""; \ + fi; \ + done + +# Help +help: + @echo "GlaMaC Test System" + @echo "" + @echo "Targets:" + @echo " all - Build all tests" + @echo " unit-tests - Build unit tests" + @echo " integration-tests- Build integration tests" + @echo " test - Build and run all tests" + @echo " test-unit - Build and run unit tests only" + @echo " test-integration - Build and run integration tests only" + @echo " test- - Run specific test by name" + @echo " test-verbose - Run all tests with verbose output" + @echo " clean - Clean test binaries" + @echo " help - Show this help" + @echo "" + @echo "Examples:" + @echo " make test # Run all tests" + @echo " make test-unit # Run only unit tests" + @echo " make test-test_glass_data # Run specific test" + +# Color definitions for use in shell commands +export RESET := \033[0m +export RED := \033[31m +export GREEN := \033[32m +export YELLOW := \033[33m +export BLUE := \033[34m +export MAGENTA := \033[35m +export CYAN := \033[36m + +.PHONY: all unit-tests integration-tests test test-unit test-integration clean help test-verbose \ No newline at end of file diff --git a/tests/data/test_glasses.json b/tests/data/test_glasses.json new file mode 100644 index 0000000..ba707aa --- /dev/null +++ b/tests/data/test_glasses.json @@ -0,0 +1,47 @@ +{ + "manufacturers": { + "SCHOTT": { + "glasses": [ + { + "name": "N-BK7", + "nd": 1.5168, + "vd": 64.17, + "glass_code": "517642", + "manufacturer": "SCHOTT" + }, + { + "name": "SF10", + "nd": 1.7283, + "vd": 28.41, + "glass_code": "728284", + "manufacturer": "SCHOTT" + }, + { + "name": "FK51A", + "nd": 1.4866, + "vd": 81.61, + "glass_code": "486816", + "manufacturer": "SCHOTT" + } + ] + }, + "HOYA": { + "glasses": [ + { + "name": "FCD1", + "nd": 1.4970, + "vd": 81.54, + "glass_code": "497815", + "manufacturer": "HOYA" + }, + { + "name": "LAC7", + "nd": 1.6510, + "vd": 58.52, + "glass_code": "651585", + "manufacturer": "HOYA" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fgla_test_internals.h b/tests/fgla_test_internals.h new file mode 100644 index 0000000..6e019c7 --- /dev/null +++ b/tests/fgla_test_internals.h @@ -0,0 +1,28 @@ +/** + * fgla_test_internals.h - Internal function declarations for testing + * + * This header exposes internal fgla functions for unit testing purposes. + * These functions should not be used in production code. + */ + +#ifndef FGLA_TEST_INTERNALS_H +#define FGLA_TEST_INTERNALS_H + +#include "../include/glautils/fgla.h" + +#ifdef TEST_BUILD + +// Re-declare internal functions as extern for testing +// These functions are normally static in fgla.c + +extern void to_lowercase_safe(char* str, size_t max_len); +extern int normalize_string_safe(const char* input, char* output, size_t output_size); +extern int contains_substring_safe(const char* haystack, const char* needle); +extern int matches_catalog(const char* manufacturer, const char* catalog_list[], int catalog_count); +extern int validate_search_term(const char* term); +extern int is_glass_code_pattern(const char* term); +extern int matches_glass_code_pattern_safe(const char* glass_code, const char* pattern); + +#endif /* TEST_BUILD */ + +#endif /* FGLA_TEST_INTERNALS_H */ \ No newline at end of file diff --git a/tests/integration/test_full_pipeline.c b/tests/integration/test_full_pipeline.c new file mode 100644 index 0000000..1bd9296 --- /dev/null +++ b/tests/integration/test_full_pipeline.c @@ -0,0 +1,273 @@ +/** + * test_full_pipeline.c - Integration tests for the full GlaMaC pipeline + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_view.h" +#include "../../include/glautils/fgla.h" + +// Test complete glass data loading and processing pipeline +int test_glass_data_pipeline() { + TEST_START("Glass Data Pipeline"); + + // Initialize glass data system + initialize_glass_data(); + + // Try to load JSON data + b32 json_loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (json_loaded) { + // Verify we have multiple catalogs + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have loaded multiple catalogs"); + + // Test each catalog + for (u32 i = 0; i < catalog_count; i++) { + set_current_catalog(i); + + const char* catalog_name = get_catalog_name(i); + TEST_ASSERT_NOT_NULL(catalog_name, "Each catalog should have a name"); + + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Each catalog should have glasses"); + + // Test glass properties + for (u32 j = 0; j < glass_count; j++) { + const Glass* glass = get_glass(j); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get each glass"); + + // Validate glass data + TEST_ASSERT(glass->abbeNumber > 10.0f && glass->abbeNumber < 100.0f, + "Abbe number should be in reasonable range"); + TEST_ASSERT(glass->refractiveIndex > 1.0f && glass->refractiveIndex < 4.0f, + "Refractive index should be in reasonable range"); + + const byte* name = get_glass_name(j); + TEST_ASSERT_NOT_NULL(name, "Each glass should have a name"); + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + } + } else { + printf(YELLOW " Info: JSON test data not available - testing with default data" RESET "\n"); + + // Test with default data + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Should have default glasses"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view system with real glass data +int test_view_with_glass_data() { + TEST_START("View System with Glass Data"); + + // Initialize systems + initialize_glass_data(); + ViewState view; + init_view(&view, 1024, 768); + + // Load test data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Test view fitting with real data + fit_view_to_data(&view); + + // After fitting, we should be able to see all glasses + TEST_ASSERT(view.zoom > 0.0f, "Zoom should be positive after fitting"); + TEST_ASSERT(view.zoom <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test coordinate transformations with actual glass data + u32 glass_count = get_glass_count(); + if (glass_count > 0) { + const Glass* glass = get_glass(0); + i32 screenX, screenY; + glass_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, &view, &screenX, &screenY); + + // Screen coordinates should be reasonable + TEST_ASSERT(screenX >= -100 && screenX <= view.windowWidth + 100, + "Screen X should be near window bounds"); + TEST_ASSERT(screenY >= -100 && screenY <= view.windowHeight + 100, + "Screen Y should be near window bounds"); + + // Test inverse transformation + f32 recovered_abbe, recovered_ri; + screen_to_glass_coords(screenX, screenY, &view, &recovered_abbe, &recovered_ri); + + TEST_ASSERT_FLOAT_EQ(glass->abbeNumber, recovered_abbe, 0.1f, + "Should recover original Abbe number"); + TEST_ASSERT_FLOAT_EQ(glass->refractiveIndex, recovered_ri, 0.001f, + "Should recover original refractive index"); + } + + // Test clustering with real data + update_clustering(&view); + TEST_ASSERT(view.clusterCount >= 0, "Clustering should complete successfully"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test FGLA search functionality with actual data +int test_fgla_search_integration() { + TEST_START("FGLA Search Integration"); + + // Test various search functions that would be used in the real application + + // Test glass code pattern matching with realistic codes + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Should match exact glass code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642123", "517642"), + "Should match first 6 digits of longer code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Should match wildcard pattern"); + + // Test realistic search terms + TEST_ASSERT(fgla_validate_search_term("N-BK7"), "N-BK7 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("SF10"), "SF10 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("FCD1"), "FCD1 should be valid search term"); + + // Test substring matching with realistic glass names + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find BK in N-BK7"); + TEST_ASSERT(fgla_contains_substring_safe("SF10", "sf"), + "Should find sf in SF10 (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("FCD1", "fcd"), + "Should find fcd in FCD1 (case insensitive)"); + + // Test catalog matching with real manufacturers + const char* real_catalogs[] = {"SCHOTT", "HOYA", "CDGM", "Ohara"}; + TEST_ASSERT(fgla_matches_catalog("SCHOTT", real_catalogs, 4), + "Should match SCHOTT catalog"); + TEST_ASSERT(fgla_matches_catalog("hoya", real_catalogs, 4), + "Should match HOYA catalog (case insensitive)"); + TEST_ASSERT(!fgla_matches_catalog("UNKNOWN", real_catalogs, 4), + "Should not match unknown manufacturer"); + + TEST_END(); +} + +// Test error handling across systems +int test_error_handling_integration() { + TEST_START("Error Handling Integration"); + + // Test glass data error handling + b32 result = load_glasses_from_json((const byte*)"nonexistent.json", NULL); + TEST_ASSERT(!result, "Should fail gracefully with non-existent file"); + + result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!result, "Should fail gracefully with NULL path"); + + // Test view system with invalid parameters + ViewState view; + init_view(&view, 0, 0); // Invalid dimensions + TEST_ASSERT(view.windowWidth > 0, "Should handle invalid width gracefully"); + TEST_ASSERT(view.windowHeight > 0, "Should handle invalid height gracefully"); + + // Test coordinate transformations with extreme values + i32 screenX, screenY; + glass_to_screen_coords(1000.0f, 10.0f, &view, &screenX, &screenY); // Extreme values + // Should not crash - exact values depend on implementation + + // Test FGLA with invalid inputs + TEST_ASSERT(!fgla_validate_search_term(NULL), "Should reject NULL search term"); + TEST_ASSERT(!fgla_validate_search_term(""), "Should reject empty search term"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "Should reject NULL glass code pattern"); + + TEST_END(); +} + +// Test memory management across systems +int test_memory_management() { + TEST_START("Memory Management"); + + // Test multiple initialize/cleanup cycles + for (int i = 0; i < 5; i++) { + initialize_glass_data(); + + // Load data + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Use the data + u32 count = get_glass_count(); + if (count > 0) { + const Glass* glass = get_glass(0); + (void)glass; // Suppress unused variable warning + } + + // Cleanup + cleanup_glass_data(); + } + + // After all cycles, we should be back to clean state + // Try to initialize again + initialize_glass_data(); + u32 final_count = get_glass_count(); + TEST_ASSERT(final_count >= 0, "Should be able to initialize after cleanup cycles"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test performance with realistic data volumes +int test_performance() { + TEST_START("Performance Test"); + + initialize_glass_data(); + + // Load data + b32 loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (loaded) { + // Test rapid catalog switching + u32 catalog_count = get_catalog_count(); + for (int i = 0; i < 100; i++) { + set_current_catalog(i % catalog_count); + u32 count = get_glass_count(); + (void)count; // Use the result + } + + // Test rapid coordinate transformations + ViewState view; + init_view(&view, 1024, 768); + fit_view_to_data(&view); + + for (int i = 0; i < 1000; i++) { + i32 screenX, screenY; + f32 abbe = 30.0f + (i % 50); + f32 ri = 1.4f + (i % 20) * 0.01f; + glass_to_screen_coords(abbe, ri, &view, &screenX, &screenY); + } + + // Test clustering performance + for (int i = 0; i < 10; i++) { + update_clustering(&view); + } + } + + cleanup_glass_data(); + TEST_END(); +} + +// Main integration test runner +int main() { + printf(BLUE "=== GlaMaC Integration Tests ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_pipeline); + RUN_TEST(test_view_with_glass_data); + RUN_TEST(test_fgla_search_integration); + RUN_TEST(test_error_handling_integration); + RUN_TEST(test_memory_management); + RUN_TEST(test_performance); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/integration/test_simple_pipeline.c b/tests/integration/test_simple_pipeline.c new file mode 100644 index 0000000..01d2533 --- /dev/null +++ b/tests/integration/test_simple_pipeline.c @@ -0,0 +1,221 @@ +/** + * test_simple_pipeline.c - Simplified integration tests for GlaMaC pipeline + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_view.h" +#include "../../include/glautils/fgla.h" + +// Test glass data loading and basic operations +int test_glass_data_operations() { + TEST_START("Glass Data Operations"); + + // Initialize glass data system + initialize_glass_data(); + + // Try to load JSON data + b32 json_loaded = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (json_loaded) { + // Verify we have catalogs + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have loaded catalogs"); + + // Test each catalog + for (u32 i = 0; i < catalog_count; i++) { + set_current_catalog(i); + + const char* catalog_name = get_catalog_name(i); + TEST_ASSERT_NOT_NULL(catalog_name, "Each catalog should have a name"); + + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Each catalog should have glasses"); + + // Test first glass in catalog + if (glass_count > 0) { + const Glass* glass = get_glass(0); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get first glass"); + + // Validate glass data + TEST_ASSERT(glass->abbeNumber > 10.0f && glass->abbeNumber < 100.0f, + "Abbe number should be in reasonable range"); + TEST_ASSERT(glass->refractiveIndex > 1.0f && glass->refractiveIndex < 4.0f, + "Refractive index should be in reasonable range"); + + const byte* name = get_glass_name(0); + TEST_ASSERT_NOT_NULL(name, "Glass should have a name"); + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + } + } else { + printf(YELLOW " Info: JSON test data not available - testing with default data" RESET "\n"); + + // Test with default data + u32 glass_count = get_glass_count(); + TEST_ASSERT(glass_count > 0, "Should have default glasses"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view system integration +int test_view_integration() { + TEST_START("View System Integration"); + + // Initialize systems + initialize_glass_data(); + ViewState view; + init_view_state(&view, 1024, 768); + + // Load test data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Refresh view with data + refresh_view_data_range(&view); + + // After refreshing, we should have valid ranges + TEST_ASSERT(view.minAbbe < view.maxAbbe, "Should have valid Abbe range"); + TEST_ASSERT(view.minRI < view.maxRI, "Should have valid RI range"); + + // Test coordinate transformations with actual glass data + u32 glass_count = get_glass_count(); + if (glass_count > 0) { + const Glass* glass = get_glass(0); + i32 screenX, screenY; + data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, &view, &screenX, &screenY); + + // Screen coordinates should be reasonable + TEST_ASSERT(screenX >= -200 && screenX <= view.windowWidth + 200, + "Screen X should be near window bounds"); + TEST_ASSERT(screenY >= -200 && screenY <= view.windowHeight + 200, + "Screen Y should be near window bounds"); + + // Test inverse transformation + f32 recovered_abbe, recovered_ri; + screen_to_data_coords(screenX, screenY, &view, &recovered_abbe, &recovered_ri); + + TEST_ASSERT_FLOAT_EQ(glass->abbeNumber, recovered_abbe, 1.0f, + "Should approximately recover original Abbe number"); + TEST_ASSERT_FLOAT_EQ(glass->refractiveIndex, recovered_ri, 0.01f, + "Should approximately recover original refractive index"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test FGLA functionality with real search patterns +int test_fgla_functionality() { + TEST_START("FGLA Functionality"); + + // Test glass code pattern matching with realistic codes + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Should match exact glass code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642123", "517642"), + "Should match first 6 digits of longer code"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Should match wildcard pattern"); + + // Test realistic search terms + TEST_ASSERT(fgla_validate_search_term("N-BK7"), "N-BK7 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("SF10"), "SF10 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("517642"), "517642 should be valid search term"); + TEST_ASSERT(fgla_validate_search_term("51x64x"), "51x64x should be valid search term"); + + // Test substring matching with realistic glass names + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find BK in N-BK7"); + TEST_ASSERT(fgla_contains_substring_safe("SF10", "sf"), + "Should find sf in SF10 (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("FCD1", "fcd"), + "Should find fcd in FCD1 (case insensitive)"); + + // Test catalog matching with real manufacturers + const char* real_catalogs[] = {"SCHOTT", "HOYA", "CDGM", "Ohara"}; + TEST_ASSERT(fgla_matches_catalog("SCHOTT", real_catalogs, 4), + "Should match SCHOTT catalog"); + TEST_ASSERT(fgla_matches_catalog("hoya", real_catalogs, 4), + "Should match HOYA catalog (case insensitive)"); + TEST_ASSERT(!fgla_matches_catalog("UNKNOWN", real_catalogs, 4), + "Should not match unknown manufacturer"); + + TEST_END(); +} + +// Test error handling across systems +int test_error_handling() { + TEST_START("Error Handling"); + + // Test glass data error handling + b32 result = load_glasses_from_json((const byte*)"nonexistent.json", NULL); + TEST_ASSERT(!result, "Should fail gracefully with non-existent file"); + + result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!result, "Should fail gracefully with NULL path"); + + // Test view system with edge case values + ViewState view; + init_view_state(&view, 1, 1); // Very small dimensions + TEST_ASSERT(view.windowWidth > 0, "Should handle small width"); + TEST_ASSERT(view.windowHeight > 0, "Should handle small height"); + + // Test FGLA with invalid inputs + TEST_ASSERT(!fgla_validate_search_term(NULL), "Should reject NULL search term"); + TEST_ASSERT(!fgla_validate_search_term(""), "Should reject empty search term"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "Should reject NULL glass code pattern"); + + TEST_END(); +} + +// Test memory management +int test_memory_management() { + TEST_START("Memory Management"); + + // Test multiple initialize/cleanup cycles + for (int i = 0; i < 3; i++) { + initialize_glass_data(); + + // Load data if available + load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + // Use the data + u32 count = get_glass_count(); + if (count > 0) { + const Glass* glass = get_glass(0); + (void)glass; // Suppress unused variable warning + } + + // Cleanup + cleanup_glass_data(); + } + + // After all cycles, we should be able to initialize again + initialize_glass_data(); + u32 final_count = get_glass_count(); + TEST_ASSERT(final_count < 1000000, "Should have reasonable glass count after cleanup cycles"); + + cleanup_glass_data(); + TEST_END(); +} + +// Main integration test runner +int main() { + printf(BLUE "=== GlaMaC Integration Tests (Simplified) ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_operations); + RUN_TEST(test_view_integration); + RUN_TEST(test_fgla_functionality); + RUN_TEST(test_error_handling); + RUN_TEST(test_memory_management); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..1d71a97 --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,120 @@ +/** + * test_framework.h - Simple unit testing framework for GlaMaC + * + * 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. + */ + +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include +#include + +// Test statistics +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +// ANSI color codes for output +#define RESET "\033[0m" +#define RED "\033[31m" +#define GREEN "\033[32m" +#define YELLOW "\033[33m" +#define BLUE "\033[34m" +#define MAGENTA "\033[35m" +#define CYAN "\033[36m" + +// Test macros +#define TEST_START(name) \ + printf(CYAN "Running test: %s" RESET "\n", name); \ + tests_run++; + +#define TEST_ASSERT(condition, message) \ + if (condition) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_EQ(expected, actual, message) \ + if ((expected) == (actual)) { \ + printf(GREEN " ✓ PASS: %s (expected: %d, actual: %d)" RESET "\n", message, expected, actual); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: %d, actual: %d)" RESET "\n", message, expected, actual); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_STR_EQ(expected, actual, message) \ + if (strcmp(expected, actual) == 0) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: '%s', actual: '%s')" RESET "\n", message, expected, actual); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_FLOAT_EQ(expected, actual, tolerance, message) \ + if (fabs((expected) - (actual)) < (tolerance)) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected: %.6f, actual: %.6f, diff: %.6f)" RESET "\n", \ + message, expected, actual, fabs((expected) - (actual))); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_NULL(ptr, message) \ + if ((ptr) == NULL) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected NULL, got non-NULL)" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_ASSERT_NOT_NULL(ptr, message) \ + if ((ptr) != NULL) { \ + printf(GREEN " ✓ PASS: %s" RESET "\n", message); \ + } else { \ + printf(RED " ✗ FAIL: %s (expected non-NULL, got NULL)" RESET "\n", message); \ + tests_failed++; \ + return 0; \ + } + +#define TEST_END() \ + tests_passed++; \ + printf(GREEN "Test completed successfully" RESET "\n\n"); \ + return 1; + +// Test runner helpers +#define RUN_TEST(test_func) \ + if (test_func()) { \ + /* Test passed */ \ + } else { \ + /* Test failed - already handled in macros */ \ + } + +#define TEST_SUMMARY() \ + printf(MAGENTA "=== Test Summary ===" RESET "\n"); \ + printf("Total tests run: %d\n", tests_run); \ + printf(GREEN "Passed: %d" RESET "\n", tests_passed); \ + if (tests_failed > 0) { \ + printf(RED "Failed: %d" RESET "\n", tests_failed); \ + } else { \ + printf("Failed: 0\n"); \ + } \ + printf("Success rate: %.1f%%\n", tests_run > 0 ? (100.0 * tests_passed / tests_run) : 0.0); \ + printf(MAGENTA "===================" RESET "\n"); \ + return tests_failed == 0 ? 0 : 1; + +#endif /* TEST_FRAMEWORK_H */ \ No newline at end of file diff --git a/tests/unit/test_fgla.c b/tests/unit/test_fgla.c new file mode 100644 index 0000000..60f4e7b --- /dev/null +++ b/tests/unit/test_fgla.c @@ -0,0 +1,231 @@ +/** + * test_fgla.c - Unit tests for fgla utility functions + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glautils/fgla.h" + +// Test string validation +int test_search_term_validation() { + TEST_START("Search Term Validation"); + + // Valid search terms + TEST_ASSERT(fgla_validate_search_term("BK7"), "BK7 should be valid"); + TEST_ASSERT(fgla_validate_search_term("N-SF6"), "N-SF6 should be valid"); + TEST_ASSERT(fgla_validate_search_term("517642"), "517642 should be valid"); + TEST_ASSERT(fgla_validate_search_term("51x64x"), "51x64x should be valid"); + TEST_ASSERT(fgla_validate_search_term("FK51A"), "FK51A should be valid"); + + // Invalid search terms + TEST_ASSERT(!fgla_validate_search_term(NULL), "NULL should be invalid"); + TEST_ASSERT(!fgla_validate_search_term(""), "Empty string should be invalid"); + TEST_ASSERT(!fgla_validate_search_term("test@#$"), "Special characters should be invalid"); + TEST_ASSERT(!fgla_validate_search_term("test|pipe"), "Pipe character should be invalid"); + + // Test boundary conditions + char long_term[300]; + memset(long_term, 'A', sizeof(long_term) - 1); + long_term[sizeof(long_term) - 1] = '\0'; + TEST_ASSERT(!fgla_validate_search_term(long_term), "Overly long term should be invalid"); + + TEST_END(); +} + +// Test glass code pattern recognition +int test_glass_code_pattern() { + TEST_START("Glass Code Pattern Recognition"); + + // Valid glass code patterns + TEST_ASSERT(fgla_is_glass_code_pattern("517642"), "517642 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("123456"), "123456 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("51x64x"), "51x64x should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("x12345"), "x12345 should be glass code pattern"); + TEST_ASSERT(fgla_is_glass_code_pattern("xxxxxx"), "xxxxxx should be glass code pattern"); + + // Invalid patterns + TEST_ASSERT(!fgla_is_glass_code_pattern("51764"), "51764 (5 digits) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("5176420"), "5176420 (7 digits) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("BK7123"), "BK7123 (letters) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern("51-642"), "51-642 (dash) should not be glass code pattern"); + TEST_ASSERT(!fgla_is_glass_code_pattern(NULL), "NULL should not be glass code pattern"); + + TEST_END(); +} + +// Test glass code pattern matching +int test_glass_code_matching() { + TEST_START("Glass Code Pattern Matching"); + + // Exact matches + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "517642"), + "Exact match should work"); + + // Wildcard matches + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51x64x"), + "Wildcard pattern should match"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "x17642"), + "Leading wildcard should match"); + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("517642", "51764x"), + "Trailing wildcard should match"); + + // Non-matches + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", "518642"), + "Different digit should not match"); + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", "51x63x"), + "Wrong wildcard pattern should not match"); + + // Handle longer glass codes (should extract first 6 digits) + TEST_ASSERT(fgla_matches_glass_code_pattern_safe("5176420123", "517642"), + "Should match first 6 digits of longer code"); + + // Edge cases + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe(NULL, "517642"), + "NULL glass code should not match"); + TEST_ASSERT(!fgla_matches_glass_code_pattern_safe("517642", NULL), + "NULL pattern should not match"); + + TEST_END(); +} + +// Test string normalization +int test_string_normalization() { + TEST_START("String Normalization"); + + char output[100]; + + // Test basic normalization + int result = fgla_normalize_string_safe("N-BK7", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "Normalization should succeed"); + TEST_ASSERT_STR_EQ("nbk7", output, "Should remove dash and convert to lowercase"); + + // Test with multiple dashes + result = fgla_normalize_string_safe("N-SF-6", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "Multi-dash normalization should succeed"); + TEST_ASSERT_STR_EQ("nsf6", output, "Should remove all dashes"); + + // Test with no dashes + result = fgla_normalize_string_safe("BK7", output, sizeof(output)); + TEST_ASSERT_EQ(0, result, "No-dash normalization should succeed"); + TEST_ASSERT_STR_EQ("bk7", output, "Should just convert to lowercase"); + + // Test error conditions + result = fgla_normalize_string_safe(NULL, output, sizeof(output)); + TEST_ASSERT_EQ(-1, result, "NULL input should return error"); + + result = fgla_normalize_string_safe("test", NULL, sizeof(output)); + TEST_ASSERT_EQ(-1, result, "NULL output should return error"); + + result = fgla_normalize_string_safe("test", output, 0); + TEST_ASSERT_EQ(-1, result, "Zero-size output should return error"); + + TEST_END(); +} + +// Test substring search +int test_substring_search() { + TEST_START("Substring Search"); + + // Case-insensitive search + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "bk"), + "Should find 'bk' in 'N-BK7' (case insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "BK"), + "Should find 'BK' in 'N-BK7'"); + + // Dash-insensitive search + TEST_ASSERT(fgla_contains_substring_safe("N-BK7", "nbk"), + "Should find 'nbk' in 'N-BK7' (dash insensitive)"); + TEST_ASSERT(fgla_contains_substring_safe("NBK7", "n-bk"), + "Should find 'n-bk' in 'NBK7' (dash insensitive)"); + + // Non-matches + TEST_ASSERT(!fgla_contains_substring_safe("N-BK7", "sf"), + "Should not find 'sf' in 'N-BK7'"); + + // Edge cases + TEST_ASSERT(!fgla_contains_substring_safe(NULL, "test"), + "NULL haystack should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("test", NULL), + "NULL needle should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("", "test"), + "Empty haystack should return false"); + TEST_ASSERT(!fgla_contains_substring_safe("test", ""), + "Empty needle should return false"); + + TEST_END(); +} + +// Test catalog matching +int test_catalog_matching() { + TEST_START("Catalog Matching"); + + const char* catalogs[] = {"SCHOTT", "HOYA", "CDGM"}; + + // Should match + TEST_ASSERT(fgla_matches_catalog("SCHOTT", catalogs, 3), + "Should match SCHOTT"); + TEST_ASSERT(fgla_matches_catalog("schott", catalogs, 3), + "Should match schott (case insensitive)"); + TEST_ASSERT(fgla_matches_catalog("HOYA", catalogs, 3), + "Should match HOYA"); + + // Should not match + TEST_ASSERT(!fgla_matches_catalog("Ohara", catalogs, 3), + "Should not match Ohara (not in list)"); + + // Empty catalog list should match all + TEST_ASSERT(fgla_matches_catalog("SCHOTT", NULL, 0), + "Empty catalog list should match anything"); + TEST_ASSERT(fgla_matches_catalog("Unknown", NULL, 0), + "Empty catalog list should match anything"); + + TEST_END(); +} + +// Test lowercase conversion +int test_lowercase_conversion() { + TEST_START("Lowercase Conversion"); + + char test_str[20]; + + // Test normal case + strcpy(test_str, "BK7"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("bk7", test_str, "Should convert BK7 to bk7"); + + // Test mixed case + strcpy(test_str, "N-SF6"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("n-sf6", test_str, "Should convert N-SF6 to n-sf6"); + + // Test already lowercase + strcpy(test_str, "already"); + fgla_to_lowercase_safe(test_str, sizeof(test_str)); + TEST_ASSERT_STR_EQ("already", test_str, "Should not change already lowercase"); + + // Test with NULL (should not crash) + fgla_to_lowercase_safe(NULL, 10); // Should not crash + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== FGLA Utility Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_search_term_validation); + RUN_TEST(test_glass_code_pattern); + RUN_TEST(test_glass_code_matching); + RUN_TEST(test_string_normalization); + RUN_TEST(test_substring_search); + RUN_TEST(test_catalog_matching); + RUN_TEST(test_lowercase_conversion); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glamac_view.c b/tests/unit/test_glamac_view.c new file mode 100644 index 0000000..2decc3f --- /dev/null +++ b/tests/unit/test_glamac_view.c @@ -0,0 +1,243 @@ +/** + * test_glamac_view.c - Unit tests for view management + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glamac_view.h" +#include "../../include/glass_data.h" + +// Test view initialization +int test_view_initialization() { + TEST_START("View Initialization"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Check initial values + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "Initial X offset should be 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Initial Y offset should be 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Initial zoom should be 1.0"); + TEST_ASSERT_EQ(800, view.windowWidth, "Window width should be set correctly"); + TEST_ASSERT_EQ(600, view.windowHeight, "Window height should be set correctly"); + + TEST_END(); +} + +// Test coordinate transformations +int test_coordinate_transformations() { + TEST_START("Coordinate Transformations"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for proper coordinate system + initialize_glass_data(); + + // Test glass to screen coordinate conversion + i32 screenX, screenY; + data_to_screen_coords(60.0f, 1.5f, &view, &screenX, &screenY); + + // Screen coordinates should be within window bounds for typical glass values + TEST_ASSERT(screenX >= 0 && screenX <= view.windowWidth, + "Screen X coordinate should be within window bounds"); + TEST_ASSERT(screenY >= 0 && screenY <= view.windowHeight, + "Screen Y coordinate should be within window bounds"); + + // Test screen to glass coordinate conversion (inverse operation) + f32 abbeNumber, refractiveIndex; + screen_to_data_coords(screenX, screenY, &view, &abbeNumber, &refractiveIndex); + + // The inverse transformation should approximately recover original values + TEST_ASSERT_FLOAT_EQ(60.0f, abbeNumber, 1.0f, + "Inverse transformation should recover Abbe number"); + TEST_ASSERT_FLOAT_EQ(1.5f, refractiveIndex, 0.01f, + "Inverse transformation should recover refractive index"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test zoom functionality +int test_zoom_functionality() { + TEST_START("Zoom Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test zoom in + f32 initial_zoom = view.zoomLevel; + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in at center + TEST_ASSERT(view.zoomLevel > initial_zoom, "Zoom should increase"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test zoom out + f32 zoomed_in = view.zoomLevel; + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out at center + TEST_ASSERT(view.zoomLevel < zoomed_in, "Zoom should decrease"); + TEST_ASSERT(view.zoomLevel >= MIN_ZOOM, "Zoom should not go below minimum"); + + // Test zoom limits + for (int i = 0; i < 20; i++) { + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in a lot + } + TEST_ASSERT_FLOAT_EQ(MAX_ZOOM, view.zoomLevel, 0.001f, "Should be clamped to max zoom"); + + for (int i = 0; i < 20; i++) { + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out a lot + } + TEST_ASSERT_FLOAT_EQ(MIN_ZOOM, view.zoomLevel, 0.001f, "Should be clamped to min zoom"); + + TEST_END(); +} + +// Test panning functionality +int test_panning_functionality() { + TEST_START("Panning Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test panning + f32 initial_x = view.offsetX; + f32 initial_y = view.offsetY; + + pan_view(&view, 100, 50); // Pan right and up + TEST_ASSERT(view.offsetX != initial_x, "X offset should change with panning"); + TEST_ASSERT(view.offsetY != initial_y, "Y offset should change with panning"); + + // Test panning in opposite direction + pan_view(&view, -100, -50); // Pan left and down + TEST_ASSERT_FLOAT_EQ(initial_x, view.offsetX, 0.1f, + "Should return to approximately initial X after opposite panning"); + TEST_ASSERT_FLOAT_EQ(initial_y, view.offsetY, 0.1f, + "Should return to approximately initial Y after opposite panning"); + + TEST_END(); +} + +// Test view fitting +int test_view_fitting() { + TEST_START("View Fitting"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for fitting + initialize_glass_data(); + + // Test fit to data + fit_view_to_data(&view); + + // After fitting, the view should be adjusted to show all data + // The exact values depend on the glass data, but zoom and offsets should be reasonable + TEST_ASSERT(view.zoomLevel > 0.0f, "Zoom should be positive after fitting"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum after fitting"); + + // Offsets should be set to center the data + TEST_ASSERT(view.offsetX != 0.0f || view.offsetY != 0.0f, + "At least one offset should be non-zero after fitting (unless data is perfectly centered)"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test clustering functionality +int test_clustering() { + TEST_START("Clustering"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for clustering + initialize_glass_data(); + + // Update clustering + update_clustering(&view); + + // Check that clustering was performed + TEST_ASSERT(view.clusterCount >= 0, "Cluster count should be non-negative"); + TEST_ASSERT(view.clusterCount <= MAX_CLUSTERS, "Cluster count should not exceed maximum"); + + // If we have clusters, test their properties + for (u32 i = 0; i < view.clusterCount; i++) { + const GlassCluster* cluster = &view.clusters[i]; + TEST_ASSERT(cluster->count > 0, "Each cluster should have at least one glass"); + TEST_ASSERT(cluster->count <= MAX_CLUSTER_SIZE, "Cluster size should not exceed maximum"); + + // Center coordinates should be reasonable + TEST_ASSERT(cluster->centerX >= 0 && cluster->centerX < view.windowWidth, + "Cluster center X should be within window bounds"); + TEST_ASSERT(cluster->centerY >= 0 && cluster->centerY < view.windowHeight, + "Cluster center Y should be within window bounds"); + } + + cleanup_glass_data(); + TEST_END(); +} + +// Test view reset +int test_view_reset() { + TEST_START("View Reset"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Modify the view + view.offsetX = 100.0f; + view.offsetY = 50.0f; + view.zoomLevel = 2.0f; + + // Reset view + reset_view(&view); + + // Check that view is back to initial state + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "X offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Y offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Zoom should be reset to 1.0"); + + TEST_END(); +} + +// Test window resize handling +int test_window_resize() { + TEST_START("Window Resize"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test resize + resize_view(&view, 1024, 768); + + TEST_ASSERT_EQ(1024, view.windowWidth, "Window width should be updated"); + TEST_ASSERT_EQ(768, view.windowHeight, "Window height should be updated"); + + // Test with zero dimensions (should be handled gracefully) + resize_view(&view, 0, 0); + TEST_ASSERT(view.windowWidth > 0, "Window width should remain positive"); + TEST_ASSERT(view.windowHeight > 0, "Window height should remain positive"); + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== View Management Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_view_initialization); + RUN_TEST(test_coordinate_transformations); + RUN_TEST(test_zoom_functionality); + RUN_TEST(test_panning_functionality); + RUN_TEST(test_view_fitting); + RUN_TEST(test_clustering); + RUN_TEST(test_view_reset); + RUN_TEST(test_window_resize); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glamac_view_simple.c b/tests/unit/test_glamac_view_simple.c new file mode 100644 index 0000000..b17e4b9 --- /dev/null +++ b/tests/unit/test_glamac_view_simple.c @@ -0,0 +1,168 @@ +/** + * test_glamac_view_simple.c - Simplified unit tests for view management + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glamac_view.h" +#include "../../include/glass_data.h" + +// Test view initialization +int test_view_initialization() { + TEST_START("View Initialization"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Check initial values + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "Initial X offset should be 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Initial Y offset should be 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Initial zoom should be 1.0"); + TEST_ASSERT_EQ(800, view.windowWidth, "Window width should be set correctly"); + TEST_ASSERT_EQ(600, view.windowHeight, "Window height should be set correctly"); + + TEST_END(); +} + +// Test coordinate transformations +int test_coordinate_transformations() { + TEST_START("Coordinate Transformations"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Initialize glass data for proper coordinate system + initialize_glass_data(); + refresh_view_data_range(&view); + + // Test data to screen coordinate conversion + i32 screenX, screenY; + data_to_screen_coords(60.0f, 1.5f, &view, &screenX, &screenY); + + // Screen coordinates should be within reasonable bounds + TEST_ASSERT(screenX >= -100 && screenX <= view.windowWidth + 100, + "Screen X coordinate should be near window bounds"); + TEST_ASSERT(screenY >= -100 && screenY <= view.windowHeight + 100, + "Screen Y coordinate should be near window bounds"); + + // Test screen to data coordinate conversion (inverse operation) + f32 abbeNumber, refractiveIndex; + screen_to_data_coords(screenX, screenY, &view, &abbeNumber, &refractiveIndex); + + // The inverse transformation should approximately recover original values + TEST_ASSERT_FLOAT_EQ(60.0f, abbeNumber, 1.0f, + "Inverse transformation should recover Abbe number"); + TEST_ASSERT_FLOAT_EQ(1.5f, refractiveIndex, 0.01f, + "Inverse transformation should recover refractive index"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test zoom functionality +int test_zoom_functionality() { + TEST_START("Zoom Functionality"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Test zoom in + f32 initial_zoom = view.zoomLevel; + handle_mouse_wheel_zoom(1, 400, 300, &view); // Zoom in at center + TEST_ASSERT(view.zoomLevel > initial_zoom, "Zoom should increase"); + TEST_ASSERT(view.zoomLevel <= MAX_ZOOM, "Zoom should not exceed maximum"); + + // Test zoom out + f32 zoomed_in = view.zoomLevel; + handle_mouse_wheel_zoom(-1, 400, 300, &view); // Zoom out at center + TEST_ASSERT(view.zoomLevel < zoomed_in, "Zoom should decrease"); + TEST_ASSERT(view.zoomLevel >= MIN_ZOOM, "Zoom should not go below minimum"); + + TEST_END(); +} + +// Test view reset +int test_view_reset() { + TEST_START("View Reset"); + + ViewState view; + init_view_state(&view, 800, 600); + + // Modify the view + view.offsetX = 100.0f; + view.offsetY = 50.0f; + view.zoomLevel = 2.0f; + + // Reset view + reset_view(&view); + + // Check that view is back to initial state + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetX, 0.001f, "X offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(0.0f, view.offsetY, 0.001f, "Y offset should be reset to 0"); + TEST_ASSERT_FLOAT_EQ(1.0f, view.zoomLevel, 0.001f, "Zoom should be reset to 1.0"); + + TEST_END(); +} + +// Test data range functionality +int test_data_range() { + TEST_START("Data Range"); + + ViewState view; + init_view_state(&view, 800, 600); + + initialize_glass_data(); + refresh_view_data_range(&view); + + // After refreshing, we should have valid data ranges + TEST_ASSERT(view.minAbbe < view.maxAbbe, "Min Abbe should be less than max Abbe"); + TEST_ASSERT(view.minRI < view.maxRI, "Min RI should be less than max RI"); + TEST_ASSERT(view.minAbbe > 0.0f, "Min Abbe should be positive"); + TEST_ASSERT(view.maxAbbe < 200.0f, "Max Abbe should be reasonable"); + TEST_ASSERT(view.minRI > 1.0f, "Min RI should be greater than 1.0"); + TEST_ASSERT(view.maxRI < 4.0f, "Max RI should be reasonable"); + + cleanup_glass_data(); + TEST_END(); +} + +// Test visible range calculation +int test_visible_range() { + TEST_START("Visible Range Calculation"); + + ViewState view; + init_view_state(&view, 800, 600); + + initialize_glass_data(); + refresh_view_data_range(&view); + + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(&view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); + + // Visible range should be valid + TEST_ASSERT(visibleMinAbbe < visibleMaxAbbe, "Visible min Abbe should be less than max"); + TEST_ASSERT(visibleMinRI < visibleMaxRI, "Visible min RI should be less than max"); + + cleanup_glass_data(); + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== View Management Unit Tests (Simplified) ===" RESET "\n\n"); + + RUN_TEST(test_view_initialization); + RUN_TEST(test_coordinate_transformations); + RUN_TEST(test_zoom_functionality); + RUN_TEST(test_view_reset); + RUN_TEST(test_data_range); + RUN_TEST(test_visible_range); + + TEST_SUMMARY(); +} \ No newline at end of file diff --git a/tests/unit/test_glass_data.c b/tests/unit/test_glass_data.c new file mode 100644 index 0000000..3498747 --- /dev/null +++ b/tests/unit/test_glass_data.c @@ -0,0 +1,174 @@ +/** + * test_glass_data.c - Unit tests for glass data management + * + * 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. + */ + +#include "../test_framework.h" +#include "../../include/glass_data.h" +#include "../../include/glamac_errors.h" + +// Test glass data initialization +int test_glass_data_initialization() { + TEST_START("Glass Data Initialization"); + + initialize_glass_data(); + + // After initialization, we should have at least the default glasses + u32 count = get_glass_count(); + TEST_ASSERT(count > 0, "Should have at least some default glasses"); + + // Test that we can get the first glass + const Glass* glass = get_glass(0); + TEST_ASSERT_NOT_NULL(glass, "Should be able to get first glass"); + + // Test bounds checking - invalid index should return NULL + const Glass* invalid_glass = get_glass(999999); + TEST_ASSERT_NULL(invalid_glass, "Invalid index should return NULL"); + + TEST_END(); +} + +// Test glass data range calculation +int test_glass_data_range() { + TEST_START("Glass Data Range Calculation"); + + initialize_glass_data(); + + f32 minAbbe, maxAbbe, minRI, maxRI; + find_glass_data_range(&minAbbe, &maxAbbe, &minRI, &maxRI); + + // Validate that ranges make sense + TEST_ASSERT(minAbbe < maxAbbe, "Min Abbe should be less than max Abbe"); + TEST_ASSERT(minRI < maxRI, "Min RI should be less than max RI"); + TEST_ASSERT(minAbbe > 0.0f, "Min Abbe should be positive"); + TEST_ASSERT(maxAbbe < 200.0f, "Max Abbe should be reasonable (< 200)"); + TEST_ASSERT(minRI > 1.0f, "Min RI should be greater than 1.0"); + TEST_ASSERT(maxRI < 4.0f, "Max RI should be reasonable (< 4.0)"); + + TEST_END(); +} + +// Test JSON loading with test data +int test_json_loading() { + TEST_START("JSON Loading"); + + // Try to load test data + b32 result = load_glasses_from_json((const byte*)"tests/data/test_glasses.json", NULL); + + if (result) { + // Verify that we loaded some glasses + u32 count = get_glass_count(); + TEST_ASSERT(count > 0, "Should have loaded glasses from test JSON"); + + // Test catalog functionality + u32 catalog_count = get_catalog_count(); + TEST_ASSERT(catalog_count > 0, "Should have at least one catalog"); + + // Test catalog name retrieval + const char* catalog_name = get_catalog_name(0); + TEST_ASSERT_NOT_NULL(catalog_name, "Should be able to get catalog name"); + + // Test catalog switching + if (catalog_count > 1) { + set_current_catalog(1); + const char* second_catalog = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(second_catalog, "Should be able to switch catalogs"); + } + } else { + printf(YELLOW " Warning: Could not load test JSON file - this may be expected in some test environments" RESET "\n"); + } + + TEST_END(); +} + +// Test glass name retrieval +int test_glass_name_retrieval() { + TEST_START("Glass Name Retrieval"); + + initialize_glass_data(); + + u32 count = get_glass_count(); + if (count > 0) { + const byte* name = get_glass_name(0); + TEST_ASSERT_NOT_NULL(name, "Should be able to get glass name"); + + // Verify name is not empty + TEST_ASSERT(strlen((const char*)name) > 0, "Glass name should not be empty"); + } + + // Test invalid index + const byte* invalid_name = get_glass_name(999999); + TEST_ASSERT_NULL(invalid_name, "Invalid index should return NULL name"); + + TEST_END(); +} + +// Test catalog cycling +int test_catalog_cycling() { + TEST_START("Catalog Cycling"); + + initialize_glass_data(); + + u32 initial_catalog_count = get_catalog_count(); + + if (initial_catalog_count > 1) { + // Get initial catalog + const char* initial_name = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(initial_name, "Should have current catalog name"); + + // Cycle forward + cycle_catalog(1); + const char* next_name = get_current_catalog_name(); + TEST_ASSERT_NOT_NULL(next_name, "Should have catalog name after cycling"); + + // Cycle backward + cycle_catalog(-1); + const char* back_name = get_current_catalog_name(); + TEST_ASSERT_STR_EQ(initial_name, back_name, "Should return to initial catalog after cycling back"); + } else { + printf(YELLOW " Info: Only one catalog available - skipping cycling test" RESET "\n"); + } + + TEST_END(); +} + +// Test data validation with invalid data +int test_data_validation() { + TEST_START("Data Validation"); + + // This test would require access to internal validation functions + // For now, we'll test through the public API by trying to load invalid data + + // Try loading from non-existent file + b32 result = load_glasses_from_json((const byte*)"nonexistent_file.json", NULL); + TEST_ASSERT(!result, "Should fail to load non-existent file"); + + // Test with invalid file path (NULL) + b32 null_result = load_glasses_from_json(NULL, NULL); + TEST_ASSERT(!null_result, "Should fail with NULL path"); + + TEST_END(); +} + +// Main test runner +int main() { + printf(BLUE "=== Glass Data Unit Tests ===" RESET "\n\n"); + + RUN_TEST(test_glass_data_initialization); + RUN_TEST(test_glass_data_range); + RUN_TEST(test_json_loading); + RUN_TEST(test_glass_name_retrieval); + RUN_TEST(test_catalog_cycling); + RUN_TEST(test_data_validation); + + // Cleanup + cleanup_glass_data(); + + TEST_SUMMARY(); +} \ No newline at end of file -- cgit v1.2.3