diff --git a/.gitattributes b/.gitattributes index 02a4eff..d52fcb6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ # Vendored headers are vendored code, to the surprise of absolutely noone. # See: # -/include/**/* linguist-vendored +/include/chroma.h linguist-vendored # Don't think linguist can detect XML, but let's tell it that the protocols are # vendored *anyway*. diff --git a/Makefile b/Makefile index c1bffd4..3163a7f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PROJECT_NAME = chroma -VERSION = 2.0.0 +VERSION = 1.1.0 # Directories SRCDIR = src @@ -14,9 +14,6 @@ PREFIX ?= /usr/local BINDIR_INSTALL = $(PREFIX)/bin SYSTEMD_INSTALL = $(HOME)/.config/systemd/user -# Config file -CONFIG_FILE_NAME = chroma.toml - # Compiler and flags CC = gcc CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g @@ -49,11 +46,8 @@ LDFLAGS += -lm -ldl # Source files (excluding generated protocol files) SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c)) -VENDOR_SOURCES = $(INCDIR)/vendor/tomlc17.c OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS) -VENDOR_OBJECTS = $(VENDOR_SOURCES:$(INCDIR)/vendor/%.c=$(OBJDIR)/%.o) -ALL_OBJECTS = $(OBJECTS) $(VENDOR_OBJECTS) -DEPENDS = $(ALL_OBJECTS:.o=.d) +DEPENDS = $(OBJECTS:.o=.d) # Override object files for image.c and render.c to suppress third-party warnings OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS)) @@ -84,9 +78,9 @@ $(INCDIR): @mkdir -p $(INCDIR) # Build main executable -$(TARGET): version-header $(PROTOCOL_HEADERS) $(ALL_OBJECTS) | $(BINDIR) +$(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR) @echo " LINK $@" - @$(CC) $(ALL_OBJECTS) -o $@ $(LDFLAGS) + @$(CC) $(OBJECTS) -o $@ $(LDFLAGS) # Compile source files $(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR) @@ -101,11 +95,6 @@ $(OBJDIR)/render.o: $(SRCDIR)/render.c $(PROTOCOL_HEADERS) | $(OBJDIR) @echo " CC $<" @$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@ -# Compile vendor files -$(OBJDIR)/%.o: $(INCDIR)/vendor/%.c | $(OBJDIR) - @echo " CC $<" - @$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@ - # Debug build debug: CFLAGS = $(DEBUG_CFLAGS) debug: $(TARGET) @@ -153,11 +142,6 @@ version-header: # Create systemd service file systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service -# Create sample configuration file -sample-config: - @echo "Creating sample configuration..." - @cp chroma.toml.sample $(CONFIG_FILE_NAME) - @echo "Sample configuration created at $(CONFIG_FILE_NAME)" # Clean build artifacts clean: @@ -246,7 +230,6 @@ help: @echo " check-deps - Check if all dependencies are available" @echo " install - Install executable and systemd service" @echo " uninstall - Remove installed files" - @echo " sample-config - Create sample configuration file" @echo " clean - Remove build artifacts" @echo " distclean - Remove all generated files" @echo " format - Format source code (requires clang-format)" @@ -271,7 +254,7 @@ help: -include $(DEPENDS) # Phony targets -.PHONY: all debug static check-deps install uninstall systemd-service sample-config version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version +.PHONY: all debug static check-deps install uninstall systemd-service version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version # Print variables print-%: diff --git a/README.md b/README.md index ca3c1da..7018606 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ comprehensive monitor management. Here's what makes Chroma stand out: - **Multiple image formats**: Supports JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM - **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering -- **Simple Configuration file**: Easy setup with TOML config files +- **Configuration file support**: Easy setup with INI-style config files - **Signal handling**: Graceful shutdown and configuration reload (SIGHUP) - **Intelligent downsampling**: Automatically reduces large image resolution to save memory (up to 94% memory savings for 8K images) @@ -96,40 +96,21 @@ make sample-config Chroma looks for configuration files in this order: -1. `$XDG_CONFIG_HOME/chroma/chroma.toml` -2. `~/.config/chroma/chroma.toml` -3. `./chroma.toml` (current directory) +1. `~/.config/chroma/chroma.conf` +2. `$XDG_CONFIG_HOME/chroma/chroma.conf` +3. `./chroma.conf` (current directory) #### Sample Configuration -```toml +```ini # Default wallpaper for outputs without specific mapping -default_image = "~/.config/chroma/default.jpg" +default_image = ~/.config/chroma/default.jpg # Output-specific wallpapers -# Format: [[output]] with name and image keys -[[output]] -name = "DP-1" -image = "~/Pictures/monitor1.jpg" - -[[output]] -name = "DP-2" -image = "~/Pictures/monitor2.png" - -[[output]] -name = "HDMI-A-1" -image = "~/Pictures/hdmi_wallpaper.jpg" - -# You can also match outputs by description using the desc: prefix -# This is useful when output names change between reboots -# Format: output."desc:DESCRIPTION_PREFIX" = "/path/to/image.jpg" -[[output]] -name = "desc:Samsung" -image = "~/Pictures/samsung_wallpaper.jpg" - -[[output]] -name = "desc:LG Ultra" -image = "~/Pictures/lg_wallpaper.jpg" +# Format: output.OUTPUT_NAME = /path/to/image.jpg +output.DP-1 = ~/Pictures/monitor1.jpg +output.DP-2 = ~/Pictures/monitor2.png +output.HDMI-A-1 = ~/Pictures/hdmi_wallpaper.jpg ``` ### Finding Output Names @@ -153,7 +134,7 @@ Options: --version Show version information Examples: - chroma -c ~/.config/chroma/chroma.toml + chroma -c ~/.config/chroma/chroma.conf chroma --daemon ``` @@ -202,13 +183,10 @@ Chroma works with any Wayland compositor that supports: - `wl_compositor` interface - `wl_output` interface -- `zwlr_layer_shell_v1` (wlr-layer-shell-unstable-v1) interface - EGL window surface creation -The wlr-layer-shell protocol is required for creating wallpaper surfaces on the -background layer. Tested only on Hyprland, but should work fine with any -wlroots-based compositor or any compositor that implements the wlr-layer-shell -protocol. +Tested only on Hyprland, but should work fine with any compositor that meets the +above criteria. Which is basically all of them I think? ## Contributing diff --git a/chroma.conf.sample b/chroma.conf.sample new file mode 100644 index 0000000..6afd464 --- /dev/null +++ b/chroma.conf.sample @@ -0,0 +1,134 @@ +# This is a sample configuration file for the Chroma wallpaper daemon. +# Lines starting with # are comments and are ignored. +# +# Configuration file locations (checked in order): +# 1. ~/.config/chroma/chroma.conf +# 2. $XDG_CONFIG_HOME/chroma/chroma.conf +# 3. ./chroma.conf (current directory) +# +# To get started: +# 1. Copy this file to ~/.config/chroma/chroma.conf +# 2. Modify the paths to point to your wallpaper images +# 3. Use 'wlr-randr' or similar tools to find your output names +# 4. Restart chroma or send SIGHUP to reload configuration + +# This image will be used for any output that doesn't have a specific mapping. +# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM +# Paths can be absolute or relative, ~ expansion is supported. +# +# Alternative examples: +# default_image = ~/Pictures/wallpapers/default.png +# default_image = /usr/share/wallpapers/default.jpg +# default_image = ./wallpapers/fallback.jpg +default_image = ~/.config/chroma/default.jpg + + +# Whether to run as a background daemon +# Usually set via command line option --daemon, but can be set here too +daemon_mode = false + +# Global scaling mode for wallpapers (used as default for all outputs) +# Options: fill, fit, stretch, center +# fill - Fill entire output, crop if necessary (default) +# fit - Fit image within output, add borders if needed +# stretch - Stretch to fill output, may distort aspect ratio +# center - Center image at original size +scale_mode = fill + +# Global image filtering quality (used as default for all outputs) +# Options: nearest, linear, bilinear, trilinear +# nearest - Nearest neighbor filtering (pixelated, fast) +# linear - Linear filtering (smooth, default) +# bilinear - Bilinear filtering (smoother) +# trilinear - Trilinear filtering (smoothest) +filter_quality = linear + +# Default anchor position (0-100 for both x and y) +# anchor_x: 0=left, 50=center, 100=right +# anchor_y: 0=top, 50=center, 100=bottom +# Can use named anchors: center, top, bottom, left, right, top-left, etc. +anchor = center +anchor_x = 50 +anchor_y = 50 + +# Image downsampling settings for performance optimization +# ======================================================== +# Enable automatic downsampling of large images to save memory and improve performance +# Set to false to keep original resolution for all images (uses more memory!) +enable_downsampling = true + +# Maximum expected output resolution +# Images larger than these dimensions may be automatically downsampled +# Adjust based on your actual monitor setup +max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p) +max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p) + +# Minimum scale factor +# Prevents images from being scaled below this percentage of original size +# Useful to preserve detail on very high-resolution images +# Range: 0.1 to 1.0 (10% to 100%) +min_scale_factor = 0.25 # Don't scale below 25% of original size + +# Output-specific wallpaper mappings +# ================================== +# Basic format: output.OUTPUT_NAME = /path/to/image.ext +# +# You can match outputs by name OR by description: +# output.DP-1 = /path/to/image.jpg # Match by port name +# output.desc:Samsung = /path/to/image.jpg # Match by description prefix +# +# The description is the human-readable name provided by the compositor +# via the Wayland wl_output description event. For example, if your +# monitor reports "Samsung T27A450" as its description, you can use +# "output.desc:Samsung" to match it. The match is a prefix match, so +# "output.desc:Sam" would also work. +# +# To find your output names and descriptions, run one of these commands: +# wlr-randr (for wlroots-based compositors) +# wayland-info | grep wl_output +# kanshi list-outputs +# +# Extended format with per-output settings: +# output.OUTPUT_NAME = /path/to/image.ext +# output.OUTPUT_NAME.scale = fill|fit|stretch|center +# output.OUTPUT_NAME.filter = nearest|linear|bilinear|trilinear +# output.OUTPUT_NAME.anchor = center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right +# +# Anchor specifies which part of the image is anchored to the output: +# center - image centered (default) +# top - image anchored to top edge +# bottom - image anchored to bottom edge +# left - image anchored to left edge +# right - image anchored to right edge +# top-left - image anchored to top-left corner +# top-right - image anchored to top-right corner +# bottom-left - image anchored to bottom-left corner +# bottom-right - image anchored to bottom-right corner +# +# Examples: +# output.HDMI-A-1 = ~/Pictures/wallpaper.jpg +# output.DP-1 = ~/Pictures/monitor1.png +# output.DP-1.scale = fit +# output.DP-1.filter = bilinear +# output.DP-1.anchor = top-left +# output.DP-2 = ~/Pictures/monitor2.jpg +# output.DP-2.scale = stretch +# +# # Match by monitor description (prefix match): +# output.desc:Samsung = ~/Pictures/samsung-wallpaper.jpg +# output.desc:Samsung.scale = fill +# output.desc:LG Ultra = ~/Pictures/lg-wallpaper.jpg +# output.desc:BenQ = ~/Pictures/benq-wallpaper.jpg +# +# # Laptop internal display: +# output.eDP-1 = ~/Pictures/laptop-wallpaper.jpg +# output.eDP-1.scale = fill +# output.eDP-1.anchor = bottom-right +# output.eDP-1.filter = trilinear +# +# Custom anchor coordinates (anchor_x, anchor_y override anchor): +# output.ULTRAWIDE = ~/Pictures/ultrawide.jpg +# output.ULTRAWIDE.scale = fill +# output.ULTRAWIDE.anchor_x = 25 # 25% from left edge +# output.ULTRAWIDE.anchor_y = 10 # 10% from top edge + diff --git a/chroma.toml.sample b/chroma.toml.sample deleted file mode 100644 index fa0fb90..0000000 --- a/chroma.toml.sample +++ /dev/null @@ -1,127 +0,0 @@ -# This is a sample TOML configuration file for the Chroma wallpaper daemon. -# For more information about TOML format, see https://toml.io -# -# Configuration file locations (checked in order): -# 1. $XDG_CONFIG_HOME/chroma/chroma.toml -# 2. ~/.config/chroma/chroma.toml -# 3. ./chroma.toml (current directory) -# -# To get started: -# 1. Copy this file to ~/.config/chroma/chroma.toml -# 2. Modify the paths to point to your wallpaper images -# 3. Use 'wlr-randr' or similar tools to find your output names -# 4. Restart chroma or send SIGHUP to reload configuration - -# Default wallpaper for outputs without specific mapping -# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM -# Paths can be absolute or relative, ~ expansion is supported. -default_image = "~/.config/chroma/default.jpg" - -# Whether to run as a background daemon -# Usually set via command line option --daemon, but can be set here too -daemon_mode = false - -# Global scaling mode for wallpapers (used as default for all outputs) -# Options: fill, fit, stretch, center -# fill - Fill entire output, crop if necessary (default) -# fit - Fit image within output, add borders if needed -# stretch - Stretch to fill output, may distort aspect ratio -# center - Center image at original size -scale_mode = "fill" - -# Global image filtering quality (used as default for all outputs) -# Options: nearest, linear, bilinear, trilinear -# nearest - Nearest neighbor filtering (pixelated, fast) -# linear - Linear filtering (smooth, default) -# bilinear - Bilinear filtering (smoother) -# trilinear - Trilinear filtering (smoothest) -filter_quality = "linear" - -# Default anchor position (0-100 for both x and y) -# anchor_x: 0=left, 50=center, 100=right -# anchor_y: 0=top, 50=center, 100=bottom -# Can use named anchors: center, top, bottom, left, right, top-left, etc. -anchor = "center" -anchor_x = 50 -anchor_y = 50 - -# Image downsampling settings for performance optimization -[downsampling] -# Enable automatic downsampling of large images to save memory and improve performance -# Set to false to keep original resolution for all images (uses more memory!) -enable = true - -# Maximum expected output resolution -# Images larger than these dimensions may be automatically downsampled -# Adjust based on your actual monitor setup -max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p) -max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p) - -# Minimum scale factor -# Prevents images from being scaled below this percentage of original size -# Useful to preserve detail on very high-resolution images -# Range: 0.1 to 1.0 (10% to 100%) -min_scale_factor = 0.25 # Don't scale below 25% of original size - -# Output-specific wallpaper mappings -# ================================== -# Each [[output]] block defines a mapping for a specific output. -# -# You can match outputs by name OR by description: -# name = "DP-1" # Match by port name -# name = "desc:Samsung" # Match by description prefix -# -# The description is the human-readable name provided by the compositor -# via the Wayland wl_output description event. For example, if your -# monitor reports "Samsung T27A450" as its description, you can use -# "desc:Samsung" to match it. The match is a prefix match, so -# "desc:Sam" would also work. -# -# To find your output names and descriptions, run one of these commands: -# wlr-randr (for wlroots-based compositors) -# wayland-info | grep wl_output -# kanshi list-outputs - -[[output]] -name = "DP-1" -image = "~/Pictures/monitor1.jpg" -scale = "fill" -filter = "linear" -anchor = "center" - -[[output]] -name = "DP-2" -image = "~/Pictures/monitor2.png" -scale = "fit" - -[[output]] -name = "HDMI-A-1" -image = "~/Pictures/hdmi_wallpaper.jpg" -scale = "stretch" - -# Match by monitor description (prefix match): -[[output]] -name = "desc:Samsung" -image = "~/Pictures/samsung-wallpaper.jpg" -scale = "fill" - -[[output]] -name = "desc:LG Ultra" -image = "~/Pictures/lg-wallpaper.jpg" -scale = "fit" - -# Laptop internal display: -[[output]] -name = "eDP-1" -image = "~/Pictures/laptop-wallpaper.jpg" -scale = "fill" -anchor = "bottom-right" -filter = "trilinear" - -# Custom anchor coordinates (anchor_x, anchor_y override anchor): -[[output]] -name = "ULTRAWIDE" -image = "~/Pictures/ultrawide.jpg" -scale = "fill" -anchor_x = 25 # 25% from left edge -anchor_y = 10 # 10% from top edge diff --git a/flake.lock b/flake.lock index 2aca472..bd727c7 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1777312179, - "narHash": "sha256-AyQh4VtqwzVeLO1uHZ6/pzS6O96nXCXiUQZgCG+3X6k=", + "lastModified": 1776074868, + "narHash": "sha256-XwIXdLWyLhr+7rCjKBc0i54ExC6/s0XEC4jT8ozI2S4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "20123554ae9affff1a5bc969ad3ad02e839f49cf", + "rev": "fbb4cb95f81097fa79dae76545e2e7f6dc311f93", "type": "github" }, "original": { diff --git a/include/chroma.h b/include/chroma.h index f8e2623..2402d45 100644 --- a/include/chroma.h +++ b/include/chroma.h @@ -4,7 +4,7 @@ #include "wlr-layer-shell-unstable-v1.h" #include "xdg-shell.h" #include -#include +#include #include #include #include @@ -16,7 +16,7 @@ #define MAX_OUTPUTS 16 #define MAX_PATH_LEN 4096 -#define CONFIG_FILE_NAME "chroma.toml" +#define CONFIG_FILE_NAME "chroma.conf" // Log levels #define CHROMA_LOG_ERROR 0 @@ -73,7 +73,6 @@ typedef struct { int channels; char path[MAX_PATH_LEN]; bool loaded; - int ref_count; // Number of outputs using this image } chroma_image_t; // Wayland output information @@ -117,7 +116,6 @@ typedef struct { bool gl_resources_initialized; bool texture_uploaded; bool vbo_dirty; // track VBO needs update - bool configured; // track if initial configure received } chroma_output_t; // Config mapping structure @@ -164,9 +162,6 @@ typedef struct chroma_state { EGLDisplay egl_display; EGLContext egl_context; EGLConfig egl_config; - - // Shared OpenGL resources - GLuint shader_program; // Outputs chroma_output_t outputs[MAX_OUTPUTS]; @@ -240,15 +235,12 @@ void chroma_layer_surface_closed(void *data, // Image loading void chroma_image_init_stb(void); int chroma_image_load(chroma_image_t *image, const char *path, - const chroma_config_t *config, int output_width, - int output_height); + const chroma_config_t *config); void chroma_image_free(chroma_image_t *image); -void chroma_image_release(chroma_image_t *image); chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, const char *path); chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, - const char *path, int output_width, - int output_height); + const char *path); int chroma_image_validate(const char *path); int chroma_image_get_info(const char *path, int *width, int *height, int *channels); @@ -256,7 +248,6 @@ void chroma_images_cleanup(chroma_state_t *state); // Configuration int chroma_config_load(chroma_config_t *config, const char *config_file); -int chroma_config_load_toml(chroma_config_t *config, const char *config_file); void chroma_config_free(chroma_config_t *config); const char *chroma_config_get_image_for_output(chroma_config_t *config, const char *output_name, diff --git a/include/chroma_version.h b/include/chroma_version.h index 67184dd..1d2f49a 100644 --- a/include/chroma_version.h +++ b/include/chroma_version.h @@ -2,7 +2,7 @@ #define CHROMA_VERSION_H #ifndef CHROMA_VERSION -#define CHROMA_VERSION "2.0.0" +#define CHROMA_VERSION "1.1.0" #endif #endif // CHROMA_VERSION_H diff --git a/include/vendor/tomlc17.c b/include/vendor/tomlc17.c deleted file mode 100644 index 44778e0..0000000 --- a/include/vendor/tomlc17.c +++ /dev/null @@ -1,2915 +0,0 @@ -/* Copyright (c) 2024-2026, CK Tan. - * https://github.com/cktan/tomlc17/blob/main/LICENSE - */ -#include "tomlc17.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const toml_datum_t DATUM_ZERO = {0}; - -static toml_option_t toml_option = {0, realloc, free}; - -#define MALLOC(n) toml_option.mem_realloc(0, n) -#define REALLOC(p, n) toml_option.mem_realloc(p, n) -#define FREE(p) toml_option.mem_free(p) - -#define DO(x) \ - if (x) \ - return -1; \ - else \ - (void)0 - -// Copy string src to dst where dst is limited to dstsz that includes -// NUL. Return 0 on success, -1 otherwise (because src[] is longer than dst[]). -static inline int copystring(char *dst, int dstsz, const char *src) { - int srcsz = strlen(src) + 1; - if (srcsz > dstsz) { - return -1; - } - memcpy(dst, src, srcsz); - return 0; -} - -/* - * Error buffer - */ -typedef struct ebuf_t ebuf_t; -struct ebuf_t { - char *ptr; - int len; -}; - -/* - * Format an error into ebuf[]. Always return -1. - */ -static int SETERROR(ebuf_t ebuf, int lineno, const char *fmt, ...) { - va_list args; - va_start(args, fmt); - char *p = ebuf.ptr; - char *q = p + ebuf.len; - if (lineno) { - snprintf(p, p < q ? q - p : 0, "(line %d) ", lineno); - p += strlen(p); - } - vsnprintf(p, p < q ? q - p : 0, fmt, args); - va_end(args); - return -1; -} - -/* - * Memory pool. Allocated a big block once and hand out piecemeal. - */ -typedef struct pool_t pool_t; -struct pool_t { - int max; // size of buf[] - int top; // offset of first free byte in buf[] - char buf[1]; // first byte starts here -}; - -/** - * Create a memory pool of N bytes. Return the memory pool on - * success, or NULL if out of memory. - */ -static pool_t *pool_create(int N) { - if (N <= 0) { - N = 100; // minimum - } - int totalsz = sizeof(pool_t) + N; - pool_t *pool = MALLOC(totalsz); - if (!pool) { - return NULL; - } - memset(pool, 0, totalsz); - pool->max = N; - return pool; -} - -/** - * Destroy a memory pool. - */ -static void pool_destroy(pool_t *pool) { FREE(pool); } - -/** - * Allocate n bytes from pool. Return the memory allocated on - * success, or NULL if out of memory. - */ -static char *pool_alloc(pool_t *pool, int n) { - if (pool->top + n > pool->max) { - return NULL; - } - char *ret = pool->buf + pool->top; - pool->top += n; - return ret; -} - -/* This is a string view. */ -typedef struct span_t span_t; -struct span_t { - const char *ptr; - int len; -}; - -/* Represents a multi-part key */ -#define KEYPARTMAX 10 -typedef struct keypart_t keypart_t; -struct keypart_t { - int nspan; - span_t span[KEYPARTMAX]; -}; - -static int utf8_to_ucs(const char *s, int len, uint32_t *ret); -static int ucs_to_utf8(uint32_t code, char buf[4]); - -// flags for toml_datum_t::flag. -#define FLAG_INLINED 1 -#define FLAG_STDEXPR 2 -#define FLAG_EXPLICIT 4 - -// Maximum levels of brackets and braces to prevent -// stack overflow during recursive descent of the parser. -#define BRACKET_LEVEL_MAX 30 -#define BRACE_LEVEL_MAX 30 - -static inline size_t align8(size_t x) { return (((x) + 7) & ~7); } - -enum toktyp_t { - TOK_DOT = 1, - TOK_EQUAL, - TOK_COMMA, - TOK_LBRACK, // [ - TOK_LLBRACK, // [[ - TOK_RBRACK, // ] - TOK_RRBRACK, // ]] - TOK_LBRACE, // { - TOK_RBRACE, // } - TOK_LIT, - TOK_STRING, // "string" - TOK_MLSTRING, // """multi-line-string""" - TOK_LITSTRING, // 'lit-string' - TOK_MLLITSTRING, // '''multi-line-lit-string''' - TOK_TIME, - TOK_DATE, - TOK_DATETIME, - TOK_DATETIMETZ, - TOK_INTEGER, - TOK_FLOAT, - TOK_BOOL, - TOK_ENDL, - TOK_FIN = -5000, // EOF -}; -typedef enum toktyp_t toktyp_t; -typedef struct scanner_t scanner_t; - -/* Remember the current state of a scanner */ -typedef struct scanner_state_t scanner_state_t; -struct scanner_state_t { - scanner_t *sp; - const char *cur; // points into scanner_t::src[] - int lineno; // current line number -}; - -// A scan token -typedef struct token_t token_t; -struct token_t { - toktyp_t toktyp; - int lineno; - span_t str; - - // values represented by str - union { - const char *escp; // point to an esc char in str - int64_t int64; - double fp64; - bool b1; - struct { - // validity depends on toktyp for TIME, DATE, DATETIME, DATETIMETZ - int year, month, day, hour, minute, sec, usec; - int tz; // +- minutes - } tsval; - } u; -}; - -// Scanner object -struct scanner_t { - const char *src; // src[] is a NUL-terminated string - const char *endp; // end of src[]. always pointing at a NUL char. - const char *cur; // current char in src[] - int lineno; // line number of current char - char *errmsg; // set to ebuf.ptr if there was an error - ebuf_t ebuf; // buffer to store error message - - int bracket_level; // count depth of [ ] - int brace_level; // count depth of { } -}; -static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, - int errbufsz); -static int scan_key(scanner_t *sp, token_t *tok); -static int scan_value(scanner_t *sp, token_t *tok); -// restore scanner to state before tok was returned -static scanner_state_t scan_mark(scanner_t *sp); -static void scan_restore(scanner_t *sp, scanner_state_t state); - -#ifndef min -static inline int min(int a, int b) { return a < b ? a : b; } -#endif - -// Copy up to dstsz - 1 chars from the current position of the scanner -// to dst, and always terminate dst[] with a NUL if dstsz > 0. -static void scan_copystr(scanner_t *sp, char *dst, int dstsz) { - assert(dstsz > 0); - int len = min(sp->endp - sp->cur, dstsz - 1); // account for NUL - if (len > 0) { - memcpy(dst, sp->cur, len); - dst[len] = '\0'; - } -} - -// Parser object -typedef struct parser_t parser_t; -struct parser_t { - scanner_t scanner; - toml_datum_t toptab; // top table - toml_datum_t *curtab; // current table - pool_t *pool; // memory pool for strings - ebuf_t ebuf; // buffer to store last error message -}; - -// Find key in tab and return its index. If not found, return -1. -static int tab_find(toml_datum_t *tab, span_t key) { - assert(tab->type == TOML_TABLE); - for (int i = 0, top = tab->u.tab.size; i < top; i++) { - if (tab->u.tab.len[i] == key.len && - 0 == memcmp(tab->u.tab.key[i], key.ptr, key.len)) { - return i; - } - } - return -1; -} - -// Put key into tab dictionary. Return a place to -// the datum for the key on success, or NULL otherwise. -static toml_datum_t *tab_emplace(toml_datum_t *tab, span_t key, - const char **reason) { - assert(tab->type == TOML_TABLE); - int i = tab_find(tab, key); - if (i >= 0) { - return &tab->u.tab.value[i]; - } - - // Expand pkey[], plen[] and value[]. - int N = tab->u.tab.size; - { - char **pkey = REALLOC(tab->u.tab.key, sizeof(*pkey) * align8(N + 1)); - int *plen = REALLOC(tab->u.tab.len, sizeof(*plen) * align8(N + 1)); - toml_datum_t *value = - REALLOC(tab->u.tab.value, sizeof(*value) * align8(N + 1)); - - // on success, must save new pointers in tab->u.tab because the - // old memory areas are gone. - if (pkey) { - tab->u.tab.key = (const char **)pkey; - } - if (plen) { - tab->u.tab.len = plen; - } - if (value) { - tab->u.tab.value = value; - } - - // if any fail, it is safe to bail out. - if (!pkey || !plen || !value) { - *reason = "out of memory"; - return NULL; - } - } - - // There is sufficient space in all the arrays for one more element. - // Append the new key. The value is set to DATUM_ZERO. Caller will - // overwrite with a valid datum. - tab->u.tab.size = N + 1; - tab->u.tab.key[N] = (char *)key.ptr; - tab->u.tab.len[N] = key.len; - tab->u.tab.value[N] = DATUM_ZERO; - return &tab->u.tab.value[N]; -} - -// Add a new key in tab. Return 0 on success, -1 otherwise. -// On error, *reason will point to an error message. -static int tab_add(toml_datum_t *tab, span_t newkey, toml_datum_t newvalue, - const char **reason) { - assert(tab->type == TOML_TABLE); - toml_datum_t *pvalue = tab_emplace(tab, newkey, reason); - if (!pvalue) { - return -1; - } - if (pvalue->type) { - *reason = "duplicate key"; - return -1; - } - *pvalue = newvalue; - return 0; -} - -// Add a new element into an array. Return 0 on success, -1 otherwise. -// On error, *reason will point to an error message. -static toml_datum_t *arr_emplace(toml_datum_t *arr, const char **reason) { - assert(arr->type == TOML_ARRAY); - int n = arr->u.arr.size; - toml_datum_t *elem = REALLOC(arr->u.arr.elem, sizeof(*elem) * align8(n + 1)); - if (!elem) { - *reason = "out of memory"; - return NULL; - } - arr->u.arr.elem = elem; - arr->u.arr.size = n + 1; - elem[n] = DATUM_ZERO; - return &elem[n]; -} - -// ------------------- parser section -static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span); -static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret); -static int parse_keyvalue_expr(parser_t *pp, token_t tok); -static int parse_std_table_expr(parser_t *pp, token_t tok); -static int parse_array_table_expr(parser_t *pp, token_t tok); - -static toml_datum_t mkdatum(toml_type_t ty) { - toml_datum_t ret = {0}; - ret.type = ty; - if (ty == TOML_DATE || ty == TOML_TIME || ty == TOML_DATETIME || - ty == TOML_DATETIMETZ) { - ret.u.ts.year = -1; - ret.u.ts.month = -1; - ret.u.ts.day = -1; - ret.u.ts.hour = -1; - ret.u.ts.minute = -1; - ret.u.ts.second = -1; - ret.u.ts.usec = -1; - ret.u.ts.tz = -1; - } - return ret; -} - -// Recursively free any dynamically allocated memory in the datum tree -static void datum_free(toml_datum_t *datum) { - if (datum->type == TOML_TABLE) { - for (int i = 0, top = datum->u.tab.size; i < top; i++) { - datum_free(&datum->u.tab.value[i]); - } - FREE(datum->u.tab.key); - FREE(datum->u.tab.len); - FREE(datum->u.tab.value); - } else if (datum->type == TOML_ARRAY) { - for (int i = 0, top = datum->u.arr.size; i < top; i++) { - datum_free(&datum->u.arr.elem[i]); - } - FREE(datum->u.arr.elem); - } - // other types do not allocate memory - *datum = DATUM_ZERO; -} - -// Make a deep copy of src to dst. -// Return 0 on success, -1 otherwise. -static int datum_copy(toml_datum_t *dst, toml_datum_t src, pool_t *pool, - const char **reason) { - *dst = mkdatum(src.type); - switch (src.type) { - case TOML_STRING: - dst->u.str.ptr = pool_alloc(pool, src.u.str.len + 1); - if (!dst->u.str.ptr) { - *reason = "out of memory"; - goto bail; - } - dst->u.str.len = src.u.str.len; - memcpy((char *)dst->u.str.ptr, src.u.str.ptr, src.u.str.len + 1); - break; - case TOML_TABLE: - for (int i = 0; i < src.u.tab.size; i++) { - span_t newkey = {src.u.tab.key[i], src.u.tab.len[i]}; - toml_datum_t *pvalue = tab_emplace(dst, newkey, reason); - if (!pvalue) { - goto bail; - } - if (datum_copy(pvalue, src.u.tab.value[i], pool, reason)) { - goto bail; - } - } - break; - case TOML_ARRAY: - for (int i = 0; i < src.u.arr.size; i++) { - toml_datum_t *pelem = arr_emplace(dst, reason); - if (!pelem) { - goto bail; - } - if (datum_copy(pelem, src.u.arr.elem[i], pool, reason)) { - goto bail; - } - } - break; - default: - *dst = src; - break; - } - - return 0; - -bail: - datum_free(dst); - return -1; -} - -// Check if datum is an array of tables. -static inline bool is_array_of_tables(toml_datum_t datum) { - bool ret = (datum.type == TOML_ARRAY); - for (int i = 0; ret && i < datum.u.arr.size; i++) { - ret = (datum.u.arr.elem[i].type == TOML_TABLE); - } - return ret; -} - -// Merge src into dst. Return 0 on success, -1 otherwise. -static int datum_merge(toml_datum_t *dst, toml_datum_t src, pool_t *pool, - const char **reason) { - if (dst->type != src.type) { - datum_free(dst); - return datum_copy(dst, src, pool, reason); - } - switch (src.type) { - case TOML_TABLE: - // for key-value in src: - // override key-value in dst. - for (int i = 0; i < src.u.tab.size; i++) { - span_t key; - key.ptr = src.u.tab.key[i]; - key.len = src.u.tab.len[i]; - toml_datum_t *pvalue = tab_emplace(dst, key, reason); - if (!pvalue) { - return -1; - } - if (pvalue->type) { - DO(datum_merge(pvalue, src.u.tab.value[i], pool, reason)); - } else { - datum_free(pvalue); - DO(datum_copy(pvalue, src.u.tab.value[i], pool, reason)); - } - } - return 0; - case TOML_ARRAY: - if (is_array_of_tables(src)) { - // append src array to dst - for (int i = 0; i < src.u.arr.size; i++) { - toml_datum_t *pelem = arr_emplace(dst, reason); - if (!pelem) { - return -1; - } - DO(datum_copy(pelem, src.u.arr.elem[i], pool, reason)); - } - return 0; - } - // fallthru - default: - break; - } - datum_free(dst); - return datum_copy(dst, src, pool, reason); -} - -// Compare the content of a and b. -static bool datum_equiv(toml_datum_t a, toml_datum_t b) { - if (a.type != b.type) { - return false; - } - int N; - switch (a.type) { - case TOML_STRING: - return a.u.str.len == b.u.str.len && - 0 == memcmp(a.u.str.ptr, b.u.str.ptr, a.u.str.len); - case TOML_INT64: - return a.u.int64 == b.u.int64; - case TOML_FP64: - return a.u.fp64 == b.u.fp64 || (isnan(a.u.fp64) && isnan(b.u.fp64)); - case TOML_BOOLEAN: - return !!a.u.boolean == !!b.u.boolean; - case TOML_DATE: - return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && - a.u.ts.day == b.u.ts.day; - case TOML_TIME: - return a.u.ts.hour == b.u.ts.hour && a.u.ts.minute == b.u.ts.minute && - a.u.ts.second == b.u.ts.second && a.u.ts.usec == b.u.ts.usec; - case TOML_DATETIME: - return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && - a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && - a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && - a.u.ts.usec == b.u.ts.usec; - case TOML_DATETIMETZ: - return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && - a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && - a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && - a.u.ts.usec == b.u.ts.usec && a.u.ts.tz == b.u.ts.tz; - case TOML_ARRAY: - N = a.u.arr.size; - if (N != b.u.arr.size) { - return false; - } - for (int i = 0; i < N; i++) { - if (!datum_equiv(a.u.arr.elem[i], b.u.arr.elem[i])) { - return false; - } - } - return true; - case TOML_TABLE: - N = a.u.tab.size; - if (N != b.u.tab.size) { - return false; - } - for (int i = 0; i < N; i++) { - int len = a.u.tab.len[i]; - if (len != b.u.tab.len[i]) { - return false; - } - if (0 != memcmp(a.u.tab.key[i], b.u.tab.key[i], len)) { - return false; - } - if (!datum_equiv(a.u.tab.value[i], b.u.tab.value[i])) { - return false; - } - } - return true; - default: - break; - } - return false; -} - -/** - * Override values in r1 using r2. Return a new result. All results - * (i.e., r1, r2 and the returned result) must be freed using toml_free() - * after use. - * - * LOGIC: - * ret = copy of r1 - * for each item x in r2: - * if x is not in ret: - * override - * elif x in ret is NOT of the same type: - * override - * elif x is an array of tables: - * append r2.x to ret.x - * elif x is a table: - * merge r2.x to ret.x - * else: - * override - */ -toml_result_t toml_merge(const toml_result_t *r1, const toml_result_t *r2) { - const char *reason = ""; - toml_result_t ret = {0}; - pool_t *pool = 0; - if (!r1->ok) { - reason = "param error: r1 not ok"; - goto bail; - } - if (!r2->ok) { - reason = "param error: r2 not ok"; - goto bail; - } - { - pool_t *r1pool = (pool_t *)r1->__internal; - pool_t *r2pool = (pool_t *)r2->__internal; - pool = pool_create(r1pool->top + r2pool->top); - if (!pool) { - reason = "out of memory"; - goto bail; - } - } - - // Make a copy of r1 - if (datum_copy(&ret.toptab, r1->toptab, pool, &reason)) { - goto bail; - } - - // Merge r2 into the result - if (datum_merge(&ret.toptab, r2->toptab, pool, &reason)) { - goto bail; - } - - ret.ok = 1; - ret.__internal = pool; - return ret; - -bail: - pool_destroy(pool); - snprintf(ret.errmsg, sizeof(ret.errmsg), "%s", reason); - return ret; -} - -bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2) { - if (!(r1->ok && r2->ok)) { - return false; - } - return datum_equiv(r1->toptab, r2->toptab); -} - -/** - * Find a key in a toml_table. Return the value of the key if found, - * or a TOML_UNKNOWN otherwise. - */ -toml_datum_t toml_get(toml_datum_t datum, const char *key) { - if (datum.type == TOML_TABLE) { - int n = datum.u.tab.size; - const char **pkey = datum.u.tab.key; - toml_datum_t *pvalue = datum.u.tab.value; - for (int i = 0; i < n; i++) { - if (0 == strcmp(pkey[i], key)) { - return pvalue[i]; - } - } - } - return DATUM_ZERO; -} - -/** - * Locate a value starting from a toml_table. Return the value of the key if - * found, or a TOML_UNKNOWN otherwise. - * - * Note: the multipart-key is separated by DOT, and must not have any escape - * chars. - */ -toml_datum_t toml_seek(toml_datum_t table, const char *multipart_key) { - if (table.type != TOML_TABLE) { - return DATUM_ZERO; - } - - // Make a mutable copy of the multipart_key for splitting - char buf[256]; - if (copystring(buf, sizeof(buf), multipart_key)) { - // if the multipart_key is longer than buffer, just - // signal a not-found. - return DATUM_ZERO; - } - - // Go through the multipart name part by part. - char *p = buf; - toml_datum_t datum = table; - while (datum.type == TOML_TABLE) { - char *q = strchr(p, '.'); - if (q) { - // traverse to next key - *q = 0; - datum = toml_get(datum, p); - p = q + 1; - continue; - } - - // At end of last keypart. - // look up p in the final table - return toml_get(datum, p); - } - - return DATUM_ZERO; -} - -/** - * Return the default options. - */ -toml_option_t toml_default_option(void) { - toml_option_t opt = {0, realloc, free}; - return opt; -} - -/** - * Override the current options. - */ -void toml_set_option(toml_option_t opt) { toml_option = opt; } - -/** - * Free the result returned by toml_parse(). - */ -void toml_free(toml_result_t result) { - datum_free(&result.toptab); - pool_destroy((pool_t *)result.__internal); -} - -/** - * Parse a toml document. - */ -toml_result_t toml_parse_file_ex(const char *fname) { - toml_result_t result = {0}; - FILE *fp = fopen(fname, "r"); - if (!fp) { - snprintf(result.errmsg, sizeof(result.errmsg), "fopen %s: %s", fname, - strerror(errno)); - return result; - } - result = toml_parse_file(fp); - fclose(fp); - return result; -} - -/** - * Parse a toml document. - */ -toml_result_t toml_parse_file(FILE *fp) { - toml_result_t result = {0}; - char *buf = 0; - int top, max; // index into buf[] - top = max = 0; - - // Read file into memory - while (!feof(fp)) { - assert(top <= max); - if (top == max) { - // need to extend buf[] - int64_t tmpmax64 = (int64_t)max * 3 / 2 + 1000; - int tmpmax = (tmpmax64 > INT_MAX - 1) ? INT_MAX - 1 : (int)tmpmax64; - if (tmpmax == INT_MAX - 1) { - snprintf(result.errmsg, sizeof(result.errmsg), "file is too big"); - FREE(buf); - return result; - } - // add an extra byte for terminating NUL - char *tmp = REALLOC(buf, tmpmax + 1); - if (!tmp) { - snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); - FREE(buf); - return result; - } - buf = tmp; - max = tmpmax; - } - - errno = 0; - top += fread(buf + top, 1, max - top, fp); - if (ferror(fp)) { - snprintf(result.errmsg, sizeof(result.errmsg), "%s", - errno ? strerror(errno) : "Error reading file"); - FREE(buf); - return result; - } - } - buf[top] = 0; // NUL terminator - - result = toml_parse(buf, top); - FREE(buf); - return result; -} - -/** - * Parse a toml document. - */ -toml_result_t toml_parse(const char *src, int len) { - toml_result_t result = {0}; - parser_t parser = {0}; - parser_t *pp = &parser; - - // Check that src is NUL terminated. - if (src[len]) { - snprintf(result.errmsg, sizeof(result.errmsg), - "src[] must be NUL terminated"); - goto bail; - } - - // If user insists, check that src[] is a valid utf8 string. - if (toml_option.check_utf8) { - int line = 1; // keeps track of line number - for (int i = 0; i < len;) { - uint32_t ch; - int n = utf8_to_ucs(src + i, len - i, &ch); - if (n < 0) { - snprintf(result.errmsg, sizeof(result.errmsg), - "invalid UTF8 char on line %d", line); - goto bail; - } - if (0xD800 <= ch && ch <= 0xDFFF) { - // explicitly prohibit surrogates (non-scalar unicode code point) - snprintf(result.errmsg, sizeof(result.errmsg), - "invalid UTF8 char \\u%04x on line %d", ch, line); - goto bail; - } - line += (ch == '\n' ? 1 : 0); - i += n; - } - } - - // Initialize parser - pp->toptab = mkdatum(TOML_TABLE); - pp->curtab = &pp->toptab; - pp->ebuf.ptr = result.errmsg; // parse error will be printed into pp->ebuf - pp->ebuf.len = sizeof(result.errmsg); - - // Alloc memory pool - pp->pool = - pool_create(len + 10); // add some extra bytes for NUL term and safety - if (!pp->pool) { - snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); - goto bail; - } - - // Initialize scanner. Scan error will be printed into pp->ebuf. - scan_init(&pp->scanner, src, len, pp->ebuf.ptr, pp->ebuf.len); - - // Keep parsing until FIN - for (;;) { - token_t tok; - if (scan_key(&pp->scanner, &tok)) { - goto bail; - } - // break on FIN - if (tok.toktyp == TOK_FIN) { - break; - } - switch (tok.toktyp) { - case TOK_ENDL: // skip blank lines - continue; - case TOK_LBRACK: - if (parse_std_table_expr(pp, tok)) { - goto bail; - } - break; - case TOK_LLBRACK: - if (parse_array_table_expr(pp, tok)) { - goto bail; - } - break; - default: - // non-blank line: parse an expression - if (parse_keyvalue_expr(pp, tok)) { - goto bail; - } - break; - } - // each expression must be followed by newline - if (scan_key(&pp->scanner, &tok)) { - goto bail; - } - if (tok.toktyp == TOK_FIN || tok.toktyp == TOK_ENDL) { - continue; - } - SETERROR(pp->ebuf, tok.lineno, "ENDL expected"); - goto bail; - } - - // return result - result.ok = true; - result.toptab = pp->toptab; - result.__internal = (void *)pp->pool; - return result; - -bail: - // return error - datum_free(&pp->toptab); - pool_destroy(pp->pool); - result.ok = false; - if (result.errmsg[0] == '\0') { - assert(0); - snprintf(result.errmsg, sizeof(result.errmsg), "Error near line %d\n", - pp->scanner.lineno); - } - return result; -} - -// Convert a (LITSTRING, LIT, MLLITSTRING, MLSTRING, or STRING) token to a -// datum. -static int token_to_string(parser_t *pp, token_t tok, toml_datum_t *ret) { - *ret = mkdatum(TOML_STRING); - span_t span; - DO(parse_norm(pp, tok, &span)); - ret->u.str.ptr = (char *)span.ptr; - ret->u.str.len = span.len; - return 0; -} - -// Convert a TIME/DATE/DATETIME/DATETIMETZ to a datum -static int token_to_timestamp(parser_t *pp, token_t tok, toml_datum_t *ret) { - (void)pp; - static const toml_type_t map[] = {[TOK_TIME] = TOML_TIME, - [TOK_DATE] = TOML_DATE, - [TOK_DATETIME] = TOML_DATETIME, - [TOK_DATETIMETZ] = TOML_DATETIMETZ}; - switch (tok.toktyp) { - case TOK_TIME: - case TOK_DATE: - case TOK_DATETIME: - case TOK_DATETIMETZ: - break; - default: - assert(0 && "unexpected token type"); - return -1; - } - - *ret = mkdatum(map[tok.toktyp]); - ret->u.ts.year = tok.u.tsval.year; - ret->u.ts.month = tok.u.tsval.month; - ret->u.ts.day = tok.u.tsval.day; - ret->u.ts.hour = tok.u.tsval.hour; - ret->u.ts.minute = tok.u.tsval.minute; - ret->u.ts.second = tok.u.tsval.sec; - ret->u.ts.usec = tok.u.tsval.usec; - ret->u.ts.tz = tok.u.tsval.tz; - return 0; -} - -// Convert an int64 token to a datum. -static int token_to_int64(parser_t *pp, token_t tok, toml_datum_t *ret) { - (void)pp; - assert(tok.toktyp == TOK_INTEGER); - *ret = mkdatum(TOML_INT64); - ret->u.int64 = tok.u.int64; - return 0; -} - -// Convert a fp64 token to a datum. -static int token_to_fp64(parser_t *pp, token_t tok, toml_datum_t *ret) { - (void)pp; - assert(tok.toktyp == TOK_FLOAT); - *ret = mkdatum(TOML_FP64); - ret->u.fp64 = tok.u.fp64; - return 0; -} - -// Convert a boolean token to a datum. -static int token_to_boolean(parser_t *pp, token_t tok, toml_datum_t *ret) { - (void)pp; - assert(tok.toktyp == TOK_BOOL); - *ret = mkdatum(TOML_BOOLEAN); - ret->u.boolean = tok.u.b1; - return 0; -} - -// Parse a multipart key. Return 0 on success, -1 otherwise. -static int parse_key(parser_t *pp, token_t tok, keypart_t *ret_keypart) { - ret_keypart->nspan = 0; - // key = simple-key | dotted_key - // simple-key = STRING | LITSTRING | LIT - // dotted-key = simple-key (DOT simple-key)+ - if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && - tok.toktyp != TOK_LIT) { - return SETERROR(pp->ebuf, tok.lineno, "missing key"); - } - - int n = 0; - span_t *kpspan = ret_keypart->span; - - // Normalize the first keypart - if (parse_norm(pp, tok, &kpspan[n])) { - return SETERROR(pp->ebuf, tok.lineno, - "unable to normalize string; probably a unicode issue"); - } - n++; - - // Scan and normalize the second to last keypart - while (1) { - scanner_state_t mark = scan_mark(&pp->scanner); - - // Eat the dot if it is there - DO(scan_key(&pp->scanner, &tok)); - - // If not a dot, we are done with keyparts. - if (tok.toktyp != TOK_DOT) { - scan_restore(&pp->scanner, mark); - break; - } - - // Scan the n-th key - DO(scan_key(&pp->scanner, &tok)); - - if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && - tok.toktyp != TOK_LIT) { - return SETERROR(pp->ebuf, tok.lineno, "expects a string in dotted-key"); - } - - if (n >= KEYPARTMAX) { - return SETERROR(pp->ebuf, tok.lineno, "too many key parts"); - } - - // Normalize the n-th key. - DO(parse_norm(pp, tok, &kpspan[n])); - n++; - } - - // This key has n parts. - ret_keypart->nspan = n; - return 0; -} - -// Starting at toptab, descend following keypart[]. If a key does not -// exist in the current table, create a new table entry for the -// key. Returns the final table represented by the key. -static toml_datum_t *descend_keypart(parser_t *pp, int lineno, - toml_datum_t *toptab, keypart_t *keypart, - bool stdtabexpr) { - toml_datum_t *tab = toptab; // current tab - - for (int i = 0; i < keypart->nspan; i++) { - const char *reason; - // Find the i-th keypart - int j = tab_find(tab, keypart->span[i]); - // Not found: add a new (key, tab) pair. - if (j < 0) { - toml_datum_t newtab = mkdatum(TOML_TABLE); - newtab.flag |= stdtabexpr ? FLAG_STDEXPR : 0; - if (tab_add(tab, keypart->span[i], newtab, &reason)) { - SETERROR(pp->ebuf, lineno, "%s", reason); - return NULL; - } - tab = &tab->u.tab.value[tab->u.tab.size - 1]; // descend - continue; - } - - // Found: extract the value of the key. - toml_datum_t *value = &tab->u.tab.value[j]; - - // If the value is a table, descend. - if (value->type == TOML_TABLE) { - tab = value; // descend - continue; - } - - // If the value is an array: locate the last entry and descend. - if (value->type == TOML_ARRAY) { - // If empty: error. - if (value->u.arr.size <= 0) { - SETERROR(pp->ebuf, lineno, "array %s has no elements", - keypart->span[i].ptr); - return NULL; - } - - // Extract the last element of the array. - value = &value->u.arr.elem[value->u.arr.size - 1]; - - // It must be a table! - if (value->type != TOML_TABLE) { - SETERROR(pp->ebuf, lineno, "array %s must be array of tables", - keypart->span[i].ptr); - return NULL; - } - tab = value; // descend - continue; - } - - // key not found - SETERROR(pp->ebuf, lineno, "cannot locate table at key %s", - keypart->span[i].ptr); - return NULL; - } - - // Return the table corresponding to the keypart[]. - return tab; -} - -// Recursively set flags on datum -static void set_flag_recursive(toml_datum_t *datum, uint32_t flag) { - datum->flag |= flag; - switch (datum->type) { - case TOML_ARRAY: - for (int i = 0, top = datum->u.arr.size; i < top; i++) { - set_flag_recursive(&datum->u.arr.elem[i], flag); - } - break; - case TOML_TABLE: - for (int i = 0, top = datum->u.tab.size; i < top; i++) { - set_flag_recursive(&datum->u.tab.value[i], flag); - } - break; - default: - break; - } -} - -// Parse an inline array. -static int parse_inline_array(parser_t *pp, token_t tok, - toml_datum_t *ret_datum) { - assert(tok.toktyp == TOK_LBRACK); - *ret_datum = mkdatum(TOML_ARRAY); - int need_comma = 0; - - // loop until RBRACK - for (;;) { - // skip ENDL - do { - DO(scan_value(&pp->scanner, &tok)); - } while (tok.toktyp == TOK_ENDL); - - // If got an RBRACK: done! - if (tok.toktyp == TOK_RBRACK) { - break; - } - - // If got a COMMA: check if it is expected. - if (tok.toktyp == TOK_COMMA) { - if (need_comma) { - need_comma = 0; - continue; - } - return SETERROR(pp->ebuf, tok.lineno, - "syntax error while parsing array: unexpected comma"); - } - - // Not a comma, but need a comma: error! - if (need_comma) { - return SETERROR(pp->ebuf, tok.lineno, - "syntax error while parsing array: missing comma"); - } - - // This is a valid value! Obtain the value. - toml_datum_t value = DATUM_ZERO; - if (parse_val(pp, tok, &value)) { - datum_free(&value); - return -1; - } - - // Add the value to the array. - const char *reason; - toml_datum_t *pelem = arr_emplace(ret_datum, &reason); - if (!pelem) { - datum_free(&value); - return SETERROR(pp->ebuf, tok.lineno, "while parsing array: %s", reason); - } - *pelem = value; - - // Need comma before the next value. - need_comma = 1; - } - - // Set the INLINE flag for all things in this array. - set_flag_recursive(ret_datum, FLAG_INLINED); - return 0; -} - -// Parse an inline table. -static int parse_inline_table(parser_t *pp, token_t tok, - toml_datum_t *ret_datum) { - assert(tok.toktyp == TOK_LBRACE); - *ret_datum = mkdatum(TOML_TABLE); - bool need_comma = 0; - bool was_comma = 0; - - // loop until RBRACE - for (;;) { - DO(scan_key(&pp->scanner, &tok)); - - // Got an RBRACE: done! - if (tok.toktyp == TOK_RBRACE) { - if (was_comma) { - /* - return SETERROR(pp->ebuf, tok.lineno, - "extra comma before closing brace"); - */ - // extra comma before RBRACE is allowed for v1.1 - (void)0; - } - break; - } - - // Got a comma: check if it is expected. - if (tok.toktyp == TOK_COMMA) { - if (need_comma) { - need_comma = 0, was_comma = 1; - continue; - } - return SETERROR(pp->ebuf, tok.lineno, "unexpected comma"); - } - - // Newline not allowed in inline table. - // newline is allowed in v1.1 - if (tok.toktyp == TOK_ENDL) { - // return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); - continue; - } - - // Not a comma, but need a comma: error! - if (need_comma) { - return SETERROR(pp->ebuf, tok.lineno, "missing comma"); - } - - // Get the keyparts - keypart_t keypart = {0}; - int keylineno = tok.lineno; - DO(parse_key(pp, tok, &keypart)); - - // Descend to one keypart before last - span_t lastkeypart = keypart.span[--keypart.nspan]; - toml_datum_t *tab = - descend_keypart(pp, keylineno, ret_datum, &keypart, false); - if (!tab) { - return -1; - } - - // If tab is a previously declared inline table: error. - if (tab->flag & FLAG_INLINED) { - return SETERROR(pp->ebuf, tok.lineno, "inline table cannot be extended"); - } - - // We are explicitly defining it now. - tab->flag |= FLAG_EXPLICIT; - - // match EQUAL - DO(scan_value(&pp->scanner, &tok)); - - if (tok.toktyp != TOK_EQUAL) { - if (tok.toktyp == TOK_ENDL) { - return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); - } else { - return SETERROR(pp->ebuf, tok.lineno, "missing '='"); - } - } - - // obtain the value - DO(scan_value(&pp->scanner, &tok)); - toml_datum_t value = DATUM_ZERO; - if (parse_val(pp, tok, &value)) { - datum_free(&value); - return -1; - } - - // Add the value to tab. - const char *reason; - if (tab_add(tab, lastkeypart, value, &reason)) { - datum_free(&value); - return SETERROR(pp->ebuf, tok.lineno, "%s", reason); - } - need_comma = 1, was_comma = 0; - } - - set_flag_recursive(ret_datum, FLAG_INLINED); - return 0; -} - -// Parse a value. -static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret) { - *ret = DATUM_ZERO; // initialize - - // val = string / boolean / array / inline-table / date-time / float / integer - switch (tok.toktyp) { - case TOK_STRING: - case TOK_MLSTRING: - case TOK_LITSTRING: - case TOK_MLLITSTRING: - return token_to_string(pp, tok, ret); - case TOK_TIME: - case TOK_DATE: - case TOK_DATETIME: - case TOK_DATETIMETZ: - return token_to_timestamp(pp, tok, ret); - case TOK_INTEGER: - return token_to_int64(pp, tok, ret); - case TOK_FLOAT: - return token_to_fp64(pp, tok, ret); - case TOK_BOOL: - return token_to_boolean(pp, tok, ret); - case TOK_LBRACK: // inline-array - return parse_inline_array(pp, tok, ret); - case TOK_LBRACE: // inline-table - return parse_inline_table(pp, tok, ret); - default: - break; - } - return SETERROR(pp->ebuf, tok.lineno, "missing value"); -} - -// Parse a standard table expression, and set the curtab of the parser -// to the table referenced. A standard table expression is a line -// like [a.b.c.d]. -static int parse_std_table_expr(parser_t *pp, token_t tok) { - // std-table = [ key ] - // Eat the [ - assert(tok.toktyp == TOK_LBRACK); // [ ate by caller - - // Read the first keypart - DO(scan_key(&pp->scanner, &tok)); - - // Extract the keypart[] - int keylineno = tok.lineno; - keypart_t keypart; - DO(parse_key(pp, tok, &keypart)); - - // Eat the ] - DO(scan_key(&pp->scanner, &tok)); - if (tok.toktyp != TOK_RBRACK) { - return SETERROR(pp->ebuf, tok.lineno, "missing right-bracket"); - } - - // Descend to one keypart before last. - span_t lastkeypart = keypart.span[--keypart.nspan]; - - // Descend keypart from the toptab. - toml_datum_t *tab = - descend_keypart(pp, keylineno, &pp->toptab, &keypart, true); - if (!tab) { - return -1; - } - - // Look for the last keypart in the final tab - int j = tab_find(tab, lastkeypart); - if (j < 0) { - // If not found: add it. - if (tab->flag & FLAG_INLINED) { - return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); - } - const char *reason; - toml_datum_t newtab = mkdatum(TOML_TABLE); - newtab.flag |= FLAG_STDEXPR; - if (tab_add(tab, lastkeypart, newtab, &reason)) { - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - // this is the new tab - tab = &tab->u.tab.value[tab->u.tab.size - 1]; - } else { - // Found: check for errors - tab = &tab->u.tab.value[j]; - if (tab->flag & FLAG_EXPLICIT) { - /* - This is not OK: - [x.y.z] - [x.y.z] - - but this is OK: - [x.y.z] - [x] - */ - return SETERROR(pp->ebuf, keylineno, "table defined more than once"); - } - if (!(tab->flag & FLAG_STDEXPR)) { - /* - [t1] # OK - t2.t3.v = 0 # OK - [t1.t2] # should FAIL - t2 was non-explicit but was not - created by std-table-expr - */ - return SETERROR(pp->ebuf, keylineno, "table defined before"); - } - } - - // Set explicit flag on tab - tab->flag |= FLAG_EXPLICIT; - - // Set tab as curtab of the parser - pp->curtab = tab; - return 0; -} - -// Parse an array table expression, and set the curtab of the parser -// to the table referenced. A standard array table expresison is a line -// like [[a.b.c.d]]. -static int parse_array_table_expr(parser_t *pp, token_t tok) { - // array-table = [[ key ]] - assert(tok.toktyp == TOK_LLBRACK); // [[ ate by caller - - // Read the first keypart - DO(scan_key(&pp->scanner, &tok)); - - int keylineno = tok.lineno; - keypart_t keypart; - DO(parse_key(pp, tok, &keypart)); - - // eat the ]] - token_t rrb; - DO(scan_key(&pp->scanner, &rrb)); - if (rrb.toktyp != TOK_RRBRACK) { - return SETERROR(pp->ebuf, rrb.lineno, "missing ']]'"); - } - - // remove the last keypart from keypart[] - span_t lastkeypart = keypart.span[--keypart.nspan]; - - // descend the key from the toptab - toml_datum_t *tab = &pp->toptab; - for (int i = 0; i < keypart.nspan; i++) { - span_t curkey = keypart.span[i]; - int j = tab_find(tab, curkey); - if (j < 0) { - // If not found: add a new (key,tab) pair - const char *reason; - toml_datum_t newtab = mkdatum(TOML_TABLE); - newtab.flag |= FLAG_STDEXPR; - if (tab_add(tab, curkey, newtab, &reason)) { - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - tab = &tab->u.tab.value[tab->u.tab.size - 1]; - continue; - } - - // Found: get the value - toml_datum_t *value = &tab->u.tab.value[j]; - - // If value is table, then point to that table and continue descent. - if (value->type == TOML_TABLE) { - tab = value; - continue; - } - - // If value is an array of table, point to the last element of the array and - // continue descent. - if (value->type == TOML_ARRAY) { - if (value->flag & FLAG_INLINED) { - return SETERROR(pp->ebuf, keylineno, "cannot expand array %s", - curkey.ptr); - } - if (value->u.arr.size <= 0) { - return SETERROR(pp->ebuf, keylineno, "array %s has no elements", - curkey.ptr); - } - value = &value->u.arr.elem[value->u.arr.size - 1]; - if (value->type != TOML_TABLE) { - return SETERROR(pp->ebuf, keylineno, "array %s must be array of tables", - curkey.ptr); - } - tab = value; - continue; - } - - // keypart not found - return SETERROR(pp->ebuf, keylineno, "cannot locate table at key %s", - curkey.ptr); - } - - // For the final keypart, make sure entry at key is an array of tables - const char *reason; - int idx = tab_find(tab, lastkeypart); - if (idx == -1) { - // If not found, add an array of table. - if (tab_add(tab, lastkeypart, mkdatum(TOML_ARRAY), &reason)) { - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - idx = tab_find(tab, lastkeypart); - assert(idx >= 0); - } - // Check that this is an array. - if (tab->u.tab.value[idx].type != TOML_ARRAY) { - return SETERROR(pp->ebuf, keylineno, "entry must be an array"); - } - // Add an empty table to the array - toml_datum_t *arr = &tab->u.tab.value[idx]; - if (arr->flag & FLAG_INLINED) { - return SETERROR(pp->ebuf, keylineno, "cannot extend a static array"); - } - toml_datum_t *pelem = arr_emplace(arr, &reason); - if (!pelem) { - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - *pelem = mkdatum(TOML_TABLE); - - // Set the last element of this array as curtab of the parser - pp->curtab = &arr->u.arr.elem[arr->u.arr.size - 1]; - assert(pp->curtab->type == TOML_TABLE); - - return 0; -} - -// Parse an expression. A toml doc is just a list of expressions. -static int parse_keyvalue_expr(parser_t *pp, token_t tok) { - // Obtain the key - int keylineno = tok.lineno; - keypart_t keypart; - DO(parse_key(pp, tok, &keypart)); - - // match the '=' - DO(scan_key(&pp->scanner, &tok)); - if (tok.toktyp != TOK_EQUAL) { - return SETERROR(pp->ebuf, tok.lineno, "expect '='"); - } - - // Locate the last table using keypart[] - const char *reason; - toml_datum_t *tab = pp->curtab; - for (int i = 0; i < keypart.nspan - 1; i++) { - int j = tab_find(tab, keypart.span[i]); - if (j < 0) { - if (i > 0 && (tab->flag & FLAG_EXPLICIT)) { - return SETERROR( - pp->ebuf, keylineno, - "cannot extend a previously defined table using dotted expression"); - } - toml_datum_t newtab = mkdatum(TOML_TABLE); - if (tab_add(tab, keypart.span[i], newtab, &reason)) { - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - tab = &tab->u.tab.value[tab->u.tab.size - 1]; - continue; - } - toml_datum_t *value = &tab->u.tab.value[j]; - if (value->type == TOML_TABLE) { - tab = value; - continue; - } - if (value->type == TOML_ARRAY) { - return SETERROR(pp->ebuf, keylineno, - "encountered previously declared array '%s'", - keypart.span[i].ptr); - } - return SETERROR(pp->ebuf, keylineno, "cannot locate table at '%s'", - keypart.span[i].ptr); - } - - // Check for disallowed situations. - if (tab->flag & FLAG_INLINED) { - return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); - } - if (keypart.nspan > 1 && (tab->flag & FLAG_EXPLICIT)) { - return SETERROR( - pp->ebuf, keylineno, - "cannot extend a previously defined table using dotted expression"); - } - - // Obtain the value - DO(scan_value(&pp->scanner, &tok)); - toml_datum_t newval = DATUM_ZERO; - if (parse_val(pp, tok, &newval)) { - datum_free(&newval); - return -1; - } - - // Add a new key/value for tab. - if (tab_add(tab, keypart.span[keypart.nspan - 1], newval, &reason)) { - datum_free(&newval); - return SETERROR(pp->ebuf, keylineno, "%s", reason); - } - - return 0; -} - -// Normalize a LIT/STRING/MLSTRING/LITSTRING/MLLITSTRING -// -> unescape all escaped chars -// The returned string is allocated out of pp->sbuf[] -static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span) { - // Allocate a buffer to store the normalized string. Add one - // extra-byte for terminating NUL. - char *p = pool_alloc(pp->pool, tok.str.len + 1); - if (!p) { - return SETERROR(pp->ebuf, tok.lineno, "out of memory"); - } - - // Copy from token string into buffer - memcpy(p, tok.str.ptr, tok.str.len); - p[tok.str.len] = 0; // additional NUL term for safety - - ret_span->ptr = p; - ret_span->len = tok.str.len; - - switch (tok.toktyp) { - case TOK_LIT: - case TOK_LITSTRING: - case TOK_MLLITSTRING: - // no need to handle escape chars - return 0; - - case TOK_STRING: - case TOK_MLSTRING: - // need to handle escape chars - break; - - default: - return SETERROR(pp->ebuf, 0, "internal: arg must be a string"); - } - - // if there is no escape char, then done! - if (!tok.u.escp) { - return 0; // success - } - - // p points to the backslash - p += (tok.u.escp - tok.str.ptr); - assert(p - ret_span->ptr == tok.u.escp - tok.str.ptr); - assert(*p == '\\'); - - // Normalize the escaped chars - char *dst = p; - while (*p) { - if (*p != '\\') { - *dst++ = *p++; - continue; - } - switch (p[1]) { - case '"': - case '\\': - *dst++ = p[1]; - p += 2; - continue; - case 'b': - *dst++ = '\b'; - p += 2; - continue; - case 't': - *dst++ = '\t'; - p += 2; - continue; - case 'n': - *dst++ = '\n'; - p += 2; - continue; - case 'f': - *dst++ = '\f'; - p += 2; - continue; - case 'r': - *dst++ = '\r'; - p += 2; - continue; - case 'e': - *dst++ = '\033'; - p += 2; - continue; - case 'x': { - char buf[3]; - memcpy(buf, p + 2, 2); - buf[2] = 0; - // There is no need to check for two hex digits here because - // the scanner already checked it. - int32_t ucs = strtol(buf, 0, 16); - int n = ucs_to_utf8(ucs, dst); - if (n < 0) { - return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", - buf); - } - dst += n; - p += 2 + 2; // \xNN - continue; - } - case 'u': - case 'U': { - char buf[9]; - int sz = (p[1] == 'u' ? 4 : 8); - memcpy(buf, p + 2, sz); - buf[sz] = 0; - // There is no need to check for 4 or 8 hex digits here because - // the scanner already checked it. - int32_t ucs = strtol(buf, 0, 16); - if (0xD800 <= ucs && ucs <= 0xDFFF) { - // explicitly prohibit surrogates (non-scalar unicode code point) - return SETERROR(pp->ebuf, tok.lineno, "invalid UTF8 char \\u%04x", ucs); - } - int n = ucs_to_utf8(ucs, dst); - if (n < 0) { - return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", - buf); - } - dst += n; - p += 2 + sz; // \uNNNN or \UNNNNNNNN - continue; - } - - case ' ': - case '\t': - case '\r': - // line-ending backslash - // --- allow for extra whitespace chars after backslash - // --- skip until newline - p++; // skip the escape char - p += strspn(p, " \t\r"); // skip whitespaces - if (*p != '\n') { - return SETERROR(pp->ebuf, tok.lineno, - "unexpected char after line-ending backslash"); - } - // fallthru - case '\n': - // skip all whitespaces including newline - p++; - p += strspn(p, " \t\r\n"); - continue; - default: - return SETERROR(pp->ebuf, tok.lineno, - "internal: unknown escape char \\%c", p[1]); - } - } - *dst = 0; - ret_span->len = dst - ret_span->ptr; - return 0; -} - -// =================================================================== -// == SCANNER SECTION -// =================================================================== - -// Get the next char -static int scan_get(scanner_t *sp) { - int ret = TOK_FIN; - const char *p = sp->cur; - if (p < sp->endp) { - ret = *p++; - if (ret == '\r' && p < sp->endp && *p == '\n') { - ret = *p++; - } - } - sp->cur = p; - sp->lineno += (ret == '\n' ? 1 : 0); - return ret; -} - -// Check if the next char matches ch. -static inline bool scan_match(scanner_t *sp, int ch) { - const char *p = sp->cur; - // exact match? done. - if (p < sp->endp && *p == ch) { - return true; - } - // \n also matches \r\n - if (ch == '\n' && p + 1 < sp->endp) { - return p[0] == '\r' && p[1] == '\n'; - } - // not a match - return false; -} - -// Check if the next char is in accept[]. -static bool scan_matchany(scanner_t *sp, const char *accept) { - for (; *accept; accept++) { - if (scan_match(sp, *accept)) { - return true; - } - } - return false; -} - -// Check if the next n chars match ch. -static inline bool scan_nmatch(scanner_t *sp, int ch, int n) { - assert(ch != '\n'); // not handled - if (sp->cur + n > sp->endp) { - return false; - } - const char *p = sp->cur; - int i; - for (i = 0; i < n && p[i] == ch; i++) - ; - return i == n; -} - -// Initialize a token. -static inline token_t mktoken(scanner_t *sp, toktyp_t typ) { - token_t tok = {0}; - tok.toktyp = typ; - tok.str.ptr = sp->cur; - tok.lineno = sp->lineno; - return tok; -} - -#define S_GET() scan_get(sp) -#define S_MATCH(ch) scan_match(sp, (ch)) -#define S_MATCH3(ch) scan_nmatch(sp, (ch), 3) -#define S_MATCH4(ch) scan_nmatch(sp, (ch), 4) -#define S_MATCH6(ch) scan_nmatch(sp, (ch), 6) - -static inline bool is_valid_char(int ch) { - // i.e. (0x20 <= ch && ch <= 0x7e) || (ch & 0x80); - return isprint(ch) || (ch & 0x80); -} - -static inline bool is_hex_char(int ch) { - ch = toupper(ch); - return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F'); -} - -// Initialize a scanner -static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, - int errbufsz) { - memset(sp, 0, sizeof(*sp)); - sp->src = src; - sp->endp = src + len; - assert(*sp->endp == '\0'); - sp->cur = src; - sp->lineno = 1; - sp->ebuf.ptr = errbuf; - sp->ebuf.len = errbufsz; -} - -static int scan_multiline_string(scanner_t *sp, token_t *tok) { - assert(S_MATCH3('"')); - S_GET(), S_GET(), S_GET(); // skip opening """ - - // According to spec: trim first newline after """ - if (S_MATCH('\n')) { - S_GET(); - } - - *tok = mktoken(sp, TOK_MLSTRING); - // scan until terminating """ - const char *escp = NULL; - while (1) { - if (S_MATCH3('"')) { - if (S_MATCH4('"')) { - // special case... """abcd """" -> (abcd ") - // but sequences of 3 or more double quotes are not allowed - if (S_MATCH6('"')) { - return SETERROR(sp->ebuf, sp->lineno, - "detected sequences of 3 or more double quotes"); - } else { - ; // no problem - } - } else { - break; // found terminating """ - } - } - int ch = S_GET(); - if (ch == TOK_FIN) { - return SETERROR(sp->ebuf, sp->lineno, "unterminated \"\"\""); - } - // If non-escaped char ... - if (ch != '\\') { - if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { - return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); - } - continue; - } - // ch is backslash - if (!escp) { - escp = sp->cur - 1; - assert(*escp == '\\'); - } - - // handle escape char - ch = S_GET(); - if (ch && strchr("btnfre\"\\", ch)) { - // skip \", \\, \b, \f, \n, \r, \t - continue; - } - int top = 0; - switch (ch) { - case 'x': - top = 2; - break; - case 'u': - top = 4; - break; - case 'U': - top = 8; - break; - default: - break; - } - if (top) { - for (int i = 0; i < top; i++) { - if (!is_hex_char(S_GET())) { - return SETERROR(sp->ebuf, sp->lineno, - "expect %d hex digits after \\%c", top, ch); - } - } - continue; - } - // handle line-ending backslash - if (ch == ' ' || ch == '\t') { - // Although the spec does not allow for whitespace following a - // line-ending backslash, some standard tests expect it. - // Skip whitespace till EOL. - while (ch != TOK_FIN && ch && strchr(" \t", ch)) { - ch = S_GET(); - } - if (ch != '\n') { - // Got a backslash followed by whitespace, followed by some char - // before newline - return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); - } - // fallthru - } - if (ch == '\n') { - // got a line-ending backslash - // - skip all whitespaces - while (scan_matchany(sp, " \t\n")) { - S_GET(); - } - continue; - } - return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); - } - tok->str.len = sp->cur - tok->str.ptr; - tok->u.escp = escp; - - assert(S_MATCH3('"')); - S_GET(), S_GET(), S_GET(); - return 0; -} - -static int scan_string(scanner_t *sp, token_t *tok) { - assert(S_MATCH('"')); - if (S_MATCH3('"')) { - return scan_multiline_string(sp, tok); - } - S_GET(); // skip opening " - - // scan until closing " - *tok = mktoken(sp, TOK_STRING); - const char *escp = NULL; - while (!S_MATCH('"')) { - int ch = S_GET(); - if (ch == TOK_FIN) { - return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); - } - // If non-escaped char ... - if (ch != '\\') { - if (!(is_valid_char(ch) || ch == ' ' || ch == '\t')) { - return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); - } - continue; - } - // ch is backslash - if (!escp) { - escp = sp->cur - 1; - assert(*escp == '\\'); - } - - // handle escape char - ch = S_GET(); - if (ch && strchr("btnfre\"\\", ch)) { - // skip \b, \t, \n, \f, \r, \e, \", \\ . - continue; - } - int top = 0; - switch (ch) { - case 'x': - top = 2; - break; - case 'u': - top = 4; - break; - case 'U': - top = 8; - break; - default: - return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); - } - for (int i = 0; i < top; i++) { - if (!is_hex_char(S_GET())) { - return SETERROR(sp->ebuf, sp->lineno, "expect %d hex digits after \\%c", - top, ch); - } - } - } - tok->str.len = sp->cur - tok->str.ptr; - tok->u.escp = escp; - - assert(S_MATCH('"')); - S_GET(); // skip the terminating " - return 0; -} - -static int scan_multiline_litstring(scanner_t *sp, token_t *tok) { - assert(S_MATCH3('\'')); - S_GET(), S_GET(), S_GET(); // skip opening ''' - - // According to spec: trim first newline after ''' - if (S_MATCH('\n')) { - S_GET(); - } - - // scan until terminating ''' - *tok = mktoken(sp, TOK_MLLITSTRING); - while (1) { - if (S_MATCH3('\'')) { - if (S_MATCH4('\'')) { - // special case... '''abcd '''' -> (abcd ') - // but sequences of 3 or more single quotes are not allowed - if (S_MATCH6('\'')) { - return SETERROR(sp->ebuf, sp->lineno, - "sequences of 3 or more single quotes"); - } else { - ; // no problem - } - } else { - break; // found terminating ''' - } - } - int ch = S_GET(); - if (ch == TOK_FIN) { - return SETERROR(sp->ebuf, sp->lineno, - "unterminated multiline lit string"); - } - if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { - return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); - } - } - tok->str.len = sp->cur - tok->str.ptr; - - assert(S_MATCH3('\'')); - S_GET(), S_GET(), S_GET(); - return 0; -} - -static int scan_litstring(scanner_t *sp, token_t *tok) { - assert(S_MATCH('\'')); - if (S_MATCH3('\'')) { - return scan_multiline_litstring(sp, tok); - } - S_GET(); // skip opening ' - - // scan until closing ' - *tok = mktoken(sp, TOK_LITSTRING); - while (!S_MATCH('\'')) { - int ch = S_GET(); - if (ch == TOK_FIN) { - return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); - } - if (!(is_valid_char(ch) || ch == '\t')) { - return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); - } - } - tok->str.len = sp->cur - tok->str.ptr; - assert(S_MATCH('\'')); - S_GET(); - return 0; -} - -static bool is_valid_date(int year, int month, int day) { - if (!(1 <= year)) { - return false; - } - if (!(1 <= month && month <= 12)) { - return false; - } - int is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); - int days_in_month[] = { - 31, 28 + is_leap_year, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - return (1 <= day && day <= days_in_month[month - 1]); -} - -static bool is_valid_time(int hour, int minute, int sec, int usec) { - if (!(0 <= hour && hour <= 23)) { - return false; - } - if (!(0 <= minute && minute <= 59)) { - return false; - } - if (!(0 <= sec && sec <= 59)) { - return false; - } - if (!(0 <= usec)) { - return false; - } - return true; -} - -static bool is_valid_timezone(int minute) { - minute = (minute < 0 ? -minute : minute); - int hour = minute / 60; - minute = minute % 60; - if (!(0 <= hour && hour <= 23)) { - return false; - } - if (!(0 <= minute && minute < 60)) { - return false; - } - return true; -} - -// Read an int (without signs) from the string p. -static int read_int(const char *p, int *ret) { - const char *pp = p; - int val = 0; - for (; isdigit(*p); p++) { - val = val * 10u + (*p - '0'); - if (val < 0) { - return 0; // overflowed - } - } - *ret = val; - return p - pp; -} - -// Read a date as YYYY-MM-DD from p[]. Return #bytes consumed. -static int read_date(const char *p, int *year, int *month, int *day) { - const char *pp = p; - int n; - n = read_int(p, year); - if (n != 4 || p[4] != '-') { - return 0; - } - n = read_int(p += n + 1, month); - if (n != 2 || p[2] != '-') { - return 0; - } - n = read_int(p += n + 1, day); - if (n != 2) { - return 0; - } - p += 2; - assert(p - pp == 10); - return p - pp; -} - -// Read a time as HH:MM:SS.subsec from p[]. Return #bytes consumed. -static int read_time(const char *p, int *hour, int *minute, int *second, - int *usec) { - const char *pp = p; - int n; - *hour = *minute = *second = *usec = 0; - // scan hours - n = read_int(p, hour); - if (n != 2 || p[2] != ':') { - return 0; - } - p += 3; - - // scan minutes - n = read_int(p, minute); - if (n != 2) { - return 0; - } - if (p[2] != ':') { - // seconds are optional in v1.1 - p += 2; - return p - pp; - } - p += 3; - - // scan seconds - n = read_int(p, second); - if (n != 2) { - return 0; - } - p += 2; - - if (*p != '.') { - return p - pp; - } - p++; // skip the period - if (!isdigit(*p)) { - // trailing period - return 0; - } - int micro_factor = 100000; - while (isdigit(*p) && micro_factor) { - *usec += (*p - '0') * micro_factor; - micro_factor /= 10; - p++; - } - return p - pp; -} - -// Reads a timezone from p[]. Return #bytes consumed. -// tzhours and tzminutes restricted to 2-char integers only. -static int read_tzone(const char *p, char *tzsign, int *tzhour, int *tzminute) { - const char *pp = p; - - // Default values - *tzhour = *tzminute = 0; - *tzsign = '+'; - - // Look for Zulu - if (*p == 'Z' || *p == 'z') { - return 1; // done! tz is +00:00. - } - - // Look for +/- - *tzsign = *p++; - if (!(*tzsign == '+' || *tzsign == '-')) { - return 0; - } - - // Look for HH:MM - int n; - n = read_int(p, tzhour); - if (n != 2 || p[2] != ':') { - return 0; - } - n = read_int(p += 3, tzminute); - if (n != 2) { - return 0; - } - p += 2; - return p - pp; -} - -static int scan_time(scanner_t *sp, token_t *tok) { - int lineno = sp->lineno; - char buffer[20]; - scan_copystr(sp, buffer, sizeof(buffer)); - - char *p = buffer; - int hour, minute, sec, usec; - int len = read_time(p, &hour, &minute, &sec, &usec); - if (len == 0) { - return SETERROR(sp->ebuf, lineno, "invalid time"); - } - if (!is_valid_time(hour, minute, sec, usec)) { - return SETERROR(sp->ebuf, lineno, "invalid time"); - } - - *tok = mktoken(sp, TOK_TIME); - tok->str.len = len; - sp->cur += len; - tok->u.tsval.year = -1; - tok->u.tsval.month = -1; - tok->u.tsval.day = -1; - tok->u.tsval.hour = hour; - tok->u.tsval.minute = minute; - tok->u.tsval.sec = sec; - tok->u.tsval.usec = usec; - tok->u.tsval.tz = -1; - return 0; -} - -static int scan_timestamp(scanner_t *sp, token_t *tok) { - int year, month, day, hour, minute, sec, usec, tz; - year = month = day = hour = minute = sec = usec = tz = -1; - - int n; - // make a copy of sp->cur into buffer to ensure NUL terminated string - char buffer[80]; - scan_copystr(sp, buffer, sizeof(buffer)); - - toktyp_t toktyp = TOK_FIN; - int lineno = sp->lineno; - - // See if this a TIME only - const char *p = buffer; - if (isdigit(p[0]) && isdigit(p[1]) && p[2] == ':') { - n = read_time(buffer, &hour, &minute, &sec, &usec); - if (!n) { - return SETERROR(sp->ebuf, lineno, "invalid time"); - } - toktyp = TOK_TIME; - p += n; - goto done; - } - - // Try reading a DATE - n = read_date(p, &year, &month, &day); - if (!n) { - return SETERROR(sp->ebuf, lineno, "invalid date"); - } - toktyp = TOK_DATE; - p += n; - - // Check if there is no time component in addition - if (!((p[0] == 'T' || p[0] == ' ' || p[0] == 't') && isdigit(p[1]) && - isdigit(p[2]) && p[3] == ':')) { - goto done; // no TIME component. we are done. - } - - // Read the TIME - n = read_time(p += 1, &hour, &minute, &sec, &usec); - if (!n) { - return SETERROR(sp->ebuf, lineno, "invalid timestamp"); - } - toktyp = TOK_DATETIME; - p += n; - - // Read the (optional) timezone - char tzsign; - int tzhour, tzminute; - n = read_tzone(p, &tzsign, &tzhour, &tzminute); - if (n == 0) { - goto done; // datetime only - } - toktyp = TOK_DATETIMETZ; - p += n; - - // Check tzminute range. This must be done here instead of is_valid_timezone() - // because we combine tzhour and tzminute into tz (by minutes only). - if (!(0 <= tzminute && tzminute < 60)) { - return SETERROR(sp->ebuf, lineno, "invalid timezone"); - } - tz = (tzhour * 60 + tzminute) * (tzsign == '-' ? -1 : 1); - goto done; // datetimetz - -done: - *tok = mktoken(sp, toktyp); - n = p - buffer; - tok->str.len = n; - sp->cur += n; - - tok->u.tsval.year = year; - tok->u.tsval.month = month; - tok->u.tsval.day = day; - tok->u.tsval.hour = hour; - tok->u.tsval.minute = minute; - tok->u.tsval.sec = sec; - tok->u.tsval.usec = usec; - tok->u.tsval.tz = tz; - - // Do some error checks based on type - switch (tok->toktyp) { - case TOK_TIME: - if (!is_valid_time(hour, minute, sec, usec)) { - return SETERROR(sp->ebuf, lineno, "invalid time"); - } - break; - case TOK_DATE: - if (!is_valid_date(year, month, day)) { - return SETERROR(sp->ebuf, lineno, "invalid date"); - } - break; - case TOK_DATETIME: - case TOK_DATETIMETZ: - if (!is_valid_date(year, month, day)) { - return SETERROR(sp->ebuf, lineno, "invalid date"); - } - if (!is_valid_time(hour, minute, sec, usec)) { - return SETERROR(sp->ebuf, lineno, "invalid time"); - } - if (tok->toktyp == TOK_DATETIMETZ && !is_valid_timezone(tz)) { - return SETERROR(sp->ebuf, lineno, "invalid timezone"); - } - break; - default: - assert(0); - return SETERROR(sp->ebuf, lineno, "internal error"); - } - - return 0; -} - -// Given a toml number (int and float) in buffer[]: -// 1. squeeze out '_' -// 2. check for syntax restrictions -static int process_numstr(char *buffer, int base, const char **reason) { - // squeeze out _ - char *q = strchr(buffer, '_'); - if (q) { - for (int i = q - buffer; buffer[i]; i++) { - if (buffer[i] != '_') { - *q++ = buffer[i]; - continue; - } - int left = (i == 0) ? 0 : buffer[i - 1]; - int right = buffer[i + 1]; - if (!isdigit(left) && !(base == 16 && is_hex_char(left))) { - *reason = "underscore only allowed between digits"; - return -1; - } - if (!isdigit(right) && !(base == 16 && is_hex_char(right))) { - *reason = "underscore only allowed between digits"; - return -1; - } - } - *q = 0; - } - - // decimal points must be surrounded by digits. Also, convert to lowercase. - for (int i = 0; buffer[i]; i++) { - if (buffer[i] == '.') { - if (i == 0 || !isdigit(buffer[i - 1]) || !isdigit(buffer[i + 1])) { - *reason = "decimal point must be surrounded by digits"; - return -1; - } - } else if ('A' <= buffer[i] && buffer[i] <= 'Z') { - buffer[i] = tolower(buffer[i]); - } - } - - if (base == 10) { - // check for leading 0: '+01' is an error! - q = buffer; - q += (*q == '+' || *q == '-') ? 1 : 0; - if (q[0] == '0' && isdigit(q[1])) { - *reason = "leading 0 in numbers"; - return -1; - } - } - - return 0; -} - -static int scan_float(scanner_t *sp, token_t *tok) { - char buffer[50]; // need to accomodate "9_007_199_254_740_991.0" - scan_copystr(sp, buffer, sizeof(buffer)); - - int lineno = sp->lineno; - char *p = buffer; - p += (*p == '+' || *p == '-') ? 1 : 0; - if (0 == memcmp(p, "nan", 3) || (0 == memcmp(p, "inf", 3))) { - p += 3; - } else { - p += strspn(p, "_0123456789eE.+-"); - } - int len = p - buffer; - buffer[len] = 0; - - const char *reason; - if (process_numstr(buffer, 10, &reason)) { - return SETERROR(sp->ebuf, lineno, "%s", reason); - } - - errno = 0; - char *q; - double fp64 = strtod(buffer, &q); - if (errno || *q || q == buffer) { - return SETERROR(sp->ebuf, lineno, "error parsing float"); - } - - *tok = mktoken(sp, TOK_FLOAT); - tok->u.fp64 = fp64; - tok->str.len = len; - sp->cur += len; - return 0; -} - -static int scan_number(scanner_t *sp, token_t *tok) { - const char *reason; - char buffer[50]; // need to accomodate "9_007_199_254_740_991.0" - scan_copystr(sp, buffer, sizeof(buffer)); - - char *p = buffer; - int lineno = sp->lineno; - // process %0x, %0o or %0b integers - if (p[0] == '0') { - const char *span = 0; - int base = 0; - switch (p[1]) { - case 'x': - base = 16; - span = "_0123456789abcdefABCDEF"; - break; - case 'o': - base = 8; - span = "_01234567"; - break; - case 'b': - base = 2; - span = "_01"; - break; - } - if (base) { - p += 2; - p += strspn(p, span); - int len = p - buffer; - buffer[len] = 0; - - if (process_numstr(buffer + 2, base, &reason)) { - return SETERROR(sp->ebuf, lineno, "%s", reason); - } - - // use strtoll to obtain the value - *tok = mktoken(sp, TOK_INTEGER); - char *q; - errno = 0; - tok->u.int64 = strtoll(buffer + 2, &q, base); - if (errno || *q || q == buffer + 2) { - return SETERROR(sp->ebuf, lineno, "error parsing integer"); - } - tok->str.len = len; - sp->cur += len; - return 0; - } - } - - // handle inf/nan - if (*p == '+' || *p == '-') { - p++; - } - if (*p == 'i' || *p == 'n') { - return scan_float(sp, tok); - } - - // regular int or float - p = buffer; - p += strspn(p, "0123456789_+-.eE"); - int len = p - buffer; - buffer[len] = 0; - - if (process_numstr(buffer, 10, &reason)) { - return SETERROR(sp->ebuf, lineno, "%s", reason); - } - - *tok = mktoken(sp, TOK_INTEGER); - char *q; - errno = 0; - tok->u.int64 = strtoll(buffer, &q, 10); - if (errno || *q || q == buffer) { - if (*q && strchr(".eE", *q)) { - return scan_float(sp, tok); // try to fit a float - } - return SETERROR(sp->ebuf, lineno, "error parsing integer"); - } - - tok->str.len = len; - sp->cur += len; - return 0; -} - -static int scan_bool(scanner_t *sp, token_t *tok) { - char buffer[10]; - scan_copystr(sp, buffer, sizeof(buffer)); - - int lineno = sp->lineno; - bool val = false; - const char *p = buffer; - if (0 == strncmp(p, "true", 4)) { - val = true; - p += 4; - } else if (0 == strncmp(p, "false", 5)) { - val = false; - p += 5; - } else { - return SETERROR(sp->ebuf, lineno, "invalid boolean value"); - } - if (*p && !strchr("# \r\n\t,}]", *p)) { - return SETERROR(sp->ebuf, lineno, "invalid boolean value"); - } - - int len = p - buffer; - *tok = mktoken(sp, TOK_BOOL); - tok->u.b1 = val; - tok->str.len = len; - sp->cur += len; - return 0; -} - -// Check if the next token may be TIME -static inline bool test_time(const char *p, const char *endp) { - return &p[2] < endp && isdigit(p[0]) && isdigit(p[1]) && p[2] == ':'; -} - -// Check if the next token may be DATE -static inline bool test_date(const char *p, const char *endp) { - return &p[4] < endp && isdigit(p[0]) && isdigit(p[1]) && isdigit(p[2]) && - isdigit(p[3]) && p[4] == '-'; -} - -// Check if the next token may be BOOL -static inline bool test_bool(const char *p, const char *endp) { - return &p[0] < endp && (*p == 't' || *p == 'f'); -} - -// Check if the next token may be NUMBER -static bool test_number(const char *p, const char *endp) { - if (&p[0] < endp && *p && strchr("0123456789+-._", *p)) { - return true; - } - if (&p[2] < endp) { - if (0 == memcmp(p, "nan", 3) || 0 == memcmp(p, "inf", 3)) { - return true; - } - } - return false; -} - -// Scan a literal that is not a string -static int scan_nonstring_literal(scanner_t *sp, token_t *tok) { - int lineno = sp->lineno; - if (test_time(sp->cur, sp->endp)) { - return scan_time(sp, tok); - } - - if (test_date(sp->cur, sp->endp)) { - return scan_timestamp(sp, tok); - } - - if (test_bool(sp->cur, sp->endp)) { - return scan_bool(sp, tok); - } - - if (test_number(sp->cur, sp->endp)) { - return scan_number(sp, tok); - } - return SETERROR(sp->ebuf, lineno, "invalid value"); -} - -// Scan a literal -static int scan_literal(scanner_t *sp, token_t *tok) { - *tok = mktoken(sp, TOK_LIT); - const char *p = sp->cur; - while (p < sp->endp && (isalnum(*p) || *p == '_' || *p == '-')) { - p++; - } - tok->str.len = p - tok->str.ptr; - sp->cur = p; - return 0; -} - -// Save the current state of the scanner -static scanner_state_t scan_mark(scanner_t *sp) { - scanner_state_t mark; - mark.sp = sp; - mark.cur = sp->cur; - mark.lineno = sp->lineno; - return mark; -} - -// Restore the scanner state to a previously saved state -static void scan_restore(scanner_t *sp, scanner_state_t mark) { - assert(mark.sp == sp); - sp->cur = mark.cur; - sp->lineno = mark.lineno; -} - -// Return the next token -static int scan_next(scanner_t *sp, bool keymode, token_t *tok) { - static const toktyp_t map[128] = { - ['\n'] = TOK_ENDL, ['.'] = TOK_DOT, ['='] = TOK_EQUAL, - [','] = TOK_COMMA, ['{'] = TOK_LBRACE, ['}'] = TOK_RBRACE}; -again: - *tok = mktoken(sp, TOK_FIN); - - int ch = S_GET(); - if (ch == TOK_FIN) { - return 0; - } - - tok->str.len = 1; - if (0 <= ch && ch < 128 && map[ch]) { - // map simple char to token type and done - tok->toktyp = map[ch]; - return 0; - } - - // handle char that require logic - switch (ch) { - case ' ': - case '\t': - goto again; // skip whitespace - - case '#': - // comment: skip until newline - while (!S_MATCH('\n')) { - ch = S_GET(); - if (ch == TOK_FIN) - break; - if ((0 <= ch && ch <= 0x8) || (0x0a <= ch && ch <= 0x1f) || - (ch == 0x7f)) { - return SETERROR(sp->ebuf, sp->lineno, "bad control char in comment"); - } - } - goto again; // skip comment - - case '[': - tok->toktyp = TOK_LBRACK; - if (keymode && S_MATCH('[')) { - S_GET(); - tok->toktyp = TOK_LLBRACK; - tok->str.len = 2; - } - break; - - case ']': - tok->toktyp = TOK_RBRACK; - if (keymode && S_MATCH(']')) { - S_GET(); - tok->toktyp = TOK_RRBRACK; - tok->str.len = 2; - } - break; - - case '"': - sp->cur--; - DO(scan_string(sp, tok)); - break; - - case '\'': - sp->cur--; - DO(scan_litstring(sp, tok)); - break; - - default: - sp->cur--; - DO(keymode ? scan_literal(sp, tok) : scan_nonstring_literal(sp, tok)); - break; - } - - return 0; -} - -// Check for stack overflow due to excessive number of brackets or braces -static int check_overflow(scanner_t *sp, token_t *tok) { - switch (tok->toktyp) { - case TOK_LBRACK: - sp->bracket_level++; - if (sp->bracket_level > BRACKET_LEVEL_MAX) { - return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); - } - break; - case TOK_RBRACK: - sp->bracket_level--; - break; - case TOK_LBRACE: - sp->brace_level++; - if (sp->brace_level > BRACE_LEVEL_MAX) { - return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); - } - break; - case TOK_RBRACE: - sp->brace_level--; - break; - default: - break; - } - return 0; -} - -static int scan_key(scanner_t *sp, token_t *tok) { - if (sp->errmsg) { - return -1; - } - if (scan_next(sp, true, tok) || check_overflow(sp, tok)) { - sp->errmsg = sp->ebuf.ptr; - return -1; - } - return 0; -} - -static int scan_value(scanner_t *sp, token_t *tok) { - if (sp->errmsg) { - return -1; - } - if (scan_next(sp, false, tok) || check_overflow(sp, tok)) { - sp->errmsg = sp->ebuf.ptr; - return -1; - } - return 0; -} - -/** - * Convert a char in utf8 into UCS, and store it in *ret. - * Return #bytes consumed or -1 on failure. - */ -static int utf8_to_ucs(const char *orig, int len, uint32_t *ret) { - const unsigned char *buf = (const unsigned char *)orig; - unsigned i = *buf++; - uint32_t v; - - /* 0x00000000 - 0x0000007F: - 0xxxxxxx - */ - if (0 == (i >> 7)) { - if (len < 1) - return -1; - v = i; - return *ret = v, 1; - } - /* 0x00000080 - 0x000007FF: - 110xxxxx 10xxxxxx - */ - if (0x6 == (i >> 5)) { - if (len < 2) - return -1; - v = i & 0x1f; - for (int j = 0; j < 1; j++) { - i = *buf++; - if (0x2 != (i >> 6)) - return -1; - v = (v << 6) | (i & 0x3f); - } - return *ret = v, (const char *)buf - orig; - } - - /* 0x00000800 - 0x0000FFFF: - 1110xxxx 10xxxxxx 10xxxxxx - */ - if (0xE == (i >> 4)) { - if (len < 3) - return -1; - v = i & 0x0F; - for (int j = 0; j < 2; j++) { - i = *buf++; - if (0x2 != (i >> 6)) - return -1; - v = (v << 6) | (i & 0x3f); - } - return *ret = v, (const char *)buf - orig; - } - - /* 0x00010000 - 0x001FFFFF: - 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (0x1E == (i >> 3)) { - if (len < 4) - return -1; - v = i & 0x07; - for (int j = 0; j < 3; j++) { - i = *buf++; - if (0x2 != (i >> 6)) - return -1; - v = (v << 6) | (i & 0x3f); - } - return *ret = v, (const char *)buf - orig; - } - - if (0) { - // NOTE: these code points taking more than 4 bytes are not supported - - /* 0x00200000 - 0x03FFFFFF: - 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (0x3E == (i >> 2)) { - if (len < 5) - return -1; - v = i & 0x03; - for (int j = 0; j < 4; j++) { - i = *buf++; - if (0x2 != (i >> 6)) - return -1; - v = (v << 6) | (i & 0x3f); - } - return *ret = v, (const char *)buf - orig; - } - - /* 0x04000000 - 0x7FFFFFFF: - 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (0x7e == (i >> 1)) { - if (len < 6) - return -1; - v = i & 0x01; - for (int j = 0; j < 5; j++) { - i = *buf++; - if (0x2 != (i >> 6)) - return -1; - v = (v << 6) | (i & 0x3f); - } - return *ret = v, (const char *)buf - orig; - } - } - - return -1; -} - -/** - * Convert a UCS char to utf8 code, and return it in buf. - * Return #bytes used in buf to encode the char, or - * -1 on error. - */ -static int ucs_to_utf8(uint32_t code, char buf[4]) { - /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 - */ - /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well - * as 0xfffe and 0xffff (UCS noncharacters) should not appear in - * conforming UTF-8 streams. - */ - /* - * https://github.com/toml-lang/toml-test/issues/165 - * [0xd800, 0xdfff] and [0xfffe, 0xffff] are implicitly allowed by TOML, so - * we disable the check. - */ - if (0) { - if (0xd800 <= code && code <= 0xdfff) - return -1; - if (0xfffe <= code && code <= 0xffff) - return -1; - } - - /* 0x00000000 - 0x0000007F: - 0xxxxxxx - */ - if (code <= 0x7F) { - buf[0] = (unsigned char)code; - return 1; - } - - /* 0x00000080 - 0x000007FF: - 110xxxxx 10xxxxxx - */ - if (code <= 0x000007FF) { - buf[0] = (unsigned char)(0xc0 | (code >> 6)); - buf[1] = (unsigned char)(0x80 | (code & 0x3f)); - return 2; - } - - /* 0x00000800 - 0x0000FFFF: - 1110xxxx 10xxxxxx 10xxxxxx - */ - if (code <= 0x0000FFFF) { - buf[0] = (unsigned char)(0xe0 | (code >> 12)); - buf[1] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); - buf[2] = (unsigned char)(0x80 | (code & 0x3f)); - return 3; - } - - /* 0x00010000 - 0x001FFFFF: - 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (code <= 0x001FFFFF) { - buf[0] = (unsigned char)(0xf0 | (code >> 18)); - buf[1] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); - buf[2] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); - buf[3] = (unsigned char)(0x80 | (code & 0x3f)); - return 4; - } - -#ifdef UNDEF - if (0) { - // NOTE: these code points taking more than 4 bytes are not supported - /* 0x00200000 - 0x03FFFFFF: - 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (code <= 0x03FFFFFF) { - buf[0] = (unsigned char)(0xf8 | (code >> 24)); - buf[1] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); - buf[2] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); - buf[3] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); - buf[4] = (unsigned char)(0x80 | (code & 0x3f)); - return 5; - } - - /* 0x04000000 - 0x7FFFFFFF: - 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx - */ - if (code <= 0x7FFFFFFF) { - buf[0] = (unsigned char)(0xfc | (code >> 30)); - buf[1] = (unsigned char)(0x80 | ((code >> 24) & 0x3f)); - buf[2] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); - buf[3] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); - buf[4] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); - buf[5] = (unsigned char)(0x80 | (code & 0x3f)); - return 6; - } - } -#endif - - return -1; -} diff --git a/include/vendor/tomlc17.h b/include/vendor/tomlc17.h deleted file mode 100644 index d5334ee..0000000 --- a/include/vendor/tomlc17.h +++ /dev/null @@ -1,190 +0,0 @@ -/* Copyright (c) 2024-2026, CK Tan. - * https://github.com/cktan/tomlc17/blob/main/LICENSE - */ -#ifndef TOMLC17_H -#define TOMLC17_H - -/* - * USAGE: - * - * 1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex() - * 2. Check result.ok - * 3. Use toml_get() or toml_seek() to query and traverse the - * result.toptab - * 4. Call toml_free() to release resources. - * - */ - -#include -#include -#include - -#ifdef __cplusplus -#define TOML_EXTERN extern "C" -#else -#define TOML_EXTERN extern -#endif - -enum toml_type_t { - TOML_UNKNOWN = 0, - TOML_STRING, - TOML_INT64, - TOML_FP64, - TOML_BOOLEAN, - TOML_DATE, - TOML_TIME, - TOML_DATETIME, - TOML_DATETIMETZ, - TOML_ARRAY, - TOML_TABLE, -}; -typedef enum toml_type_t toml_type_t; - -/* This is a Node in a Tree that represents a toml document rooted - * at toml_result_t::toptab. - */ -typedef struct toml_datum_t toml_datum_t; -struct toml_datum_t { - toml_type_t type; - uint32_t flag; // internal - union { - const char *s; // same as str.ptr; use if there are no NUL in string. - struct { - const char *ptr; // NUL terminated string - int len; // length excluding the terminating NUL. - } str; - int64_t int64; // integer - double fp64; // float - bool boolean; - struct { // date, time - int16_t year, month, day; - int16_t hour, minute, second; - int32_t usec; - int16_t tz; // in minutes - } ts; - struct { // array - int32_t size; // count elem - toml_datum_t *elem; // elem[] - } arr; - struct { // table - int32_t size; // count key - const char **key; // key[] - int *len; // len[] - toml_datum_t *value; // value[] - } tab; - } u; -}; - -/* Result returned by toml_parse() */ -typedef struct toml_result_t toml_result_t; -struct toml_result_t { - bool ok; // success flag - toml_datum_t toptab; // valid if ok - char errmsg[200]; // valid if not ok - void *__internal; // do not use -}; - -/** - * Parse a toml document. Returns a toml_result which must be freed - * using toml_free() eventually. - * - * IMPORTANT: src[] must be a NUL terminated string! The len parameter - * does not include the NUL terminator. - */ -TOML_EXTERN toml_result_t toml_parse(const char *src, int len); - -/** - * Parse a toml file. Returns a toml_result which must be freed - * using toml_free() eventually. - * - * IMPORTANT: you are still responsible to fclose(fp). - */ -TOML_EXTERN toml_result_t toml_parse_file(FILE *fp); - -/** - * Parse a toml file. Returns a toml_result which must be freed - * using toml_free() eventually. - */ -TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname); - -/** - * Release the result. - */ -TOML_EXTERN void toml_free(toml_result_t result); - -/** - * Find a key in a toml_table. Return the value of the key if found, - * or a TOML_UNKNOWN otherwise. - */ -TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key); - -/** - * Locate a value starting from a toml_table. Return the value of the key if - * found, or a TOML_UNKNOWN otherwise. - * - * Note: the multipart-key is separated by DOT, and must not have any escape - * chars. The maximum length of the multipart_key must not exceed 127 bytes. - */ -TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table, - const char *multipart_key); - -/** - * OBSOLETE: use toml_get() instead. - * Find a key in a toml_table. Return the value of the key if found, - * or a TOML_UNKNOWN otherwise. ( - */ -static inline toml_datum_t toml_table_find(toml_datum_t table, - const char *key) { - return toml_get(table, key); -} - -/** - * Override values in r1 using r2. Return a new result. All results - * (i.e., r1, r2 and the returned result) must be freed using toml_free() - * after use. - * - * LOGIC: - * ret = copy of r1 - * for each item x in r2: - * if x is not in ret: - * override - * elif x in ret is NOT of the same type: - * override - * elif x is an array of tables: - * append r2.x to ret.x - * elif x is a table: - * merge r2.x to ret.x - * else: - * override - */ -TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1, - const toml_result_t *r2); - -/** - * Check if two results are the same. Dictionary and array orders are - * sensitive. - */ -TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2); - -/* Options that override tomlc17 defaults globally */ -typedef struct toml_option_t toml_option_t; -struct toml_option_t { - bool check_utf8; // Check all chars are valid utf8; default: false. - void *(*mem_realloc)(void *ptr, size_t size); // default: realloc() - void (*mem_free)(void *ptr); // default: free() -}; - -/** - * Get the default options. IF NECESSARY, use this to initialize - * toml_option_t and override values before calling - * toml_set_option(). - */ -TOML_EXTERN toml_option_t toml_default_option(void); - -/** - * Set toml options globally. Do this ONLY IF you are not satisfied with the - * defaults. - */ -TOML_EXTERN void toml_set_option(toml_option_t opt); - -#endif // TOMLC17_H diff --git a/nix/package.nix b/nix/package.nix index 9ba6c0d..6de98fc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -15,7 +15,7 @@ in stdenv.mkDerivation { pname = "chroma"; - version = "2.0.0"; + version = "1.1.0"; src = fs.toSource { root = s; @@ -64,7 +64,7 @@ in ''; postInstall = '' - install -Dm755 ${../chroma.toml.sample} $out/share/chroma.toml.sample + install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample ''; meta = { diff --git a/src/config.c b/src/config.c index e9c615e..0c28132 100644 --- a/src/config.c +++ b/src/config.c @@ -5,9 +5,29 @@ #include #include -#include "chroma.h" -#include "vendor/tomlc17.h" +#include "../include/chroma.h" +static char *trim_whitespace(char *str) { + char *end; + + // Trim leading whitespace + while (isspace((unsigned char)*str)) + str++; + + // All spaces? + if (*str == 0) + return str; + + // Trim trailing whitespace + end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) + end--; + + *(end + 1) = '\0'; + return str; +} + +// Remove quotes from a string // Match output name/description against a config pattern // Supports: // - Exact name match: "DP-1" matches wl_output.name == "DP-1" @@ -40,8 +60,31 @@ static bool match_output(const char *pattern, const char *output_name, return false; } +static char *remove_quotes(char *str) { + size_t len = strlen(str); + if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') || + (str[0] == '\'' && str[len - 1] == '\''))) { + str[len - 1] = '\0'; + return str + 1; + } + return str; +} + +// Parse boolean value from string +static bool parse_bool(const char *value) { + if (!value) + return false; + + if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 || + strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) { + return true; + } + + return false; +} + // Parse scaling mode from string -chroma_scale_mode_t parse_scale_mode(const char *value) { +static chroma_scale_mode_t parse_scale_mode(const char *value) { if (!value) return CHROMA_SCALE_FILL; // default @@ -60,7 +103,7 @@ chroma_scale_mode_t parse_scale_mode(const char *value) { } // Parse filter quality from string -chroma_filter_quality_t parse_filter_quality(const char *value) { +static chroma_filter_quality_t parse_filter_quality(const char *value) { if (!value) return CHROMA_FILTER_LINEAR; // default @@ -111,7 +154,7 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) { } // Parse anchor position from string -chroma_anchor_t parse_anchor(const char *value) { +static chroma_anchor_t parse_anchor(const char *value) { if (!value) return CHROMA_ANCHOR_CENTER; @@ -170,10 +213,12 @@ static const char *anchor_to_string(chroma_anchor_t anchor) { } // Output-to-image mapping -int add_output_mapping(chroma_config_t *config, const char *output_name, - const char *image_path, chroma_scale_mode_t scale_mode, - chroma_filter_quality_t filter_quality, - chroma_anchor_t anchor, float anchor_x, float anchor_y) { +static int add_output_mapping(chroma_config_t *config, const char *output_name, + const char *image_path, + chroma_scale_mode_t scale_mode, + chroma_filter_quality_t filter_quality, + chroma_anchor_t anchor, float anchor_x, + float anchor_y) { if (!config || !output_name || !image_path) { return CHROMA_ERROR_INIT; } @@ -223,194 +268,6 @@ int add_output_mapping(chroma_config_t *config, const char *output_name, return CHROMA_OK; } -// Parse TOML configuration file -int chroma_config_load_toml(chroma_config_t *config, const char *config_file) { - if (!config || !config_file) { - return CHROMA_ERROR_INIT; - } - - toml_result_t result = toml_parse_file_ex(config_file); - if (!result.ok) { - chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg); - toml_free(result); - return CHROMA_ERROR_CONFIG; - } - - chroma_log("INFO", "Loading TOML configuration from: %s", config_file); - - // Parse default_image - toml_datum_t default_image = toml_seek(result.toptab, "default_image"); - if (default_image.type == TOML_STRING) { - char *expanded_path = chroma_expand_path(default_image.u.s); - const char *path_to_use = expanded_path ? expanded_path : default_image.u.s; - if (strlen(path_to_use) < sizeof(config->default_image)) { - strcpy(config->default_image, path_to_use); - } else { - chroma_log("WARN", - "default_image path too long (%zu >= %zu), truncating: %s", - strlen(path_to_use), sizeof(config->default_image), path_to_use); - snprintf(config->default_image, sizeof(config->default_image), "%s", - path_to_use); - } - if (expanded_path) { - free(expanded_path); - } - } - - // Parse daemon_mode - toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode"); - if (daemon_mode.type == TOML_BOOLEAN) { - config->daemon_mode = daemon_mode.u.boolean; - } - - // Parse scale_mode - toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode"); - if (scale_mode.type == TOML_STRING) { - config->default_scale_mode = parse_scale_mode(scale_mode.u.s); - } - - // Parse filter_quality - toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality"); - if (filter_quality.type == TOML_STRING) { - config->default_filter_quality = parse_filter_quality(filter_quality.u.s); - } - - // Parse anchor - toml_datum_t anchor = toml_seek(result.toptab, "anchor"); - if (anchor.type == TOML_STRING) { - config->default_anchor = parse_anchor(anchor.u.s); - } - - // Parse anchor_x - toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x"); - if (anchor_x.type == TOML_INT64) { - config->default_anchor_x = (float)anchor_x.u.int64; - } else if (anchor_x.type == TOML_FP64) { - config->default_anchor_x = (float)anchor_x.u.fp64; - } - - // Parse anchor_y - toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y"); - if (anchor_y.type == TOML_INT64) { - config->default_anchor_y = (float)anchor_y.u.int64; - } else if (anchor_y.type == TOML_FP64) { - config->default_anchor_y = (float)anchor_y.u.fp64; - } - - // Parse downsampling section - toml_datum_t downsampling = toml_seek(result.toptab, "downsampling"); - if (downsampling.type == TOML_TABLE) { - toml_datum_t ds_tab = downsampling; - - toml_datum_t enable = toml_seek(ds_tab, "enable"); - if (enable.type == TOML_BOOLEAN) { - config->enable_downsampling = enable.u.boolean; - } - - toml_datum_t max_width = toml_seek(ds_tab, "max_output_width"); - if (max_width.type == TOML_INT64) { - config->max_output_width = (int)max_width.u.int64; - } - - toml_datum_t max_height = toml_seek(ds_tab, "max_output_height"); - if (max_height.type == TOML_INT64) { - config->max_output_height = (int)max_height.u.int64; - } - - toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor"); - if (min_scale.type == TOML_FP64) { - config->min_scale_factor = (float)min_scale.u.fp64; - } else if (min_scale.type == TOML_INT64) { - config->min_scale_factor = (float)min_scale.u.int64; - } - } - - // Parse output mappings array - toml_datum_t outputs = toml_seek(result.toptab, "output"); - if (outputs.type == TOML_ARRAY) { - for (int i = 0; i < outputs.u.arr.size; i++) { - toml_datum_t output = outputs.u.arr.elem[i]; - if (output.type != TOML_TABLE) { - continue; - } - - toml_datum_t output_tab = output; - - // Get output name - toml_datum_t name = toml_seek(output_tab, "name"); - if (name.type != TOML_STRING) { - chroma_log("WARN", "Output mapping %d missing name", i); - continue; - } - - // Get image path - toml_datum_t image = toml_seek(output_tab, "image"); - if (image.type != TOML_STRING) { - chroma_log("WARN", "Output mapping %d missing image", i); - continue; - } - - char *expanded_path = chroma_expand_path(image.u.s); - const char *path_to_use = expanded_path ? expanded_path : image.u.s; - - // Get optional settings with defaults from global config - chroma_scale_mode_t out_scale = config->default_scale_mode; - chroma_filter_quality_t out_filter = config->default_filter_quality; - chroma_anchor_t out_anchor = config->default_anchor; - float out_anchor_x = config->default_anchor_x; - float out_anchor_y = config->default_anchor_y; - - toml_datum_t out_scale_val = toml_seek(output_tab, "scale"); - if (out_scale_val.type == TOML_STRING) { - out_scale = parse_scale_mode(out_scale_val.u.s); - } - - toml_datum_t out_filter_val = toml_seek(output_tab, "filter"); - if (out_filter_val.type == TOML_STRING) { - out_filter = parse_filter_quality(out_filter_val.u.s); - } - - toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor"); - if (out_anchor_val.type == TOML_STRING) { - out_anchor = parse_anchor(out_anchor_val.u.s); - } - - toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x"); - if (out_anchor_x_val.type == TOML_INT64) { - out_anchor_x = (float)out_anchor_x_val.u.int64; - } else if (out_anchor_x_val.type == TOML_FP64) { - out_anchor_x = (float)out_anchor_x_val.u.fp64; - } - - toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y"); - if (out_anchor_y_val.type == TOML_INT64) { - out_anchor_y = (float)out_anchor_y_val.u.int64; - } else if (out_anchor_y_val.type == TOML_FP64) { - out_anchor_y = (float)out_anchor_y_val.u.fp64; - } - - // Add the mapping - if (add_output_mapping(config, name.u.s, path_to_use, out_scale, - out_filter, out_anchor, out_anchor_x, - out_anchor_y) != CHROMA_OK) { - chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s); - } - - if (expanded_path) { - free(expanded_path); - } - } - } - - toml_free(result); - - chroma_log("INFO", - "Loaded TOML configuration: %d output mappings, default image: %s", - config->mapping_count, config->default_image); - - return CHROMA_OK; -} - // Initialize configuration with defaults static void init_default_config(chroma_config_t *config) { if (!config) @@ -434,12 +291,313 @@ static void init_default_config(chroma_config_t *config) { config->max_output_height = 2160; // 4K height config->min_scale_factor = 0.25f; // don't scale below 25% - // Leave default_image empty - user must configure it explicitly - // This avoids errors when the hardcoded path doesn't exist - config->default_image[0] = '\0'; + // Set default image path (can be overridden) + const char *home = getenv("HOME"); + if (home) { + snprintf(config->default_image, sizeof(config->default_image), + "%s/.config/chroma/default.jpg", home); + } else { + strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg"); + } } -// Load configuration from file (TOML format only) +// Parse a single configuration line +static int parse_config_line(chroma_config_t *config, char *line, + int line_number) { + if (!config || !line) { + return CHROMA_ERROR_INIT; + } + + // Skip empty lines and comments + char *trimmed = trim_whitespace(line); + if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') { + return CHROMA_OK; + } + + // Find the equals sign + char *equals = strchr(trimmed, '='); + if (!equals) { + chroma_log("WARN", "Invalid config line %d: no '=' found", line_number); + return CHROMA_OK; // continue parsing + } + + *equals = '\0'; + char *key = trim_whitespace(trimmed); + char *value = trim_whitespace(equals + 1); + + value = remove_quotes(value); + + if (*key == '\0' || *value == '\0') { + chroma_log("WARN", "Invalid config line %d: empty key or value", + line_number); + return CHROMA_OK; + } + + // Parse configuration options + if (strcasecmp(key, "default_image") == 0) { + char *expanded_path = chroma_expand_path(value); + const char *path_to_use = expanded_path ? expanded_path : value; + size_t path_len = strlen(path_to_use); + + if (path_len >= sizeof(config->default_image)) { + chroma_log("ERROR", "Default image path too long: %s (max %zu)", + path_to_use, sizeof(config->default_image) - 1); + if (expanded_path) { + free(expanded_path); + } + return CHROMA_ERROR_CONFIG; + } + + strcpy(config->default_image, path_to_use); + + if (expanded_path) { + chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path); + chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'", + path_len, expanded_path); + free(expanded_path); + } else { + chroma_log("WARN", "Failed to expand path, using original: %s", value); + } + } else if (strcasecmp(key, "daemon") == 0 || + strcasecmp(key, "daemon_mode") == 0) { + config->daemon_mode = parse_bool(value); + chroma_log("DEBUG", "Set daemon mode: %s", + config->daemon_mode ? "true" : "false"); + chroma_log("TRACE", + "Daemon mode configuration: key='%s', value='%s', parsed=%s", + key, value, config->daemon_mode ? "true" : "false"); + } else if (strcasecmp(key, "scale_mode") == 0 || + strcasecmp(key, "default_scale_mode") == 0) { + config->default_scale_mode = parse_scale_mode(value); + chroma_log("DEBUG", "Set default scale mode: %s", + scale_mode_to_string(config->default_scale_mode)); + chroma_log("TRACE", + "Scale mode configuration: key='%s', value='%s', parsed=%s", key, + value, scale_mode_to_string(config->default_scale_mode)); + } else if (strcasecmp(key, "filter_quality") == 0 || + strcasecmp(key, "default_filter_quality") == 0) { + config->default_filter_quality = parse_filter_quality(value); + chroma_log("DEBUG", "Set default filter quality: %s", + filter_quality_to_string(config->default_filter_quality)); + chroma_log("TRACE", + "Filter quality configuration: key='%s', value='%s', parsed=%s", + key, value, + filter_quality_to_string(config->default_filter_quality)); + } else if (strcasecmp(key, "enable_downsampling") == 0) { + config->enable_downsampling = parse_bool(value); + chroma_log("DEBUG", "Set downsampling: %s", + config->enable_downsampling ? "enabled" : "disabled"); + } else if (strcasecmp(key, "anchor") == 0) { + config->default_anchor = parse_anchor(value); + chroma_log("DEBUG", "Set default anchor: %s", + anchor_to_string(config->default_anchor)); + } else if (strcasecmp(key, "anchor_x") == 0) { + char *endptr = NULL; + float ax = strtof(value, &endptr); + if (endptr == value || *endptr != '\0') { + chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)", + value); + config->default_anchor_x = 50.0f; + } else if (ax < 0.0f || ax > 100.0f) { + chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value); + config->default_anchor_x = 50.0f; + } else { + config->default_anchor_x = ax; + chroma_log("DEBUG", "Set default anchor_x: %.1f", (double)ax); + } + } else if (strcasecmp(key, "anchor_y") == 0) { + char *endptr = NULL; + float ay = strtof(value, &endptr); + if (endptr == value || *endptr != '\0') { + chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)", + value); + config->default_anchor_y = 50.0f; + } else if (ay < 0.0f || ay > 100.0f) { + chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value); + config->default_anchor_y = 50.0f; + } else { + config->default_anchor_y = ay; + chroma_log("DEBUG", "Set default anchor_y: %.1f", (double)ay); + } + } else if (strcasecmp(key, "max_output_width") == 0) { + int width = atoi(value); + if (width > 0 && width <= 16384) { // Reasonable limits + config->max_output_width = width; + chroma_log("DEBUG", "Set max output width: %d", width); + } else { + chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value); + } + } else if (strcasecmp(key, "max_output_height") == 0) { + int height = atoi(value); + if (height > 0 && height <= 16384) { // Reasonable limits + config->max_output_height = height; + chroma_log("DEBUG", "Set max output height: %d", height); + } else { + chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value); + } + } else if (strcasecmp(key, "min_scale_factor") == 0) { + float factor = (float)atof(value); + if (factor > 0.0f && factor <= 1.0f) { + config->min_scale_factor = factor; + chroma_log("DEBUG", "Set minimum scale factor: %.2f", (double)factor); + } else { + chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value); + } + } else if (strncasecmp(key, "output.", 7) == 0) { + // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg + const char *output_name = key + 7; + if (*output_name == '\0') { + chroma_log("WARN", "Invalid output mapping line %d: no output name", + line_number); + return CHROMA_OK; + } + + // Check for extended output configuration with properties. Format: + // output.DP-1.scale = fill + // output.DP-1.filter = linear + char *dot = strchr(output_name, '.'); + if (dot) { + // This is an output property (scale or filter) + *dot = '\0'; + const char *property = dot + 1; + + // Find existing mapping for this output + chroma_config_mapping_t *mapping = NULL; + for (int i = 0; i < config->mapping_count; i++) { + if (strcmp(config->mappings[i].output_name, output_name) == 0) { + mapping = &config->mappings[i]; + break; + } + } + + if (!mapping) { + chroma_log("WARN", "Output %s not found for property %s (line %d)", + output_name, property, line_number); + return CHROMA_OK; + } + + if (strcasecmp(property, "scale") == 0) { + mapping->scale_mode = parse_scale_mode(value); + chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name, + scale_mode_to_string(mapping->scale_mode)); + } else if (strcasecmp(property, "filter") == 0) { + mapping->filter_quality = parse_filter_quality(value); + chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name, + filter_quality_to_string(mapping->filter_quality)); + } else if (strcasecmp(property, "anchor") == 0) { + mapping->anchor = parse_anchor(value); + // Set anchor_x/anchor_y based on named anchor + switch (mapping->anchor) { + case CHROMA_ANCHOR_TOP: + mapping->anchor_x = 50.0f; + mapping->anchor_y = 0.0f; + break; + case CHROMA_ANCHOR_BOTTOM: + mapping->anchor_x = 50.0f; + mapping->anchor_y = 100.0f; + break; + case CHROMA_ANCHOR_LEFT: + mapping->anchor_x = 0.0f; + mapping->anchor_y = 50.0f; + break; + case CHROMA_ANCHOR_RIGHT: + mapping->anchor_x = 100.0f; + mapping->anchor_y = 50.0f; + break; + case CHROMA_ANCHOR_TOP_LEFT: + mapping->anchor_x = 0.0f; + mapping->anchor_y = 0.0f; + break; + case CHROMA_ANCHOR_TOP_RIGHT: + mapping->anchor_x = 100.0f; + mapping->anchor_y = 0.0f; + break; + case CHROMA_ANCHOR_BOTTOM_LEFT: + mapping->anchor_x = 0.0f; + mapping->anchor_y = 100.0f; + break; + case CHROMA_ANCHOR_BOTTOM_RIGHT: + mapping->anchor_x = 100.0f; + mapping->anchor_y = 100.0f; + break; + default: + mapping->anchor_x = 50.0f; + mapping->anchor_y = 50.0f; + break; + } + chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)", + output_name, anchor_to_string(mapping->anchor), + (double)mapping->anchor_x, (double)mapping->anchor_y); + } else if (strcasecmp(property, "anchor_x") == 0) { + float ax = (float)atof(value); + if (ax >= 0.0f && ax <= 100.0f) { + mapping->anchor_x = ax; + chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name, + (double)ax); + } else { + mapping->anchor_x = 50.0f; + chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", + value); + } + } else if (strcasecmp(property, "anchor_y") == 0) { + float ay = (float)atof(value); + if (ay >= 0.0f && ay <= 100.0f) { + mapping->anchor_y = ay; + chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name, + (double)ay); + } else { + mapping->anchor_y = 50.0f; + chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", + value); + } + } else { + chroma_log("WARN", "Unknown output property: %s (line %d)", property, + line_number); + } + + return CHROMA_OK; + } + + // Expand path before validation and storage + char *expanded_path = chroma_expand_path(value); + const char *path_to_validate = expanded_path ? expanded_path : value; + + // Validate image path + if (chroma_image_validate(path_to_validate) != CHROMA_OK) { + chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)", + output_name, value, path_to_validate); + if (expanded_path) { + free(expanded_path); + } + return CHROMA_OK; + } + + if (add_output_mapping( + config, output_name, path_to_validate, config->default_scale_mode, + config->default_filter_quality, config->default_anchor, + config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) { + chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name, + path_to_validate); + if (expanded_path) { + free(expanded_path); + } + return CHROMA_ERROR_CONFIG; + } + + if (expanded_path) { + free(expanded_path); + } + } else { + chroma_log("WARN", "Unknown configuration key line %d: %s", line_number, + key); + chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'", + line_number, key, value); + } + + return CHROMA_OK; +} + +// Load configuration from file int chroma_config_load(chroma_config_t *config, const char *config_file) { if (!config) { return CHROMA_ERROR_INIT; @@ -453,20 +611,52 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) { return CHROMA_OK; } - // Check if file exists - if (!chroma_path_exists(config_file)) { - chroma_log("INFO", "Config file not found: %s (using defaults)", - config_file); - return CHROMA_OK; + FILE *file = fopen(config_file, "r"); + if (!file) { + if (errno == ENOENT) { + chroma_log("INFO", "Config file not found: %s (using defaults)", + config_file); + return CHROMA_OK; + } else { + chroma_log("ERROR", "Failed to open config file %s: %s", config_file, + strerror(errno)); + return CHROMA_ERROR_CONFIG; + } } - // Load TOML configuration - int result = chroma_config_load_toml(config, config_file); - if (result != CHROMA_OK) { - chroma_log("ERROR", "Failed to load TOML config from %s", config_file); - return result; + chroma_log("INFO", "Loading configuration from: %s", config_file); + chroma_log("TRACE", + "Starting configuration parsing, estimated config size: %ld bytes", + chroma_get_file_size(config_file)); + + char line[1024]; + int line_number = 0; + int parse_errors = 0; + + while (fgets(line, sizeof(line), file)) { + line_number++; + + char *newline = strchr(line, '\n'); + if (newline) { + *newline = '\0'; + } + + if (parse_config_line(config, line, line_number) != CHROMA_OK) { + parse_errors++; + } } + fclose(file); + + if (parse_errors > 0) { + chroma_log("WARN", "Config file contained %d errors", parse_errors); + // Continue anyway with partial configuration + } + + chroma_log("INFO", + "Loaded configuration: %d output mappings, default image: %s", + config->mapping_count, config->default_image); + // Log configuration memory usage size_t config_size = sizeof(chroma_config_t) + diff --git a/src/core.c b/src/core.c index 9d499ba..6751853 100644 --- a/src/core.c +++ b/src/core.c @@ -85,19 +85,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state, return CHROMA_ERROR_CONFIG; } - // Check if image path is empty (no default configured) - if (strlen(image_path) == 0) { - chroma_log("WARN", - "No wallpaper image configured for output %u (%s). " - "Set default_image in config or provide -c config.toml", - output->id, output->name ? output->name : "unknown"); - return CHROMA_ERROR_CONFIG; - } - - // Load or get cached image with output dimensions for intelligent - // downsampling - chroma_image_t *image = chroma_image_get_or_load( - state, image_path, output->width, output->height); + // Load or get cached image + chroma_image_t *image = chroma_image_get_or_load(state, image_path); if (!image) { chroma_log("ERROR", "Failed to load image for output %u: %s", output->id, image_path); @@ -107,7 +96,6 @@ static int assign_wallpaper_to_output(chroma_state_t *state, // Check if image changed and invalidate texture cache if neceessary bool image_changed = (output->image != image); if (image_changed && output->image) { - chroma_image_release(output->image); chroma_output_invalidate_texture(output); output->vbo_dirty = true; // VBO needs update for new image chroma_log("DEBUG", "Image changed for output %u, invalidated texture", @@ -173,8 +161,7 @@ static int assign_wallpaper_to_output(chroma_state_t *state, // Render wallpaper int ret = chroma_render_wallpaper(state, output); if (ret != CHROMA_OK) { - chroma_log("ERROR", "Failed to render wallpaper for output %u: %s", output->id, - chroma_error_string(ret)); + chroma_log("ERROR", "Failed to render wallpaper for output %u", output->id); return ret; } diff --git a/src/image.c b/src/image.c index ef63c4c..96482c1 100644 --- a/src/image.c +++ b/src/image.c @@ -58,9 +58,8 @@ static void calculate_optimal_size(int original_width, int original_height, // being*. Must be revisited in the future to see how it stands as the program // evolves. static int downsample_image(unsigned char *src_data, int src_width, - int src_height, int src_channels, - unsigned char *dst_data, int dst_width, - int dst_height, int dst_channels) { + int src_height, unsigned char *dst_data, + int dst_width, int dst_height) { if (!src_data || !dst_data) { return -1; } @@ -78,18 +77,14 @@ static int downsample_image(unsigned char *src_data, int src_width, src_x = (src_x >= src_width) ? src_width - 1 : src_x; src_y = (src_y >= src_height) ? src_height - 1 : src_y; - // Copy pixel data - int src_idx = (src_y * src_width + src_x) * src_channels; - int dst_idx = (y * dst_width + x) * dst_channels; + // Copy pixel (RGBA) + int src_idx = (src_y * src_width + src_x) * 4; + int dst_idx = (y * dst_width + x) * 4; dst_data[dst_idx + 0] = src_data[src_idx + 0]; // R dst_data[dst_idx + 1] = src_data[src_idx + 1]; // G dst_data[dst_idx + 2] = src_data[src_idx + 2]; // B - if (dst_channels == 4 && src_channels == 4) { - dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A - } else if (dst_channels == 4) { - dst_data[dst_idx + 3] = 255; // Full alpha for RGB source - } + dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A } } @@ -97,11 +92,8 @@ static int downsample_image(unsigned char *src_data, int src_width, } // Load image from file with configurable downsampling -// output_width/output_height: actual output dimensions for intelligent -// downsampling int chroma_image_load(chroma_image_t *image, const char *path, - const chroma_config_t *config, int output_width, - int output_height) { + const chroma_config_t *config) { if (!image || !path) { chroma_log("ERROR", "Invalid parameters for image loading"); return CHROMA_ERROR_INIT; @@ -125,24 +117,12 @@ int chroma_image_load(chroma_image_t *image, const char *path, (double)file_size / (1024.0 * 1024.0)); } - // Load image data using stb_image - // First, check actual channels to decide if we need alpha + // Load image data using stb_image, force RGBA format to avoid conversion stbi_set_flip_vertically_on_load(0); // keep images right-side up - int actual_channels = 0; - if (!stbi_info(path, &image->width, &image->height, &actual_channels)) { - chroma_log("ERROR", "Failed to get image info for %s: %s", path, - stbi_failure_reason()); - return CHROMA_ERROR_IMAGE; - } - - // Load with actual channels or force RGBA if image has alpha - // For wallpapers, we typically don't need alpha unless the image has it - int desired_channels = (actual_channels == 4 || actual_channels == 2) ? 4 : 3; - - image->data = stbi_load(path, &image->width, &image->height, &image->channels, - desired_channels); - image->channels = desired_channels; + image->data = + stbi_load(path, &image->width, &image->height, &image->channels, 4); + image->channels = 4; // always RGBA after forced conversion if (!image->data) { chroma_log("ERROR", "Failed to load image %s: %s", path, stbi_failure_reason()); @@ -157,8 +137,13 @@ int chroma_image_load(chroma_image_t *image, const char *path, return CHROMA_ERROR_IMAGE; } - chroma_log("DEBUG", "Loaded image %s with %d channels (original had %d)", - path, image->channels, actual_channels); + // Validate we have RGBA data (should always be true with forced conversion) + if (image->channels != 4) { + chroma_log("ERROR", "Failed to load image as RGBA: got %d channels", + image->channels); + chroma_image_free(image); + return CHROMA_ERROR_IMAGE; + } // Store original dimensions before potential downsampling int original_width = image->width; @@ -170,14 +155,9 @@ int chroma_image_load(chroma_image_t *image, const char *path, int optimal_height = original_height; if (config && config->enable_downsampling) { - // Use output dimensions if provided, otherwise fall back to config defaults - int max_width = - (output_width > 0) ? output_width : config->max_output_width; - int max_height = - (output_height > 0) ? output_height : config->max_output_height; - - calculate_optimal_size(original_width, original_height, max_width, - max_height, &optimal_width, &optimal_height); + calculate_optimal_size(original_width, original_height, + config->max_output_width, config->max_output_height, + &optimal_width, &optimal_height); // Apply minimum scale factor constraint float scale_x = (float)optimal_width / (float)original_width; @@ -198,7 +178,7 @@ int chroma_image_load(chroma_image_t *image, const char *path, (optimal_width < original_width || optimal_height < original_height); } - // Downsample if needed and enabled + // Downsamp if needed and enabled if (should_downsample) { double reduction_ratio = (double)(optimal_width * optimal_height) / (double)(original_width * original_height) * 100.0; @@ -207,8 +187,7 @@ int chroma_image_load(chroma_image_t *image, const char *path, original_width, original_height, optimal_width, optimal_height, reduction_ratio); - size_t optimal_size = - (size_t)optimal_width * (size_t)optimal_height * image->channels; + size_t optimal_size = (size_t)optimal_width * (size_t)optimal_height * 4; unsigned char *downsampled_data = malloc(optimal_size); if (!downsampled_data) { chroma_log("ERROR", "Failed to allocate memory for downsampled image"); @@ -217,8 +196,8 @@ int chroma_image_load(chroma_image_t *image, const char *path, } if (downsample_image(image->data, original_width, original_height, - image->channels, downsampled_data, optimal_width, - optimal_height, image->channels) != 0) { + downsampled_data, optimal_width, + optimal_height) != 0) { chroma_log("ERROR", "Failed to downsample image"); free(downsampled_data); chroma_image_free(image); @@ -239,7 +218,6 @@ int chroma_image_load(chroma_image_t *image, const char *path, } image->loaded = true; - image->ref_count = 1; // Initial reference from the first output // Calculate and log memory allocation size_t image_size = @@ -280,7 +258,6 @@ void chroma_image_free(chroma_image_t *image) { image->height = 0; image->channels = 0; image->loaded = false; - image->ref_count = 0; if (strlen(image->path) > 0) { chroma_log("DEBUG", "Freed image: %s", image->path); @@ -289,18 +266,6 @@ void chroma_image_free(chroma_image_t *image) { memset(image->path, 0, sizeof(image->path)); } -// Release a reference to an image; free when ref_count reaches zero -void chroma_image_release(chroma_image_t *image) { - if (!image) { - return; - } - - image->ref_count--; - if (image->ref_count <= 0) { - chroma_image_free(image); - } -} - // Find image by path in state chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, const char *path) { @@ -318,11 +283,8 @@ chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, } // Load image if not already loaded -// output_width/output_height: actual output dimensions for intelligent -// downsampling chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, - const char *path, int output_width, - int output_height) { + const char *path) { if (!state || !path) { return NULL; } @@ -330,9 +292,7 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, // Check if already loaded chroma_image_t *existing = chroma_image_find_by_path(state, path); if (existing && existing->loaded) { - chroma_log("DEBUG", "Using cached image: %s (ref_count: %d)", path, - existing->ref_count); - existing->ref_count++; + chroma_log("DEBUG", "Using cached image: %s", path); return existing; } @@ -347,9 +307,8 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, state->image_count++; } - // Load the image with configuration and output dimensions - if (chroma_image_load(image, path, &state->config, output_width, - output_height) != CHROMA_OK) { + // Load the image with configuration + if (chroma_image_load(image, path, &state->config) != CHROMA_OK) { // If this was a new slot, decrement count if (!existing) { state->image_count--; @@ -417,7 +376,7 @@ void chroma_images_cleanup(chroma_state_t *state) { chroma_log("DEBUG", "Cleaning up %d images", state->image_count); for (int i = 0; i < state->image_count; i++) { - chroma_image_release(&state->images[i]); + chroma_image_free(&state->images[i]); } state->image_count = 0; diff --git a/src/main.c b/src/main.c index 9f1fe92..b0d72c2 100644 --- a/src/main.c +++ b/src/main.c @@ -24,7 +24,7 @@ static void print_usage(const char *program_name) { printf(" -h, --help Show this help\n"); printf(" --version Show version information\n"); printf("\nExamples:\n"); - printf(" %s -c ~/.config/chroma/chroma.toml\n", program_name); + printf(" %s -c ~/.config/chroma/chroma.conf\n", program_name); printf(" %s --daemon\n", program_name); } diff --git a/src/render.c b/src/render.c index 732a1ae..2ade4c4 100644 --- a/src/render.c +++ b/src/render.c @@ -187,6 +187,7 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode, // Vertex shader for simple texture rendering static const char *vertex_shader_source = + "#version 120\n" "attribute vec2 position;\n" "attribute vec2 texcoord;\n" "varying vec2 v_texcoord;\n" @@ -197,7 +198,7 @@ static const char *vertex_shader_source = // Fragment shader for simple texture rendering static const char *fragment_shader_source = - "precision mediump float;\n" + "#version 120\n" "varying vec2 v_texcoord;\n" "uniform sampler2D texture;\n" "void main() {\n" @@ -213,7 +214,7 @@ static const float vertices[] = { -1.0f, 1.0f, 0.0f, 0.0f, // top left }; -static const unsigned short indices[] = { +static const unsigned int indices[] = { 0, 1, 2, // first triangle 2, 3, 0 // second triangle }; @@ -283,29 +284,17 @@ static GLuint create_shader_program(void) { } // Initialize OpenGL resources for output -static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) { +static int init_gl_resources(chroma_output_t *output) { if (!output || output->gl_resources_initialized) { return CHROMA_OK; } - // Use shared shader program from state if available - if (state && state->shader_program != 0) { - output->shader_program = state->shader_program; - chroma_log("DEBUG", "Using shared shader program for output %u", + // Create shader prog + output->shader_program = create_shader_program(); + if (!output->shader_program) { + chroma_log("ERROR", "Failed to create shader program for output %u", output->id); - } else { - // Create shader program - output->shader_program = create_shader_program(); - if (!output->shader_program) { - chroma_log("ERROR", "Failed to create shader program for output %u", - output->id); - return CHROMA_ERROR_EGL; - } - // Store in state for sharing - if (state) { - state->shader_program = output->shader_program; - chroma_log("DEBUG", "Created shared shader program"); - } + return CHROMA_ERROR_EGL; } // Create and setup VBO/EBO @@ -319,10 +308,6 @@ static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) { glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - // Unbind buffers - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - output->texture_id = 0; // will be created when image is assigned output->vbo_dirty = true; // VBO needs initial update output->gl_resources_initialized = true; @@ -385,24 +370,9 @@ static int update_texture_from_image(chroma_output_t *output, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); - // Upload texture data with appropriate format based on channels - GLint internal_format; - GLenum format; - if (image->channels == 4) { - internal_format = GL_RGBA; - format = GL_RGBA; - } else if (image->channels == 3) { - internal_format = GL_RGB; - format = GL_RGB; - } else if (image->channels == 2) { - internal_format = GL_LUMINANCE_ALPHA; - format = GL_LUMINANCE_ALPHA; - } else { - internal_format = GL_LUMINANCE; - format = GL_LUMINANCE; - } - glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height, - 0, format, GL_UNSIGNED_BYTE, image->data); + // Upload texture data (always RGBA now) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, image->data); // Generate mipmaps for trilinear filtering if they're needed if (needs_mipmaps) { @@ -415,24 +385,38 @@ static int update_texture_from_image(chroma_output_t *output, // Mark this output as having uploaded its texture output->texture_uploaded = true; - // Decrement reference count and free system RAM when all outputs - // have uploaded their textures - image->ref_count--; - if (image->ref_count <= 0 && image->data) { - size_t freed_bytes = - (size_t)image->width * (size_t)image->height * (size_t)image->channels; - stbi_image_free(image->data); - image->data = NULL; - chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s", - (double)freed_bytes / (1024.0 * 1024.0), image->path); - chroma_log_resource_deallocation("image_data", freed_bytes, - "post-gpu-upload"); - chroma_log_memory_stats("post-gpu-upload"); - } else { - chroma_log("DEBUG", - "Keeping image data for %s (ref_count: %d, waiting for %d more " - "outputs)", - image->path, image->ref_count, image->ref_count); + // Free system RAM copy only when ALL outputs using this image have uploaded + // to GPU + if (image->data) { + // Count total outputs using this image and how many have uploaded + int total_using = 0; + int uploaded_count = 0; + + chroma_state_t *state = output->state; + for (int i = 0; i < state->output_count; i++) { + if (state->outputs[i].active && state->outputs[i].image == image) { + total_using++; + if (state->outputs[i].texture_uploaded) { + uploaded_count++; + } + } + } + + // Only free image data when ALL outputs using it have uploaded + if (total_using > 0 && uploaded_count >= total_using) { + size_t freed_bytes = (size_t)image->width * (size_t)image->height * + (size_t)image->channels; + stbi_image_free(image->data); + image->data = NULL; + chroma_log("INFO", + "Freed %.2f MB of image data after all %d outputs uploaded to " + "GPU: %s", + (double)freed_bytes / (1024.0 * 1024.0), total_using, + image->path); + chroma_log_resource_deallocation("image_data", freed_bytes, + "post-gpu-upload"); + chroma_log_memory_stats("post-gpu-upload"); + } } chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id, @@ -441,8 +425,7 @@ static int update_texture_from_image(chroma_output_t *output, } // Cleanup OpenGL resources for output -static void cleanup_gl_resources(chroma_state_t *state, - chroma_output_t *output) { +static void cleanup_gl_resources(chroma_output_t *output) { if (!output || !output->gl_resources_initialized) { return; } @@ -453,12 +436,8 @@ static void cleanup_gl_resources(chroma_state_t *state, output->texture_id = 0; } - // Don't delete shared shader program here, it's managed by state if (output->shader_program != 0) { - // Only delete if this output owns the program (not shared) - if (state && state->shader_program != output->shader_program) { - glDeleteProgram(output->shader_program); - } + glDeleteProgram(output->shader_program); output->shader_program = 0; } @@ -487,9 +466,9 @@ static int choose_egl_config(EGLDisplay display, EGLConfig *config) { EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, - 0, + 8, EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES2_BIT, + EGL_OPENGL_BIT, EGL_NONE}; EGLint num_configs; @@ -529,9 +508,9 @@ int chroma_egl_init(chroma_state_t *state) { chroma_log("INFO", "EGL initialized: version %d.%d", major, minor); chroma_log_memory_stats("post-egl-init"); - // Bind OpenGL ES API for better efficiency on embedded/GPU drivers - if (!eglBindAPI(EGL_OPENGL_ES_API)) { - chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError()); + // Bind OpenGL API + if (!eglBindAPI(EGL_OPENGL_API)) { + chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError()); chroma_egl_cleanup(state); return CHROMA_ERROR_EGL; } @@ -542,8 +521,9 @@ int chroma_egl_init(chroma_state_t *state) { return CHROMA_ERROR_EGL; } - // Create EGL context for GLES 2.0 - EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; + // Create EGL context + EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2, + EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE}; state->egl_context = eglCreateContext(state->egl_display, state->egl_config, EGL_NO_CONTEXT, context_attributes); @@ -563,12 +543,6 @@ void chroma_egl_cleanup(chroma_state_t *state) { return; } - // Clean up shared shader program while context is still current - if (state->shader_program != 0) { - glDeleteProgram(state->shader_program); - state->shader_program = 0; - } - if (state->egl_display != EGL_NO_DISPLAY) { eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); @@ -670,8 +644,8 @@ void chroma_surface_destroy(chroma_output_t *output) { return; } - // Clean up OpenGL resources first. We use output's back-reference to state - cleanup_gl_resources(output->state, output); + // Clean up OpenGL resources first + cleanup_gl_resources(output); if (output->egl_surface != EGL_NO_SURFACE) { eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface); @@ -693,8 +667,6 @@ void chroma_surface_destroy(chroma_output_t *output) { output->surface = NULL; } - output->configured = false; - // Log surface destruction size_t surface_size = (size_t)output->width * (size_t)output->height * 4; chroma_log_resource_deallocation("egl_surface", surface_size, @@ -717,7 +689,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { return CHROMA_ERROR_EGL; } - if (init_gl_resources(state, output) != CHROMA_OK) { + if (init_gl_resources(output) != CHROMA_OK) { return CHROMA_ERROR_EGL; } @@ -790,7 +762,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { 4 * sizeof(float), (void *)(2 * sizeof(float))); // Draw - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // Unbind resources glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -798,13 +770,16 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); - // Swap buffers; this implicitly commits the surface for EGL on Wayland + // Swap buffers if (!eglSwapBuffers(state->egl_display, output->egl_surface)) { chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x", output->id, eglGetError()); return CHROMA_ERROR_EGL; } + // Commit surface + wl_surface_commit(output->surface); + chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)", output->id); return CHROMA_OK; diff --git a/src/wayland.c b/src/wayland.c index c5fa252..e931511 100644 --- a/src/wayland.c +++ b/src/wayland.c @@ -91,8 +91,9 @@ static void layer_surface_configure(void *data, chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u", output->id, serial); - // Mark as configured - actual commit happens in render via eglSwapBuffers - output->configured = true; + // Commit the surface to apply the acknowledgment + wl_surface_commit(output->surface); + chroma_log("TRACE", "Surface committed for output %u", output->id); chroma_log("DEBUG", "Acknowledged layer surface configure for output %u", output->id); @@ -381,12 +382,6 @@ void chroma_output_remove(chroma_state_t *state, uint32_t id) { chroma_log("INFO", "Removing output %u (%s)", id, output->name ? output->name : "unknown"); - // Release image reference if the output holds one - if (output->image) { - chroma_image_release(output->image); - output->image = NULL; - } - // Clean up surface if it exists if (output->surface) { chroma_surface_destroy(output);