diff --git a/Makefile b/Makefile index fb133da..0dc9917 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,10 @@ USERLAND_COLLOID_ICONS = $(shell { \ find $(SRC_DIR)/userland -type f -name '*.c' ! -path '*/third_party/*' -exec grep -hoE '"[^"]+\.png"' {} + 2>/dev/null; \ find $(SRC_DIR)/userland -type f -name '*.h' ! -path '*/third_party/*' ! -name 'stb_image.h' -exec grep -hoE '"[^"]+\.png"' {} + 2>/dev/null; \ } | sed 's/"//g' | sed 's@.*/@@' | sort -u) -COLLOID_ICONS = $(sort $(DOCK_COLLOID_ICONS) $(USERLAND_COLLOID_ICONS)) +USERLAND_METADATA_ICONS = $(shell { \ + find $(SRC_DIR)/userland -type f -name '*.c' -exec sed -n 's@^[[:space:]]*//[[:space:]]*BOREDOS_APP_ICONS:[[:space:]]*@@p' {} + 2>/dev/null; \ +} | tr ';' '\n' | sed 's@.*/@@' | sed '/^[[:space:]]*$$/d' | sort -u) +COLLOID_ICONS = $(sort $(DOCK_COLLOID_ICONS) $(USERLAND_COLLOID_ICONS) $(USERLAND_METADATA_ICONS) xterm.png) C_SOURCES = $(wildcard $(SRC_DIR)/core/*.c) \ $(wildcard $(SRC_DIR)/sys/*.c) \ diff --git a/src/sys/app_metadata.c b/src/sys/app_metadata.c new file mode 100644 index 0000000..4435499 --- /dev/null +++ b/src/sys/app_metadata.c @@ -0,0 +1,361 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#include "app_metadata.h" + +#include "memory_manager.h" +#include "vfs.h" + +#define APP_METADATA_CACHE_SIZE 64 + +typedef struct { + bool valid; + bool has_metadata; + char path[VFS_MAX_PATH]; + boredos_app_metadata_t metadata; +} app_metadata_cache_entry_t; + +static app_metadata_cache_entry_t g_app_metadata_cache[APP_METADATA_CACHE_SIZE]; +static int g_app_metadata_cache_next = 0; + +static size_t am_strlen(const char *str) { + size_t len = 0; + if (!str) return 0; + while (str[len]) len++; + return len; +} + +static bool am_str_eq(const char *a, const char *b) { + if (!a || !b) return false; + while (*a && *b) { + if (*a != *b) return false; + a++; + b++; + } + return (*a == '\0' && *b == '\0'); +} + +static bool am_mem_eq(const uint8_t *a, const uint8_t *b, size_t len) { + if (!a || !b) return false; + for (size_t i = 0; i < len; i++) { + if (a[i] != b[i]) return false; + } + return true; +} + +static void am_mem_copy(uint8_t *dest, const uint8_t *src, size_t len) { + if (!dest || !src) return; + for (size_t i = 0; i < len; i++) { + dest[i] = src[i]; + } +} + +static void am_str_copy(char *dest, const char *src, size_t dest_size) { + size_t i = 0; + if (!dest || dest_size == 0) return; + if (!src) { + dest[0] = '\0'; + return; + } + + while (src[i] && i + 1 < dest_size) { + dest[i] = src[i]; + i++; + } + dest[i] = '\0'; +} + +static bool am_seek(vfs_file_t *file, uint64_t offset) { + if (!file) return false; + if (offset > 0x7FFFFFFFUL) return false; + return vfs_seek(file, (int)offset, 0) == 0; +} + +static bool am_read_exact(vfs_file_t *file, void *buf, uint32_t size) { + uint8_t *dst = (uint8_t *)buf; + uint32_t total = 0; + + while (total < size) { + int rc = vfs_read(file, dst + total, (int)(size - total)); + if (rc <= 0) return false; + total += (uint32_t)rc; + } + + return true; +} + +static uint32_t am_align4(uint32_t value) { + return (value + 3U) & ~3U; +} + +static bool am_validate_metadata(const boredos_app_metadata_t *metadata) { + if (!metadata) return false; + if (metadata->magic != BOREDOS_APP_METADATA_MAGIC) return false; + if (metadata->version != BOREDOS_APP_METADATA_VERSION) return false; + if (metadata->image_count > BOREDOS_APP_METADATA_MAX_IMAGES) return false; + return true; +} + +static bool am_note_name_matches(const char *name, uint32_t name_size) { + size_t expected_len = am_strlen(BOREDOS_APP_NOTE_OWNER); + if (!name || name_size == 0) return false; + if ((size_t)name_size < expected_len) return false; + + for (size_t i = 0; i < expected_len; i++) { + if (name[i] != BOREDOS_APP_NOTE_OWNER[i]) return false; + } + return true; +} + +static void am_sanitize_metadata(boredos_app_metadata_t *metadata) { + if (!metadata) return; + + metadata->app_name[BOREDOS_APP_METADATA_MAX_APP_NAME - 1] = '\0'; + metadata->description[BOREDOS_APP_METADATA_MAX_DESCRIPTION - 1] = '\0'; + + for (uint32_t i = 0; i < BOREDOS_APP_METADATA_MAX_IMAGES; i++) { + metadata->images[i][BOREDOS_APP_METADATA_MAX_IMAGE_PATH - 1] = '\0'; + } + + if (metadata->image_count > BOREDOS_APP_METADATA_MAX_IMAGES) { + metadata->image_count = BOREDOS_APP_METADATA_MAX_IMAGES; + } +} + +static bool am_parse_note_section(vfs_file_t *file, + const Elf64_Shdr *section, + boredos_app_metadata_t *out_metadata) { + uint32_t offset = 0; + + if (!file || !section || !out_metadata) return false; + + while ((uint64_t)offset + sizeof(Elf64_Nhdr) <= section->sh_size) { + Elf64_Nhdr nhdr; + if (!am_seek(file, section->sh_offset + offset)) return false; + if (!am_read_exact(file, &nhdr, sizeof(Elf64_Nhdr))) return false; + + offset += (uint32_t)sizeof(Elf64_Nhdr); + + if ((uint64_t)offset + nhdr.n_namesz > section->sh_size) return false; + if (nhdr.n_namesz > 256U) return false; + + char *name_buf = (char *)kmalloc((size_t)nhdr.n_namesz + 1U); + if (!name_buf) return false; + + if (!am_seek(file, section->sh_offset + offset) || !am_read_exact(file, name_buf, nhdr.n_namesz)) { + kfree(name_buf); + return false; + } + name_buf[nhdr.n_namesz] = '\0'; + + offset += nhdr.n_namesz; + offset = am_align4(offset); + + if ((uint64_t)offset + nhdr.n_descsz > section->sh_size) { + kfree(name_buf); + return false; + } + + bool is_target_note = (nhdr.n_type == BOREDOS_APP_NOTE_TYPE) && am_note_name_matches(name_buf, nhdr.n_namesz); + kfree(name_buf); + + if (is_target_note) { + if (nhdr.n_descsz < sizeof(boredos_app_metadata_t)) return false; + + boredos_app_metadata_t metadata; + if (!am_seek(file, section->sh_offset + offset) || + !am_read_exact(file, &metadata, (uint32_t)sizeof(boredos_app_metadata_t))) { + return false; + } + + if (!am_validate_metadata(&metadata)) return false; + + am_sanitize_metadata(&metadata); + *out_metadata = metadata; + return true; + } + + offset += nhdr.n_descsz; + offset = am_align4(offset); + } + + return false; +} + +static bool am_scan_raw_notes(vfs_file_t *file, uint32_t file_size, boredos_app_metadata_t *out_metadata) { + uint8_t *buf = NULL; + size_t owner_len = am_strlen(BOREDOS_APP_NOTE_OWNER); + + if (!file || !out_metadata || file_size < sizeof(Elf64_Nhdr) + owner_len + sizeof(boredos_app_metadata_t)) { + return false; + } + if (file_size > 16U * 1024U * 1024U) { + return false; + } + + buf = (uint8_t *)kmalloc(file_size); + if (!buf) return false; + + if (!am_seek(file, 0) || !am_read_exact(file, buf, file_size)) { + kfree(buf); + return false; + } + + for (uint32_t off = 0; off + sizeof(Elf64_Nhdr) <= file_size; off++) { + Elf64_Nhdr nhdr; + uint32_t name_off; + uint32_t desc_off; + boredos_app_metadata_t metadata; + + am_mem_copy((uint8_t *)&nhdr, buf + off, sizeof(Elf64_Nhdr)); + if (nhdr.n_type != BOREDOS_APP_NOTE_TYPE) continue; + if (nhdr.n_namesz < owner_len) continue; + if (nhdr.n_descsz < sizeof(boredos_app_metadata_t)) continue; + + name_off = off + (uint32_t)sizeof(Elf64_Nhdr); + if ((uint64_t)name_off + nhdr.n_namesz > file_size) continue; + + if (!am_mem_eq(buf + name_off, (const uint8_t *)BOREDOS_APP_NOTE_OWNER, owner_len)) continue; + + desc_off = name_off + am_align4(nhdr.n_namesz); + if ((uint64_t)desc_off + sizeof(boredos_app_metadata_t) > file_size) continue; + + am_mem_copy((uint8_t *)&metadata, buf + desc_off, sizeof(boredos_app_metadata_t)); + if (!am_validate_metadata(&metadata)) continue; + + am_sanitize_metadata(&metadata); + *out_metadata = metadata; + kfree(buf); + return true; + } + + kfree(buf); + return false; +} + +static bool app_metadata_read_uncached(const char *path, boredos_app_metadata_t *out_metadata) { + bool found = false; + vfs_file_t *file = NULL; + char *shstrtab = NULL; + uint32_t file_size = 0; + + if (!path || !out_metadata) return false; + + file = vfs_open(path, "r"); + if (!file || !file->valid) goto cleanup; + + file_size = vfs_file_size(file); + if (file_size > 0) { + found = am_scan_raw_notes(file, file_size, out_metadata); + if (found) goto cleanup; + } + + Elf64_Ehdr ehdr; + if (!am_read_exact(file, &ehdr, sizeof(Elf64_Ehdr))) goto cleanup; + + if (ehdr.e_ident[0] != ELFMAG0 || ehdr.e_ident[1] != ELFMAG1 || + ehdr.e_ident[2] != ELFMAG2 || ehdr.e_ident[3] != ELFMAG3) { + goto cleanup; + } + + if (ehdr.e_ident[4] != ELFCLASS64 || ehdr.e_ident[5] != ELFDATA2LSB) goto cleanup; + if (ehdr.e_shoff == 0 || ehdr.e_shnum == 0 || ehdr.e_shentsize < sizeof(Elf64_Shdr)) goto cleanup; + if (ehdr.e_shstrndx == 0 || ehdr.e_shstrndx >= ehdr.e_shnum) goto cleanup; + + Elf64_Shdr shstr_hdr; + uint64_t shstr_off = ehdr.e_shoff + ((uint64_t)ehdr.e_shstrndx * ehdr.e_shentsize); + if (!am_seek(file, shstr_off) || !am_read_exact(file, &shstr_hdr, sizeof(Elf64_Shdr))) goto cleanup; + if (shstr_hdr.sh_size == 0 || shstr_hdr.sh_size > 65536U) goto cleanup; + + shstrtab = (char *)kmalloc((size_t)shstr_hdr.sh_size + 1U); + if (!shstrtab) goto cleanup; + if (!am_seek(file, shstr_hdr.sh_offset) || !am_read_exact(file, shstrtab, (uint32_t)shstr_hdr.sh_size)) goto cleanup; + shstrtab[shstr_hdr.sh_size] = '\0'; + + for (uint16_t i = 0; i < ehdr.e_shnum; i++) { + Elf64_Shdr shdr; + uint64_t shdr_off = ehdr.e_shoff + ((uint64_t)i * ehdr.e_shentsize); + if (!am_seek(file, shdr_off) || !am_read_exact(file, &shdr, sizeof(Elf64_Shdr))) goto cleanup; + if (shdr.sh_type != SHT_NOTE) continue; + if (shdr.sh_name >= shstr_hdr.sh_size) continue; + + const char *section_name = shstrtab + shdr.sh_name; + if (!am_str_eq(section_name, BOREDOS_APP_NOTE_SECTION)) continue; + + if (am_parse_note_section(file, &shdr, out_metadata)) { + found = true; + break; + } + } + + if (!found && file_size > 0) { + found = am_scan_raw_notes(file, file_size, out_metadata); + } + +cleanup: + if (shstrtab) kfree(shstrtab); + if (file) vfs_close(file); + return found; +} + +static bool app_metadata_cache_lookup(const char *path, boredos_app_metadata_t *out_metadata, bool *out_found) { + if (!path || !out_found) return false; + + for (int i = 0; i < APP_METADATA_CACHE_SIZE; i++) { + app_metadata_cache_entry_t *entry = &g_app_metadata_cache[i]; + if (!entry->valid) continue; + if (!am_str_eq(entry->path, path)) continue; + + *out_found = entry->has_metadata; + if (entry->has_metadata && out_metadata) { + *out_metadata = entry->metadata; + } + return true; + } + + return false; +} + +static void app_metadata_cache_store(const char *path, const boredos_app_metadata_t *metadata, bool has_metadata) { + app_metadata_cache_entry_t *entry; + + if (!path) return; + + entry = &g_app_metadata_cache[g_app_metadata_cache_next]; + g_app_metadata_cache_next = (g_app_metadata_cache_next + 1) % APP_METADATA_CACHE_SIZE; + + entry->valid = true; + entry->has_metadata = has_metadata; + am_str_copy(entry->path, path, sizeof(entry->path)); + if (has_metadata && metadata) { + entry->metadata = *metadata; + } +} + +bool app_metadata_read(const char *path, boredos_app_metadata_t *out_metadata) { + bool found = false; + + if (!path || !out_metadata) return false; + + if (app_metadata_cache_lookup(path, out_metadata, &found)) { + return found; + } + + found = app_metadata_read_uncached(path, out_metadata); + app_metadata_cache_store(path, out_metadata, found); + return found; +} + +bool app_metadata_get_primary_image(const char *path, char *out_path, size_t out_path_size) { + boredos_app_metadata_t metadata; + + if (!path || !out_path || out_path_size == 0) return false; + out_path[0] = '\0'; + + if (!app_metadata_read(path, &metadata)) return false; + if (metadata.image_count == 0) return false; + if (metadata.images[0][0] == '\0') return false; + + am_str_copy(out_path, metadata.images[0], out_path_size); + return out_path[0] != '\0'; +} diff --git a/src/sys/app_metadata.h b/src/sys/app_metadata.h new file mode 100644 index 0000000..dacb8b4 --- /dev/null +++ b/src/sys/app_metadata.h @@ -0,0 +1,14 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#ifndef APP_METADATA_H +#define APP_METADATA_H + +#include +#include +#include "elf.h" + +bool app_metadata_read(const char *path, boredos_app_metadata_t *out_metadata); +bool app_metadata_get_primary_image(const char *path, char *out_path, size_t out_path_size); + +#endif diff --git a/src/sys/elf.h b/src/sys/elf.h index 20f286c..7dfaa0a 100644 --- a/src/sys/elf.h +++ b/src/sys/elf.h @@ -44,6 +44,25 @@ typedef struct { Elf64_Xword p_align; /* Segment alignment */ } Elf64_Phdr; +typedef struct { + Elf64_Word sh_name; /* Section name (string tbl index) */ + Elf64_Word sh_type; /* Section type */ + Elf64_Xword sh_flags; /* Section flags */ + Elf64_Addr sh_addr; /* Section virtual addr at execution */ + Elf64_Off sh_offset; /* Section file offset */ + Elf64_Xword sh_size; /* Section size in bytes */ + Elf64_Word sh_link; /* Link to another section */ + Elf64_Word sh_info; /* Additional section information */ + Elf64_Xword sh_addralign; /* Section alignment */ + Elf64_Xword sh_entsize; /* Entry size if section holds table */ +} Elf64_Shdr; + +typedef struct { + Elf64_Word n_namesz; /* Name size in bytes */ + Elf64_Word n_descsz; /* Descriptor size in bytes */ + Elf64_Word n_type; /* Note type */ +} Elf64_Nhdr; + /* e_ident constants */ #define ELFMAG0 0x7f #define ELFMAG1 'E' @@ -63,11 +82,37 @@ typedef struct { /* p_type constants */ #define PT_LOAD 1 +/* sh_type constants */ +#define SHT_NOTE 7 + /* p_flags constants */ #define PF_X 1 #define PF_W 2 #define PF_R 4 +/* BoredOS app metadata note constants */ +#define BOREDOS_APP_NOTE_OWNER "BOREDOS" +#define BOREDOS_APP_NOTE_NAME BOREDOS_APP_NOTE_OWNER +#define BOREDOS_APP_NOTE_SECTION ".note.boredos.app" +#define BOREDOS_APP_NOTE_TYPE 0x41505031U +#define BOREDOS_APP_METADATA_MAGIC 0x414d4431U +#define BOREDOS_APP_METADATA_VERSION 1U + +#define BOREDOS_APP_METADATA_MAX_APP_NAME 64 +#define BOREDOS_APP_METADATA_MAX_DESCRIPTION 192 +#define BOREDOS_APP_METADATA_MAX_IMAGES 4 +#define BOREDOS_APP_METADATA_MAX_IMAGE_PATH 160 + +typedef struct __attribute__((packed)) { + uint32_t magic; + uint16_t version; + uint16_t image_count; + uint16_t reserved; + char app_name[BOREDOS_APP_METADATA_MAX_APP_NAME]; + char description[BOREDOS_APP_METADATA_MAX_DESCRIPTION]; + char images[BOREDOS_APP_METADATA_MAX_IMAGES][BOREDOS_APP_METADATA_MAX_IMAGE_PATH]; +} boredos_app_metadata_t; + #include #include diff --git a/src/userland/Makefile b/src/userland/Makefile index 6e2f2fa..41822b1 100644 --- a/src/userland/Makefile +++ b/src/userland/Makefile @@ -5,10 +5,19 @@ CC = x86_64-elf-gcc AS = nasm LD = x86_64-elf-ld -CFLAGS = -Wall -Wextra -std=gnu11 -ffreestanding -O2 -fno-stack-protector -fno-stack-check -fno-lto -fno-pie -m64 -march=x86-64 -mno-red-zone -I. -Ilibc +CFLAGS = -Wall -Wextra -std=gnu11 -ffreestanding -O2 -fno-stack-protector -fno-stack-check -fno-lto -fno-pie -m64 -march=x86-64 -mno-red-zone -I. -Ilibc -I../sys LDFLAGS = -m elf_x86_64 -nostdlib -static -no-pie -Ttext=0x40000000 --no-dynamic-linker -z text -z max-page-size=0x1000 -e _start BIN_DIR = bin +APP_METADATA_TOOL = ../../tools/gen_userland_note.sh +APP_ICON_SOURCE_DIR = ../images/icons/colloid +APP_METADATA_SOURCE_DOOM = games/doom/doomgeneric_boredos.c +APP_METADATA_SOURCE_LUA = cli/third_party/lua/boredos_onelua.c +APP_SOURCE_DIRS = . cli gui sys games net cli/third_party + +define app_source_for +$(if $(filter doom,$1),$(APP_METADATA_SOURCE_DOOM),$(if $(filter lua,$1),$(APP_METADATA_SOURCE_LUA),$(firstword $(foreach d,$(APP_SOURCE_DIRS),$(wildcard $(d)/$1.c))))) +endef LIBC_SOURCES = $(wildcard libc/*.c) LIBC_OBJS = $(patsubst libc/%.c, $(BIN_DIR)/%.o, $(LIBC_SOURCES)) $(BIN_DIR)/crt0.o $(BIN_DIR)/libwidget.o @@ -19,6 +28,9 @@ vpath %.c cli gui sys games libc net cli/third_party APP_SOURCES_FULL = $(wildcard cli/*.c gui/*.c sys/*.c games/*.c *.c net/*.c cli/third_party/*.c) APP_SOURCES = $(filter-out stb_image.c, $(APP_SOURCES_FULL)) APP_ELFS = $(patsubst %.c, $(BIN_DIR)/%.elf, $(notdir $(APP_SOURCES))) +APP_NAMES = $(sort $(basename $(notdir $(APP_SOURCES))) doom lua) +APP_NOTE_CSOURCES = $(patsubst %, $(BIN_DIR)/%.note.c, $(APP_NAMES)) +APP_NOTE_OBJS = $(patsubst %, $(BIN_DIR)/%.note.o, $(APP_NAMES)) DOOM_SOURCES = $(wildcard games/doom/*.c) DOOM_OBJS = $(patsubst games/doom/%.c, $(BIN_DIR)/%.o, $(DOOM_SOURCES)) @@ -43,35 +55,46 @@ $(BIN_DIR)/libwidget.o: ../wm/libwidget.c | $(BIN_DIR) $(BIN_DIR)/stb_image.o: stb_image.c | $(BIN_DIR) $(CC) $(CFLAGS) -c $< -o $@ +$(BIN_DIR)/%.note.c: $(APP_METADATA_TOOL) | $(BIN_DIR) + src="$(call app_source_for,$*)"; \ + if [ -z "$$src" ] || [ ! -f "$$src" ]; then \ + echo "error: metadata source not found for app '$*'" >&2; \ + exit 1; \ + fi; \ + sh $(APP_METADATA_TOOL) "$*" "$$src" "$(APP_ICON_SOURCE_DIR)" "$@" + +$(BIN_DIR)/%.note.o: $(BIN_DIR)/%.note.c + $(CC) $(CFLAGS) -c $< -o $@ + $(BIN_DIR)/%.o: %.c | $(BIN_DIR) $(CC) $(CFLAGS) -c $< -o $@ -$(BIN_DIR)/viewer.elf: $(LIBC_OBJS) $(BIN_DIR)/viewer.o $(BIN_DIR)/stb_image.o +$(BIN_DIR)/viewer.elf: $(LIBC_OBJS) $(BIN_DIR)/viewer.o $(BIN_DIR)/stb_image.o $(BIN_DIR)/viewer.note.o $(LD) $(LDFLAGS) $^ -o $@ -$(BIN_DIR)/settings.elf: $(LIBC_OBJS) $(BIN_DIR)/settings.o $(BIN_DIR)/stb_image.o +$(BIN_DIR)/settings.elf: $(LIBC_OBJS) $(BIN_DIR)/settings.o $(BIN_DIR)/stb_image.o $(BIN_DIR)/settings.note.o $(LD) $(LDFLAGS) $^ -o $@ -$(BIN_DIR)/browser.elf: $(LIBC_OBJS) $(BIN_DIR)/browser.o $(BIN_DIR)/stb_image.o +$(BIN_DIR)/browser.elf: $(LIBC_OBJS) $(BIN_DIR)/browser.o $(BIN_DIR)/stb_image.o $(BIN_DIR)/browser.note.o $(LD) $(LDFLAGS) $^ -o $@ -$(BIN_DIR)/screenshot.elf: $(LIBC_OBJS) $(BIN_DIR)/screenshot.o $(BIN_DIR)/stb_image.o +$(BIN_DIR)/screenshot.elf: $(LIBC_OBJS) $(BIN_DIR)/screenshot.o $(BIN_DIR)/stb_image.o $(BIN_DIR)/screenshot.note.o $(LD) $(LDFLAGS) $^ -o $@ $(BIN_DIR)/%.o: games/doom/%.c | $(BIN_DIR) $(CC) $(CFLAGS) -Wno-error -DBOREDOS -Igames/doom -c $< -o $@ -$(BIN_DIR)/doom.elf: $(LIBC_OBJS) $(DOOM_OBJS) $(BIN_DIR)/stb_image.o +$(BIN_DIR)/doom.elf: $(LIBC_OBJS) $(DOOM_OBJS) $(BIN_DIR)/stb_image.o $(BIN_DIR)/doom.note.o $(LD) $(LDFLAGS) $^ -o $@ -$(BIN_DIR)/%.elf: $(LIBC_OBJS) $(BIN_DIR)/%.o +$(BIN_DIR)/%.elf: $(LIBC_OBJS) $(BIN_DIR)/%.o $(BIN_DIR)/%.note.o $(LD) $(LDFLAGS) $^ -o $@ # Lua 5.5.0 - compiled as a single translation unit $(BIN_DIR)/lua_onelua.o: $(LUA_DIR)/boredos_onelua.c | $(BIN_DIR) $(CC) $(LUA_CFLAGS) -c $< -o $@ -$(BIN_DIR)/lua.elf: $(LIBC_OBJS) $(BIN_DIR)/lua_onelua.o +$(BIN_DIR)/lua.elf: $(LIBC_OBJS) $(BIN_DIR)/lua_onelua.o $(BIN_DIR)/lua.note.o $(LD) $(LDFLAGS) $^ -o $@ clean: diff --git a/src/userland/cli/help.c b/src/userland/cli/help.c index 8648e48..a06b980 100644 --- a/src/userland/cli/help.c +++ b/src/userland/cli/help.c @@ -1,6 +1,7 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Show command and system help. #include #include diff --git a/src/userland/cli/man.c b/src/userland/cli/man.c index 1fb8b55..360be7c 100644 --- a/src/userland/cli/man.c +++ b/src/userland/cli/man.c @@ -1,6 +1,7 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Manual pages CLI utility #include #include diff --git a/src/userland/cli/third_party/lua/boredos_onelua.c b/src/userland/cli/third_party/lua/boredos_onelua.c index 4c127e2..59f70b4 100644 --- a/src/userland/cli/third_party/lua/boredos_onelua.c +++ b/src/userland/cli/third_party/lua/boredos_onelua.c @@ -3,6 +3,7 @@ ** Based on the official onelua.c, adapted for BoredOS freestanding environment. */ +// BOREDOS_APP_DESC: Lua REPL and scripting runtime. #include #include #include diff --git a/src/userland/games/2048.c b/src/userland/games/2048.c index 1b01ea1..721c7f5 100644 --- a/src/userland/games/2048.c +++ b/src/userland/games/2048.c @@ -1,3 +1,5 @@ +// BOREDOS_APP_DESC: 2048 number puzzle game. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/applications-games.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/games/doom/doomgeneric_boredos.c b/src/userland/games/doom/doomgeneric_boredos.c index 6a6abf0..5140233 100644 --- a/src/userland/games/doom/doomgeneric_boredos.c +++ b/src/userland/games/doom/doomgeneric_boredos.c @@ -1,3 +1,5 @@ +// BOREDOS_APP_DESC: DOOM game runtime. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/doom-2016.png;/Library/images/icons/colloid/applications-games.png #include "doomgeneric.h" #include "doomkeys.h" #include diff --git a/src/userland/games/minesweeper.c b/src/userland/games/minesweeper.c index 3c5ce96..b8de810 100644 --- a/src/userland/games/minesweeper.c +++ b/src/userland/games/minesweeper.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Minesweeper puzzle game. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/gnome-mines.png;/Library/images/icons/colloid/applications-games.png #include "libc/syscall.h" #include "libc/libui.h" #include diff --git a/src/userland/games/snake.c b/src/userland/games/snake.c index db8af45..69ae859 100644 --- a/src/userland/games/snake.c +++ b/src/userland/games/snake.c @@ -1,3 +1,5 @@ +// BOREDOS_APP_DESC: Classic snake arcade game. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/cartridges.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/about.c b/src/userland/gui/about.c index 3a5763e..73134ae 100644 --- a/src/userland/gui/about.c +++ b/src/userland/gui/about.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Shows BoredOS information. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/indicator-cpufreq.png #include "syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/boredword.c b/src/userland/gui/boredword.c index af51ba4..8808100 100644 --- a/src/userland/gui/boredword.c +++ b/src/userland/gui/boredword.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Document and PDF viewer/editor. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/libreoffice-writer.png;/Library/images/icons/colloid/text-editor.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/browser.c b/src/userland/gui/browser.c index 7c29386..966d80d 100644 --- a/src/userland/gui/browser.c +++ b/src/userland/gui/browser.c @@ -1,5 +1,7 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// BOREDOS_APP_DESC: Web browser for internet pages. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/web-browser.png #include "libc/syscall.h" #include "libc/libui.h" #include "stb_image.h" @@ -2134,4 +2136,4 @@ int main(int argc, char **argv) { } } return 0; -} \ No newline at end of file +} diff --git a/src/userland/gui/calculator.c b/src/userland/gui/calculator.c index e11fd46..da2d528 100644 --- a/src/userland/gui/calculator.c +++ b/src/userland/gui/calculator.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Graphical calculator utility. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/calc.png #include "syscall.h" #include "libui.h" #include "../../wm/libwidget.h" diff --git a/src/userland/gui/grapher.c b/src/userland/gui/grapher.c index aa40945..61bbda4 100644 --- a/src/userland/gui/grapher.c +++ b/src/userland/gui/grapher.c @@ -2,6 +2,8 @@ // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: 3/2D Graphing and plotting utility. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/se.sjoerd.Graphs.png;/Library/images/icons/colloid/app-icon-preview.png #include "syscall.h" #include "libui.h" #include "../../wm/libwidget.h" @@ -1765,4 +1767,4 @@ int main(void) { free(graph_fb); sys_exit(0); return 0; -} \ No newline at end of file +} diff --git a/src/userland/gui/markdown.c b/src/userland/gui/markdown.c index 00d2604..7d8029d 100644 --- a/src/userland/gui/markdown.c +++ b/src/userland/gui/markdown.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Markdown document viewer. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/text-editor.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/notepad.c b/src/userland/gui/notepad.c index 1e586e3..c6b118b 100644 --- a/src/userland/gui/notepad.c +++ b/src/userland/gui/notepad.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Jotting down notes and thoughts. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/text-editor.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/paint.c b/src/userland/gui/paint.c index db72643..264e7e4 100644 --- a/src/userland/gui/paint.c +++ b/src/userland/gui/paint.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Simple drawing and paint app. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/gnome-paint.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" @@ -247,4 +249,4 @@ int main(int argc, char **argv) { } } return 0; -} \ No newline at end of file +} diff --git a/src/userland/gui/screenshot.c b/src/userland/gui/screenshot.c index 58809a0..8180272 100644 --- a/src/userland/gui/screenshot.c +++ b/src/userland/gui/screenshot.c @@ -1,3 +1,5 @@ +// BOREDOS_APP_DESC: Screen capture utility. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/accessories-screenshot.png #include #include #include "stdlib.h" diff --git a/src/userland/gui/settings.c b/src/userland/gui/settings.c index 175a2cd..199d255 100644 --- a/src/userland/gui/settings.c +++ b/src/userland/gui/settings.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: System configuration and preferences. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/preferences-system.png;/Library/images/icons/colloid/preferences-system-services.png #include "libc/syscall.h" #include "libc/libui.h" #include "libc/stdlib.h" diff --git a/src/userland/gui/taskman.c b/src/userland/gui/taskman.c index 593c70f..dc533ff 100644 --- a/src/userland/gui/taskman.c +++ b/src/userland/gui/taskman.c @@ -1,5 +1,7 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// BOREDOS_APP_DESC: Task and process manager. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/utilities-system-monitor.png #include "syscall.h" #include "libui.h" #include "stdlib.h" diff --git a/src/userland/gui/terminal.c b/src/userland/gui/terminal.c index a77ba04..8c1d0da 100644 --- a/src/userland/gui/terminal.c +++ b/src/userland/gui/terminal.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: BoredOS Terminal shell. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/xterm.png;/Library/images/icons/colloid/utilities-terminal_su.png #include #include #include "libc/libui.h" diff --git a/src/userland/gui/viewer.c b/src/userland/gui/viewer.c index b22ebb4..cf1706d 100644 --- a/src/userland/gui/viewer.c +++ b/src/userland/gui/viewer.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Image viewer utility. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/preferences-desktop-wallpaper.png;/Library/images/icons/colloid/org.gnome.Loupe.png #include "stb_image.h" #include "libc/syscall.h" #include "libc/libui.h" diff --git a/src/userland/sys/clock.c b/src/userland/sys/clock.c index a733c7d..379615b 100644 --- a/src/userland/sys/clock.c +++ b/src/userland/sys/clock.c @@ -1,6 +1,8 @@ // Copyright (c) 2023-2026 Chris (boreddevnl) // This software is released under the GNU General Public License v3.0. See LICENSE file for details. // This header needs to maintain in any file it is present in, as per the GPL license terms. +// BOREDOS_APP_DESC: Clock and time utility. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/preferences-system-time.png #include "syscall.h" #include "libui.h" #include "stdlib.h" diff --git a/src/wm/explorer.c b/src/wm/explorer.c index cfe7cd4..ec23b86 100644 --- a/src/wm/explorer.c +++ b/src/wm/explorer.c @@ -10,6 +10,7 @@ #include "wm.h" #include "memory_manager.h" #include "process.h" +#include "app_metadata.h" #define EXPLORER_ITEM_HEIGHT 80 #define EXPLORER_ITEM_WIDTH 120 #define EXPLORER_COLS 4 @@ -901,8 +902,6 @@ static void explorer_draw_colloid_slot_icon(int x, int y, int slot_index) { } static void explorer_draw_file_icon(int x, int y, bool is_dir, const char *filename, const char *current_path) { - (void)current_path; - if (is_dir) { explorer_draw_colloid_slot_icon(x + 5, y + 5, EXPLORER_DOCK_SLOT_FILES); } else if (explorer_str_ends_with(filename, ".shortcut")) { @@ -948,7 +947,17 @@ static void explorer_draw_file_icon(int x, int y, bool is_dir, const char *filen } else if (explorer_str_ends_with(filename, ".pdf")) { explorer_draw_colloid_slot_icon(x + 5, y + 5, EXPLORER_DOCK_SLOT_WORD); } else if (explorer_str_ends_with(filename, ".elf")) { - explorer_draw_colloid_slot_icon(x + 5, y + 5, EXPLORER_DOCK_SLOT_TERMINAL); + char app_path[FAT32_MAX_PATH]; + char icon_path[BOREDOS_APP_METADATA_MAX_IMAGE_PATH]; + + explorer_strcpy(app_path, current_path); + if (app_path[explorer_strlen(app_path) - 1] != '/') explorer_strcat(app_path, "/"); + explorer_strcat(app_path, filename); + + if (!(app_metadata_get_primary_image(app_path, icon_path, sizeof(icon_path)) && + draw_icon_path(x + 5, y + 5, icon_path))) { + explorer_draw_colloid_slot_icon(x + 5, y + 5, EXPLORER_DOCK_SLOT_TERMINAL); + } } else { explorer_draw_colloid_slot_icon(x + 5, y + 5, EXPLORER_DOCK_SLOT_NOTEPAD); } diff --git a/src/wm/wm.c b/src/wm/wm.c index 6f42282..6f19340 100644 --- a/src/wm/wm.c +++ b/src/wm/wm.c @@ -18,6 +18,7 @@ #include "userland/stb_image.h" #include "memory_manager.h" #include "disk.h" +#include "app_metadata.h" #include "../sys/work_queue.h" #include "../sys/smp.h" #include "../core/kconsole.h" @@ -1103,6 +1104,39 @@ void draw_image_icon(int x, int y, const char *label) { // Removing the explicit `draw_icon_label` call here to prevent double-text since `wm.c` or Explorer manually draws it as well inside their draw block } +bool draw_icon_path(int x, int y, const char *path) { + uint32_t *icon = NULL; + + if (!path || !path[0]) return false; + + icon = thumb_cache_lookup(path); + if (!icon && !thumb_cache_is_failed(path)) { + icon = thumb_cache_decode(path); + } + if (!icon) return false; + + int dx = x + 24; + int dy = y + 12; + for (int ty = 0; ty < 32; ty++) { + for (int tx = 0; tx < 32; tx++) { + int src_x = tx * 48 / 32; + int src_y = ty * 48 / 32; + uint32_t src = icon[src_y * 48 + src_x]; + uint32_t a = (src >> 24) & 0xFF; + if (a == 0) continue; + + if (a == 255) { + put_pixel(dx + tx, dy + ty, 0xFF000000 | (src & 0x00FFFFFF)); + } else { + uint32_t dst = graphics_get_pixel(dx + tx, dy + ty); + put_pixel(dx + tx, dy + ty, blend_src_over_dst(dst, src)); + } + } + } + + return true; +} + void draw_notepad_icon(int x, int y, const char *label) { draw_scaled_icon(x, y, draw_dock_notepad); draw_icon_label(x, y, label); @@ -1839,7 +1873,29 @@ static void wm_paint_region(int y_start, int y_end, DirtyRect dirty, int pass) { else if (str_starts_with(icon->name, "Grapher")) draw_grapher_icon(icon->x, icon->y, label); else draw_icon(icon->x, icon->y, label); } else { - if (str_ends_with(icon->name, ".elf")) draw_elf_icon(icon->x, icon->y, icon->name); + if (str_ends_with(icon->name, ".elf")) { + char full_path[128] = "/root/Desktop/"; + char icon_path[BOREDOS_APP_METADATA_MAX_IMAGE_PATH]; + bool drew_icon = false; + int p = 14; + int n = 0; + + while (icon->name[n] && p < 127) full_path[p++] = icon->name[n++]; + full_path[p] = 0; + + if (app_metadata_get_primary_image(full_path, icon_path, sizeof(icon_path))) { + drew_icon = draw_icon_path(icon->x, icon->y, icon_path); + } + if (!drew_icon) { + drew_icon = draw_icon_path(icon->x, icon->y, "/Library/images/icons/colloid/xterm.png"); + } + + if (drew_icon) { + draw_icon_label(icon->x, icon->y, icon->name); + } else { + draw_elf_icon(icon->x, icon->y, icon->name); + } + } else if (str_ends_with(icon->name, ".pnt")) draw_paint_icon(icon->x, icon->y, icon->name); else if (is_image_file(icon->name)) { char full_path[128] = "/root/Desktop/"; int p=14; int n=0; while(icon->name[n] && p < 127) full_path[p++] = icon->name[n++]; full_path[p]=0; diff --git a/src/wm/wm.h b/src/wm/wm.h index 8046370..c4a2398 100644 --- a/src/wm/wm.h +++ b/src/wm/wm.h @@ -127,6 +127,7 @@ void draw_document_icon(int x, int y, const char *label); void draw_pdf_icon(int x, int y, const char *label); void draw_elf_icon(int x, int y, const char *label); void draw_image_icon(int x, int y, const char *label); +bool draw_icon_path(int x, int y, const char *path); void draw_notepad_icon(int x, int y, const char *label); void draw_calculator_icon(int x, int y, const char *label); void draw_terminal_icon(int x, int y, const char *label); diff --git a/tools/gen_userland_note.sh b/tools/gen_userland_note.sh new file mode 100644 index 0000000..89454bc --- /dev/null +++ b/tools/gen_userland_note.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env sh +set -eu + +if [ "$#" -ne 4 ]; then + echo "usage: gen_userland_note.sh " >&2 + exit 1 +fi + +APP_NAME="$1" +SOURCE_PATH="$2" +ICON_SOURCE_DIR="$3" +OUT_PATH="$4" + +MAX_APP_NAME=63 +MAX_DESC=191 +MAX_IMAGE_PATH=159 +MAX_IMAGES=4 + +DEFAULT_ICON_PATH="/Library/images/icons/colloid/xterm.png" +DEFAULT_DESC="BoredOS userspace application." + +escape_c_string() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +trim_spaces() { + printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' +} + +truncate_bytes() { + value="$1" + max_len="$2" + printf '%s' "$value" | cut -c1-"$max_len" +} + +if [ ! -f "$SOURCE_PATH" ]; then + echo "error: source file '$SOURCE_PATH' not found for app '$APP_NAME'" >&2 + exit 1 +fi + +app_desc="$DEFAULT_DESC" +image_spec="$DEFAULT_ICON_PATH" + +source_desc=$(sed -n 's@^[[:space:]]*//[[:space:]]*BOREDOS_APP_DESC:[[:space:]]*@@p' "$SOURCE_PATH" | head -n 1) +source_icons=$(sed -n 's@^[[:space:]]*//[[:space:]]*BOREDOS_APP_ICONS:[[:space:]]*@@p' "$SOURCE_PATH" | head -n 1) + +if [ -n "$source_desc" ]; then + app_desc="$source_desc" +fi +if [ -n "$source_icons" ]; then + image_spec="$source_icons" +fi + +app_desc=$(trim_spaces "$app_desc") +if [ -z "$app_desc" ]; then + app_desc="$DEFAULT_DESC" +fi + +image_spec=$(trim_spaces "$image_spec") +if [ -z "$image_spec" ]; then + image_spec="$DEFAULT_ICON_PATH" +fi + +app_name_value=$(truncate_bytes "$APP_NAME" "$MAX_APP_NAME") +app_desc=$(truncate_bytes "$app_desc" "$MAX_DESC") + +IMAGE_1="" +IMAGE_2="" +IMAGE_3="" +IMAGE_4="" +IMAGE_COUNT=0 + +saved_ifs="$IFS" +IFS=';' +for raw_image in $image_spec; do + image_path=$(trim_spaces "$raw_image") + if [ -z "$image_path" ]; then + continue + fi + image_path=$(truncate_bytes "$image_path" "$MAX_IMAGE_PATH") + + image_file="${image_path##*/}" + if [ ! -f "$ICON_SOURCE_DIR/$image_file" ]; then + echo "error: icon '$image_file' (from '$image_path') not found in $ICON_SOURCE_DIR for app '$APP_NAME'" >&2 + exit 1 + fi + + IMAGE_COUNT=$((IMAGE_COUNT + 1)) + if [ "$IMAGE_COUNT" -gt "$MAX_IMAGES" ]; then + break + fi + + case "$IMAGE_COUNT" in + 1) IMAGE_1="$image_path" ;; + 2) IMAGE_2="$image_path" ;; + 3) IMAGE_3="$image_path" ;; + 4) IMAGE_4="$image_path" ;; + esac +done +IFS="$saved_ifs" + +if [ "$IMAGE_COUNT" -eq 0 ]; then + IMAGE_1="$DEFAULT_ICON_PATH" + IMAGE_COUNT=1 +fi + +app_name_escaped=$(escape_c_string "$app_name_value") +app_desc_escaped=$(escape_c_string "$app_desc") +image_1_escaped=$(escape_c_string "$IMAGE_1") +image_2_escaped=$(escape_c_string "$IMAGE_2") +image_3_escaped=$(escape_c_string "$IMAGE_3") +image_4_escaped=$(escape_c_string "$IMAGE_4") + +cat > "$OUT_PATH" < +#include "elf.h" + +struct __attribute__((packed, aligned(4))) boredos_app_note_blob { + Elf64_Word namesz; + Elf64_Word descsz; + Elf64_Word type; + char name[sizeof(BOREDOS_APP_NOTE_NAME)]; + boredos_app_metadata_t metadata; +}; + +__attribute__((used, section(".note.boredos.app"), aligned(4))) +static const struct boredos_app_note_blob g_boredos_app_note = { + .namesz = sizeof(BOREDOS_APP_NOTE_NAME), + .descsz = sizeof(boredos_app_metadata_t), + .type = BOREDOS_APP_NOTE_TYPE, + .name = BOREDOS_APP_NOTE_NAME, + .metadata = { + .magic = BOREDOS_APP_METADATA_MAGIC, + .version = BOREDOS_APP_METADATA_VERSION, + .image_count = ${IMAGE_COUNT}, + .reserved = 0, + .app_name = "${app_name_escaped}", + .description = "${app_desc_escaped}", + .images = { + "${image_1_escaped}", + "${image_2_escaped}", + "${image_3_escaped}", + "${image_4_escaped}", + }, + }, +}; +EOF