diff options
author | admin <admin@optics-design.com> | 2025-08-05 11:28:41 +0200 |
---|---|---|
committer | admin <admin@optics-design.com> | 2025-08-05 11:28:41 +0200 |
commit | 04b3fcb479f5aaae06d18b315a8bdc8c298f4eae (patch) | |
tree | 92f60caef26f98a83681aa0e1f360df03203bbe4 | |
parent | 9496ae0a50e6848121c7e913ca2dc55c8e6c84c1 (diff) |
removed clustering
40 files changed, 3260 insertions, 1253 deletions
@@ -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_errors.h b/include/glamac/core/glamac_errors.h index 4e7b9d0..2065600 100644 --- a/include/glamac_errors.h +++ b/include/glamac/core/glamac_errors.h @@ -14,6 +14,7 @@ #define GLAMAC_ERRORS_H
#include "glamacdef.h"
+#include "security.h"
// Error codes for GlaMaC operations
typedef enum {
@@ -26,16 +27,12 @@ typedef enum { 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_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);
-// 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/glamacdef.h b/include/glamac/core/glamacdef.h index aeba926..46cbf82 100644 --- a/include/glamacdef.h +++ b/include/glamac/core/glamacdef.h @@ -16,6 +16,7 @@ #include <stdint.h>
#include <uchar.h>
#include <stddef.h>
+#include <stdlib.h>
/* 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;
@@ -37,7 +38,35 @@ 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))
+
+/* 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/glass_data.h b/include/glamac/data/glass_data.h index 56c2c7f..00f30c0 100644 --- a/include/glass_data.h +++ b/include/glamac/data/glass_data.h @@ -13,8 +13,8 @@ #ifndef GLASS_DATA_H
#define GLASS_DATA_H
-#include "glamacdef.h" // For type definitions
-#include "glamac_errors.h" // For error handling
+#include "../core/glamacdef.h" // For type definitions
+#include "../core/glamac_errors.h" // For error handling
// Structure to represent an optical glass
typedef struct {
diff --git a/include/glamac_render.h b/include/glamac/graphics/glamac_render.h index 899702d..4c20751 100644 --- a/include/glamac_render.h +++ b/include/glamac/graphics/glamac_render.h @@ -6,7 +6,7 @@ #include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
-#include "glamacdef.h"
+#include "../core/glamacdef.h"
#include "glamac_view.h"
// Drawing primitives
diff --git a/include/glamac_view.h b/include/glamac/graphics/glamac_view.h index 6ca4b9b..58de2b1 100644 --- a/include/glamac_view.h +++ b/include/glamac/graphics/glamac_view.h @@ -5,7 +5,7 @@ #define GLAMAC_VIEW_H
#include <SDL3/SDL.h>
-#include "glamacdef.h"
+#include "../core/glamacdef.h"
// Constants for view
#define MIN_PADDING 20 // Minimum padding in pixels
@@ -20,44 +20,6 @@ #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
@@ -75,22 +37,6 @@ typedef struct { 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
@@ -158,18 +104,6 @@ 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/glamac_events.h b/include/glamac/input/glamac_events.h index c4145c0..f06a703 100644 --- a/include/glamac_events.h +++ b/include/glamac/input/glamac_events.h @@ -5,8 +5,8 @@ #define GLAMAC_EVENTS_H
#include <SDL3/SDL.h>
-#include "glamacdef.h"
-#include "glamac_view.h"
+#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);
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 <SDL3_ttf/SDL_ttf.h>
#include <stdio.h>
#include <stdlib.h>
-#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 <SDL3/SDL.h>
#include <stdio.h>
#include <math.h>
-#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 <stdlib.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
-#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 <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
-#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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 <stdlib.h>
#include <string.h>
#include <ctype.h>
-#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] <search_term>\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-<name> - 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+
+// 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 |