This commit is contained in:
Aviral Garg 2026-07-26 01:08:40 -07:00 committed by GitHub
commit 2cab69aee0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 5434 additions and 236 deletions

View file

@ -0,0 +1,37 @@
# ##############################################################################
# apps/examples/calculator/CMakeLists.txt
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
# license agreements. See the NOTICE file distributed with this work for
# additional information regarding copyright ownership. The ASF licenses this
# file to you under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
# ##############################################################################
if(CONFIG_EXAMPLES_CALCULATOR)
nuttx_add_application(
NAME
calc
PRIORITY
${CONFIG_EXAMPLES_CALCULATOR_PRIORITY}
STACKSIZE
${CONFIG_EXAMPLES_CALCULATOR_STACKSIZE}
MODULE
${CONFIG_EXAMPLES_CALCULATOR}
DEPENDS
lvgl
SRCS
calculator_main.c)
endif()

View file

@ -0,0 +1,35 @@
#
# For a description of the syntax of this configuration file,
# see the file kconfig-language.txt in the NuttX tools repository.
#
menuconfig EXAMPLES_CALCULATOR
tristate "Calculator"
default n
depends on GRAPHICS_LVGL && INPUT_TOUCHSCREEN
---help---
Enable the simple LVGL touchscreen calculator example.
if EXAMPLES_CALCULATOR
config EXAMPLES_CALCULATOR_PRIORITY
int "calculator task priority"
default 100
config EXAMPLES_CALCULATOR_STACKSIZE
int "calculator stack size"
default 16384
config EXAMPLES_CALCULATOR_INPUT_DEVPATH
string "Touchscreen device path"
default "/dev/input0"
---help---
The path to the touchscreen device. Default: "/dev/input0"
config EXAMPLES_CALCULATOR_FBDEVPATH
string "Framebuffer device path"
default "/dev/fb0"
---help---
The path to the framebuffer device. Default: "/dev/fb0"
endif # EXAMPLES_CALCULATOR

View file

@ -0,0 +1,25 @@
############################################################################
# apps/examples/calculator/Make.defs
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
ifneq ($(CONFIG_EXAMPLES_CALCULATOR),)
CONFIGURED_APPS += $(APPDIR)/examples/calculator
endif

View file

@ -0,0 +1,32 @@
############################################################################
# apps/examples/calculator/Makefile
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
include $(APPDIR)/Make.defs
PROGNAME = calc
PRIORITY = $(CONFIG_EXAMPLES_CALCULATOR_PRIORITY)
STACKSIZE = $(CONFIG_EXAMPLES_CALCULATOR_STACKSIZE)
MODULE = $(CONFIG_EXAMPLES_CALCULATOR)
MAINSRC = calculator_main.c
include $(APPDIR)/Application.mk

View file

@ -0,0 +1,518 @@
/****************************************************************************
* apps/examples/calculator/calculator_main.c
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/boardctl.h>
#include <lvgl/lvgl.h>
#include <system/nxstore_chrome.h>
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#undef NEED_BOARDINIT
#if defined(CONFIG_BOARDCTL) && !defined(CONFIG_NSH_ARCHINIT)
# define NEED_BOARDINIT 1
#endif
#define CALC_DISPLAY_MAX 16
/****************************************************************************
* Private Data
****************************************************************************/
/* Set only by quit_signal_handler(), an async-signal-safe flag - the
* actual shutdown happens from the main loop, a known-safe point,
* rather than from the handler itself. See games/NXDoom's
* i_install_quit_signal()/i_poll_quit_signal() for the same pattern:
* an external supervisor (nxstore) has no in-app quit path to drive,
* so it can only ask this process to exit from the outside via
* SIGTERM.
*/
static volatile sig_atomic_t g_quit_requested;
static lv_obj_t *g_display;
static char g_display_buf[CALC_DISPLAY_MAX + 1] = "0";
static double g_accumulator;
static char g_pending_op;
static bool g_start_new_entry = true;
/****************************************************************************
* Private Functions
****************************************************************************/
static void quit_signal_handler(int signo)
{
(void)signo;
g_quit_requested = 1;
}
/* nxstore's own "Close" bar is confined to the top 36px of the display
* so it can coexist with full-screen children like this one - a
* convention that only works if the child app leaves that strip alone.
* This app's display/keypad fill the entire canvas, so nxstore's bar
* gets overdrawn as soon as this screen loads. Give the calculator its
* own close affordance instead: tapping it just sets the same quit flag
* SIGTERM does, so nxstore's nxstore_poll_running_app() sees the process
* exit on its own and returns to the app list automatically.
*/
static void close_event_cb(lv_event_t *e)
{
UNUSED(e);
g_quit_requested = 1;
}
static void calc_update_display(void)
{
lv_label_set_text(g_display, g_display_buf);
}
static void calc_set_display_number(double value)
{
snprintf(g_display_buf, sizeof(g_display_buf), "%.10g", value);
calc_update_display();
}
static double calc_apply(double a, double b, char op, bool *ok)
{
*ok = true;
switch (op)
{
case '+':
return a + b;
case '-':
return a - b;
case '*':
return a * b;
case '/':
if (b == 0)
{
*ok = false;
return 0;
}
return a / b;
default:
return b;
}
}
static void calc_clear(void)
{
g_accumulator = 0;
g_pending_op = 0;
g_start_new_entry = true;
snprintf(g_display_buf, sizeof(g_display_buf), "0");
calc_update_display();
}
static void calc_handle_digit(char digit)
{
size_t len;
if (g_start_new_entry)
{
g_display_buf[0] = '\0';
g_start_new_entry = false;
}
if (strcmp(g_display_buf, "0") == 0)
{
g_display_buf[0] = '\0';
}
len = strlen(g_display_buf);
if (len + 1 >= sizeof(g_display_buf))
{
/* Display is already at capacity - drop the keypress rather than
* overflow g_display_buf or silently truncate mid-number.
*/
return;
}
g_display_buf[len] = digit;
g_display_buf[len + 1] = '\0';
calc_update_display();
}
static void calc_handle_point(void)
{
size_t len;
if (g_start_new_entry)
{
snprintf(g_display_buf, sizeof(g_display_buf), "0");
g_start_new_entry = false;
}
if (strchr(g_display_buf, '.') != NULL)
{
return;
}
len = strlen(g_display_buf);
if (len + 1 >= sizeof(g_display_buf))
{
return;
}
g_display_buf[len] = '.';
g_display_buf[len + 1] = '\0';
calc_update_display();
}
static void calc_handle_op(char op)
{
double current = atof(g_display_buf);
bool ok;
if (g_pending_op != 0)
{
g_accumulator = calc_apply(g_accumulator, current, g_pending_op, &ok);
if (!ok)
{
snprintf(g_display_buf, sizeof(g_display_buf), "Error");
calc_update_display();
g_pending_op = 0;
g_start_new_entry = true;
return;
}
}
else
{
g_accumulator = current;
}
g_pending_op = op;
g_start_new_entry = true;
calc_set_display_number(g_accumulator);
}
static void calc_handle_equals(void)
{
double current = atof(g_display_buf);
bool ok;
if (g_pending_op == 0)
{
g_accumulator = current;
}
else
{
g_accumulator = calc_apply(g_accumulator, current, g_pending_op, &ok);
if (!ok)
{
snprintf(g_display_buf, sizeof(g_display_buf), "Error");
calc_update_display();
g_pending_op = 0;
g_start_new_entry = true;
return;
}
}
g_pending_op = 0;
g_start_new_entry = true;
calc_set_display_number(g_accumulator);
}
static void button_event_cb(lv_event_t *e)
{
FAR const char *code = lv_event_get_user_data(e);
switch (code[0])
{
case 'C':
calc_clear();
break;
case '.':
calc_handle_point();
break;
case '=':
calc_handle_equals();
break;
case '+':
case '-':
case '*':
case '/':
calc_handle_op(code[0]);
break;
default:
calc_handle_digit(code[0]);
break;
}
}
static lv_obj_t *make_button(lv_obj_t *parent, FAR const char *label,
FAR const char *code, uint32_t color)
{
lv_obj_t *btn;
lv_obj_t *btn_label;
btn = lv_obj_create(parent);
lv_obj_set_style_radius(btn, 10, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(color), 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0xffffff),
LV_STATE_PRESSED);
lv_obj_set_style_bg_opa(btn, LV_OPA_30, LV_STATE_PRESSED);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
btn_label = lv_label_create(btn);
lv_label_set_text(btn_label, label);
lv_obj_set_style_text_color(btn_label, lv_color_hex(0xffffff), 0);
lv_obj_center(btn_label);
lv_obj_add_event_cb(btn, button_event_cb, LV_EVENT_CLICKED,
(void *)code);
return btn;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int main(int argc, FAR char *argv[])
{
lv_nuttx_dsc_t info;
lv_nuttx_result_t result;
lv_obj_t *screen;
lv_obj_t *grid;
lv_obj_t *close_btn;
lv_obj_t *close_label;
struct sigaction sa;
int32_t display_height;
int32_t display_width;
static const char *const keys[5][4] =
{
{ "7", "8", "9", "/" },
{ "4", "5", "6", "*" },
{ "1", "2", "3", "-" },
{ "C", "0", ".", "+" },
{ "=", "=", "=", "=" },
};
static const int32_t col_dsc[] =
{
LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_TEMPLATE_LAST
};
static const int32_t row_dsc[] =
{
LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1),
LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST
};
g_quit_requested = 0;
g_display_buf[0] = '0';
g_display_buf[1] = '\0';
g_accumulator = 0;
g_pending_op = 0;
g_start_new_entry = true;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = quit_signal_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGTERM, &sa, NULL) < 0)
{
LV_LOG_ERROR("calculator: failed to install SIGTERM handler");
return 1;
}
if (lv_is_initialized())
{
LV_LOG_ERROR("LVGL already initialized! aborting.");
return -1;
}
#ifdef NEED_BOARDINIT
boardctl(BOARDIOC_INIT, 0);
#endif
lv_init();
lv_nuttx_dsc_init(&info);
info.fb_path = CONFIG_EXAMPLES_CALCULATOR_FBDEVPATH;
info.input_path = CONFIG_EXAMPLES_CALCULATOR_INPUT_DEVPATH;
lv_nuttx_init(&info, &result);
if (result.disp == NULL)
{
LV_LOG_ERROR("calculator: LVGL display init failed!");
lv_nuttx_deinit(&result);
lv_deinit();
return 1;
}
if (result.indev == NULL)
{
LV_LOG_ERROR("calculator: touchscreen init failed!");
lv_nuttx_deinit(&result);
lv_deinit();
return 1;
}
display_width = lv_display_get_horizontal_resolution(result.disp);
display_height = lv_display_get_vertical_resolution(result.disp);
if (display_width <= 0 || display_height <= NXSTORE_BAR_HEIGHT)
{
LV_LOG_ERROR("calculator: display is too small");
lv_nuttx_deinit(&result);
lv_deinit();
return 1;
}
screen = lv_obj_create(NULL);
lv_obj_set_size(screen, display_width,
display_height - NXSTORE_BAR_HEIGHT);
lv_obj_set_pos(screen, 0, NXSTORE_BAR_HEIGHT);
lv_obj_set_style_bg_color(screen, lv_color_hex(0x101827), 0);
lv_obj_set_style_border_width(screen, 0, 0);
lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE);
g_display = lv_label_create(screen);
lv_obj_set_width(g_display, LV_PCT(90));
lv_obj_set_style_text_font(g_display, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(g_display, lv_color_hex(0xffffff), 0);
lv_obj_set_style_text_align(g_display, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_text(g_display, g_display_buf);
lv_obj_align(g_display, LV_ALIGN_TOP_MID, 0, 48);
close_btn = lv_obj_create(screen);
lv_obj_set_size(close_btn, 36, 36);
lv_obj_align(close_btn, LV_ALIGN_TOP_RIGHT, -8, 8);
lv_obj_set_style_radius(close_btn, 8, 0);
lv_obj_set_style_bg_color(close_btn, lv_color_hex(0xf0554c), 0);
lv_obj_set_style_bg_color(close_btn, lv_color_hex(0xb03830),
LV_STATE_PRESSED);
lv_obj_set_style_border_width(close_btn, 0, 0);
lv_obj_clear_flag(close_btn, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(close_btn, LV_OBJ_FLAG_CLICKABLE);
close_label = lv_label_create(close_btn);
lv_label_set_text(close_label, LV_SYMBOL_CLOSE);
lv_obj_set_style_text_color(close_label, lv_color_hex(0xffffff), 0);
lv_obj_clear_flag(close_label, LV_OBJ_FLAG_CLICKABLE);
lv_obj_center(close_label);
lv_obj_add_event_cb(close_btn, close_event_cb, LV_EVENT_CLICKED, NULL);
grid = lv_obj_create(screen);
lv_obj_set_size(grid, LV_PCT(90), LV_PCT(70));
lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -16);
lv_obj_set_style_bg_opa(grid, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(grid, 0, 0);
lv_obj_set_style_pad_all(grid, 4, 0);
lv_obj_set_style_pad_gap(grid, 8, 0);
lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_grid_dsc_array(grid, col_dsc, row_dsc);
lv_obj_set_layout(grid, LV_LAYOUT_GRID);
for (int row = 0; row < 5; row++)
{
for (int col = 0; col < 4; col++)
{
FAR const char *code = keys[row][col];
uint32_t color = 0x2a3648;
lv_obj_t *btn;
if (row == 4)
{
/* Single wide "=" button spans the whole row - only
* instantiate it once, on the first column.
*/
if (col > 0)
{
continue;
}
}
if (strchr("+-*/=", code[0]) != NULL)
{
color = 0x3d8bff;
}
else if (code[0] == 'C')
{
color = 0xf0554c;
}
btn = make_button(grid, code, code, color);
if (row == 4)
{
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 4,
LV_GRID_ALIGN_STRETCH, row, 1);
}
else
{
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, col, 1,
LV_GRID_ALIGN_STRETCH, row, 1);
}
}
}
lv_screen_load(screen);
while (!g_quit_requested)
{
uint32_t idle;
idle = lv_timer_handler();
/* Minimum sleep of 1ms */
idle = idle ? idle : 1;
usleep(idle * 1000);
}
lv_nuttx_deinit(&result);
lv_deinit();
return 0;
}

View file

@ -0,0 +1,37 @@
/****************************************************************************
* apps/include/system/nxstore_chrome.h
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
#ifndef __APPS_INCLUDE_SYSTEM_NXSTORE_CHROME_H
#define __APPS_INCLUDE_SYSTEM_NXSTORE_CHROME_H
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/* Full-screen applications launched by nxstore share the framebuffer with
* its supervisor bar. They must leave this top strip untouched so the
* Close control remains visible and usable.
*/
#define NXSTORE_BAR_HEIGHT 36
#endif /* __APPS_INCLUDE_SYSTEM_NXSTORE_CHROME_H */

View file

@ -38,6 +38,7 @@ if(CONFIG_SYSTEM_NXPKG)
pkg_log.c
pkg_manifest.c
pkg_metadata.c
pkg_repo.c
pkg_store.c
pkg_txn.c)
endif()

View file

@ -29,4 +29,15 @@ config SYSTEM_NXPKG_STACKSIZE
int "'nxpkg' stack size"
default 16384
config SYSTEM_NXPKG_ROOT
string "'nxpkg' storage root"
default "/var/lib/nxpkg"
---help---
Base directory used by nxpkg for its local index, installed
metadata, temporary downloads, and package payload storage.
The default follows the conventional persistent application-data
location. A board must mount persistent storage at /var or set
this to another persistent location, such as /mnt/sdcard/nxpkg,
if packages need to survive a reset.
endif

View file

@ -29,6 +29,7 @@ MODULE = $(CONFIG_SYSTEM_NXPKG)
CSRCS = pkg_compat.c pkg_hash.c pkg_install.c pkg_log.c pkg_manifest.c
CSRCS += pkg_metadata.c pkg_store.c pkg_txn.c
CSRCS += pkg_repo.c
MAINSRC = pkg_main.c
include $(APPDIR)/Application.mk

View file

@ -27,30 +27,93 @@
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define PKG_REPO_DIR "/etc/nxpkg"
#define PKG_REPO_INDEX "/etc/nxpkg/index.json"
#define PKG_REPO_INSTALLED "/var/lib/nxpkg/installed.json"
#define PKG_STORE_DIR "/var/lib/nxpkg/pkgs"
#define PKG_TMP_DIR "/var/cache/nxpkg"
#define PKG_TMP_PKG_DIR "/var/cache/nxpkg/pkg"
#define PKG_ROOT_DIR CONFIG_SYSTEM_NXPKG_ROOT
#define PKG_REPO_DIR PKG_ROOT_DIR
#define PKG_REPO_INDEX PKG_ROOT_DIR "/index.jsn"
#define PKG_REPO_SOURCE PKG_ROOT_DIR "/repo.url"
#define PKG_REPO_INSTALLED PKG_ROOT_DIR "/instpkg.jsn"
#define PKG_STORE_DIR PKG_ROOT_DIR "/pkgs"
#define PKG_TMP_DIR PKG_ROOT_DIR "/tmp"
#define PKG_TMP_PKG_DIR PKG_ROOT_DIR "/tmp/pkg"
#define PKG_NAME_MAX 63
#define PKG_VERSION_MAX 31
#define PKG_ARCH_MAX 31
#define PKG_COMPAT_MAX 63
#define PKG_DESCRIPTION_MAX 127
#define PKG_CATEGORY_MAX 31
#define PKG_HASH_HEX_LEN 64
#define PKG_INDEX_MAX 32
/* Each manifest slot is ~1.7KB (dominated by PKG_LAUNCH_ARGS_MAX slots).
* Keep the catalog bounded so repository-provided metadata cannot cause
* unbounded memory use. Callers should allocate struct pkg_index_s from
* the application heap rather than placing it on a small task stack.
*/
#define PKG_INDEX_MAX 16
#define PKG_INSTALLED_MAX 16
#define PKG_INSTALLED_VERSIONS_MAX 8
#define PKG_LAUNCH_ARGS_MAX 8
#define PKG_LAUNCH_ARG_MAX 127
/* Caps against a malicious/compromised HTTP server: without these, an
* oversized response can exhaust SD-card space (downloads) or force an
* unbounded single heap allocation sized directly off attacker-controlled
* content (pkg_store_read_text). Text/metadata files (index.jsn,
* instpkg.jsn) are always small; artifact downloads cover the largest
* real payloads seen in practice (a multi-MB WAD, a ~1MB game ELF) with
* generous headroom.
*/
#define PKG_TEXT_MAX_SIZE (256 * 1024)
#define PKG_DOWNLOAD_MAX_SIZE (32 * 1024 * 1024)
/* nxpkg is a one-shot CLI, not a daemon, so a lock file older than this
* cannot belong to a still-running install under normal use (even a full
* multi-MB artifact over a slow link finishes well within this window) -
* it can only be left over from a process that was killed or a device
* that lost power mid-install. Reclaiming it is what makes install/
* update/rollback usable again after the crash/power-loss scenarios this
* target is prone to, instead of failing with EBUSY forever.
*/
#define PKG_LOCK_STALE_SECONDS (600)
static inline void *pkg_malloc(size_t size)
{
return malloc(size);
}
static inline void *pkg_zalloc(size_t size)
{
return calloc(1, size);
}
static inline void *pkg_realloc(void *ptr, size_t size)
{
return realloc(ptr, size);
}
static inline void pkg_free(void *ptr)
{
free(ptr);
}
static inline FAR char *pkg_path_alloc(void)
{
return pkg_malloc(PATH_MAX);
}
/****************************************************************************
* Public Types
@ -83,7 +146,12 @@ struct pkg_manifest_s
char compat[PKG_COMPAT_MAX + 1];
char artifact[PATH_MAX];
char sha256[PKG_HASH_HEX_LEN + 1];
char launch_args[PKG_LAUNCH_ARGS_MAX][PKG_LAUNCH_ARG_MAX + 1];
char description[PKG_DESCRIPTION_MAX + 1];
char category[PKG_CATEGORY_MAX + 1];
char icon[PATH_MAX];
enum pkg_payload_type_e type;
size_t launch_argc;
};
struct pkg_index_s
@ -116,6 +184,7 @@ struct pkg_installed_db_s
const char *pkg_manifest_type_str(enum pkg_payload_type_e type);
int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest);
bool pkg_validate_path_component(FAR const char *value);
int pkg_manifest_parse_type(FAR const char *value,
FAR enum pkg_payload_type_e *type);
@ -124,6 +193,7 @@ int pkg_store_ensure_package_root(FAR const char *name);
int pkg_store_ensure_version_dir(FAR const char *name,
FAR const char *version);
int pkg_store_format_index_path(FAR char *buffer, size_t size);
int pkg_store_format_repo_source_path(FAR char *buffer, size_t size);
int pkg_store_format_installed_path(FAR char *buffer, size_t size);
int pkg_store_format_package_root(FAR char *buffer, size_t size,
FAR const char *name);
@ -152,6 +222,8 @@ int pkg_store_read_text(FAR const char *path, FAR char **buffer);
int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text);
int pkg_store_copy_file(FAR const char *src, FAR const char *dest);
int pkg_store_remove_file(FAR const char *path);
int pkg_store_remove_version_dir(FAR const char *name,
FAR const char *version);
const char *pkg_runtime_arch(void);
const char *pkg_runtime_compat(void);
@ -160,7 +232,11 @@ int pkg_compat_check(FAR const struct pkg_manifest_s *manifest);
int pkg_hash_file_sha256(FAR const char *path,
FAR char digest[PKG_HASH_HEX_LEN + 1]);
int pkg_metadata_load_index_path(FAR const char *path,
FAR struct pkg_index_s *index);
int pkg_metadata_load_index(FAR struct pkg_index_s *index);
int pkg_metadata_load_manifest_path(FAR const char *path,
FAR struct pkg_manifest_s *manifest);
FAR const struct pkg_manifest_s *
pkg_metadata_find_latest(FAR const struct pkg_index_s *index,
FAR const char *name);
@ -178,7 +254,20 @@ const char *pkg_txn_state_str(enum pkg_txn_state_e state);
int pkg_txn_write_state(FAR const char *name, enum pkg_txn_state_e state);
int pkg_txn_clear_state(FAR const char *name);
bool pkg_source_is_url(FAR const char *source);
int pkg_resolve_artifact_source(FAR char *buffer, size_t size,
FAR const struct pkg_manifest_s *manifest);
int pkg_resolve_icon_source(FAR char *buffer, size_t size,
FAR const struct pkg_manifest_s *manifest);
int pkg_acquire_source(FAR const char *source, FAR const char *dest,
FAR const char *renew_lock_path);
int pkg_lock_create(FAR const char *path);
void pkg_reclaim_stale_lock(FAR const char *path);
int pkg_sync(FAR const char *source);
int pkg_install(FAR const char *name);
int pkg_uninstall(FAR const char *name);
int pkg_rollback(FAR const char *name);
int pkg_available(FAR FILE *stream);
int pkg_list(FAR FILE *stream);
void pkg_error(FAR const char *fmt, ...);

View file

@ -40,7 +40,16 @@ const char *pkg_runtime_arch(void)
const char *pkg_runtime_compat(void)
{
#ifdef CONFIG_ARCH_BOARD
return CONFIG_ARCH_BOARD;
#elif defined(CONFIG_ARCH_BOARD_CUSTOM_NAME)
if (CONFIG_ARCH_BOARD_CUSTOM_NAME[0] != '\0')
{
return CONFIG_ARCH_BOARD_CUSTOM_NAME;
}
#endif
return "";
}
int pkg_compat_check(FAR const struct pkg_manifest_s *manifest)

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,8 @@
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include "pkg.h"
@ -33,13 +35,19 @@
* Private Functions
****************************************************************************/
static void pkg_vlog(FAR FILE *stream, FAR const char *level,
FAR const char *fmt, va_list ap)
static void pkg_vlog(FAR const char *level, FAR const char *fmt, va_list ap)
{
fprintf(stream, "nxpkg: %s: ", level);
vfprintf(stream, fmt, ap);
fputc('\n', stream);
fflush(stream);
char message[256];
int ret;
ret = vsnprintf(message, sizeof(message), fmt, ap);
if (ret < 0)
{
return;
}
syslog(strcmp(level, "error") == 0 ? LOG_ERR : LOG_INFO,
"nxpkg: %s: %s", level, message);
}
/****************************************************************************
@ -51,7 +59,7 @@ void pkg_error(FAR const char *fmt, ...)
va_list ap;
va_start(ap, fmt);
pkg_vlog(stderr, "error", fmt, ap);
pkg_vlog("error", fmt, ap);
va_end(ap);
}
@ -60,6 +68,6 @@ void pkg_info(FAR const char *fmt, ...)
va_list ap;
va_start(ap, fmt);
pkg_vlog(stdout, "info", fmt, ap);
pkg_vlog("info", fmt, ap);
va_end(ap);
}

View file

@ -29,8 +29,18 @@
#include <stdlib.h>
#include <string.h>
#include <netutils/cJSON.h>
#include "pkg.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define PKG_USAGE \
"Usage: %s <install|update|remove|rollback|list|available|sync|help> " \
"[args]\n"
/****************************************************************************
* Public Functions
****************************************************************************/
@ -38,12 +48,15 @@
int main(int argc, FAR char *argv[])
{
FAR const char *cmd;
cJSON_Hooks hooks;
hooks.malloc_fn = malloc;
hooks.free_fn = free;
cJSON_InitHooks(&hooks);
if (argc < 2)
{
fprintf(stderr,
"Usage: %s <install|update|list|rollback|help> [args]\n",
argv[0]);
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
@ -52,9 +65,7 @@ int main(int argc, FAR char *argv[])
if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 ||
strcmp(cmd, "-h") == 0)
{
fprintf(stdout,
"Usage: %s <install|update|list|rollback|help> [args]\n",
argv[0]);
fprintf(stdout, PKG_USAGE, argv[0]);
return EXIT_SUCCESS;
}
@ -63,19 +74,59 @@ int main(int argc, FAR char *argv[])
if (argc != 3)
{
pkg_error("install expects exactly one package name");
fprintf(stderr,
"Usage: %s <install|update|list|rollback|help> [args]\n",
argv[0]);
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_install(argv[2]);
/* pkg_install() returns a real negative errno on the meaningful
* pipeline failures (nxstore uses that directly), not just
* EXIT_SUCCESS/EXIT_FAILURE - normalize to a plain 0/1 shell exit
* status here.
*/
return pkg_install(argv[2]) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
/* "update" is a package name resolving to whatever version is latest
* in the local index - pkg_install() already handles the "already
* installed at a different version" transition transparently via
* pkg_install_update_installed(), so no separate code path is needed.
*/
if (strcmp(cmd, "update") == 0)
{
pkg_error("'update' is not implemented yet in the current unit");
return EXIT_FAILURE;
if (argc != 3)
{
pkg_error("update expects exactly one package name");
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_install(argv[2]) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
if (strcmp(cmd, "remove") == 0 || strcmp(cmd, "uninstall") == 0)
{
if (argc != 3)
{
pkg_error("remove expects exactly one package name");
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_uninstall(argv[2]);
}
if (strcmp(cmd, "rollback") == 0)
{
if (argc != 3)
{
pkg_error("rollback expects exactly one package name");
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_rollback(argv[2]);
}
if (strcmp(cmd, "list") == 0)
@ -83,24 +134,38 @@ int main(int argc, FAR char *argv[])
if (argc != 2)
{
pkg_error("list does not take additional arguments");
fprintf(stderr,
"Usage: %s <install|update|list|rollback|help> [args]\n",
argv[0]);
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_list(stdout);
}
if (strcmp(cmd, "rollback") == 0)
if (strcmp(cmd, "available") == 0)
{
pkg_error("'rollback' is not implemented yet in the current unit");
return EXIT_FAILURE;
if (argc != 2)
{
pkg_error("available does not take additional arguments");
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_available(stdout);
}
if (strcmp(cmd, "sync") == 0)
{
if (argc != 3)
{
pkg_error("sync expects exactly one index source");
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}
return pkg_sync(argv[2]) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
fprintf(stderr, "ERROR: Unknown subcommand '%s'\n", cmd);
fprintf(stderr,
"Usage: %s <install|update|list|rollback|help> [args]\n",
argv[0]);
fprintf(stderr, PKG_USAGE, argv[0]);
return EXIT_FAILURE;
}

View file

@ -78,6 +78,49 @@ static bool pkg_validate_hex(FAR const char *value)
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: pkg_validate_path_component
*
* Description:
* Reject any value that could escape the intended directory when spliced
* into a filesystem path (pkg_store.c's PKG_STORE_DIR "/%s/%s/..."
* formatters). This is required for "name" and "version" specifically,
* since both come straight from an untrusted, network-fetched
* index.json and are used unsanitized to build install paths - a
* version of "../../evil" would otherwise let a malicious index write
* or delete files outside the package store entirely.
*
****************************************************************************/
bool pkg_validate_path_component(FAR const char *value)
{
FAR const char *p;
if (pkg_validate_required(value) < 0)
{
return false;
}
/* Reject a leading '.' outright: blocks ".", "..", and any
* "../"-prefixed traversal in one check.
*/
if (value[0] == '.')
{
return false;
}
for (p = value; *p != '\0'; p++)
{
if (*p == '/' || *p == '\\')
{
return false;
}
}
return true;
}
const char *pkg_manifest_type_str(enum pkg_payload_type_e type)
{
switch (type)
@ -95,6 +138,8 @@ const char *pkg_manifest_type_str(enum pkg_payload_type_e type)
int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest)
{
size_t i;
if (manifest == NULL)
{
return -EINVAL;
@ -110,6 +155,19 @@ int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest)
return -EINVAL;
}
/* "name" and "version" get spliced unsanitized into on-disk paths
* (pkg_store.c) - they must not contain path separators or traversal
* sequences. "artifact" is validated separately in pkg_repo.c, where
* it's legitimately allowed to be a relative repo path (just not an
* absolute one or one that escapes the repo root).
*/
if (!pkg_validate_path_component(manifest->name) ||
!pkg_validate_path_component(manifest->version))
{
return -EINVAL;
}
if (strlen(manifest->sha256) != PKG_HASH_HEX_LEN)
{
return -EINVAL;
@ -126,6 +184,19 @@ int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest)
return -EINVAL;
}
if (manifest->launch_argc > PKG_LAUNCH_ARGS_MAX)
{
return -EINVAL;
}
for (i = 0; i < manifest->launch_argc; i++)
{
if (pkg_validate_required(manifest->launch_args[i]) < 0)
{
return -EINVAL;
}
}
return 0;
}

View file

@ -24,6 +24,7 @@
* Included Files
****************************************************************************/
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
@ -100,6 +101,54 @@ static FAR cJSON *pkg_metadata_packages_array(FAR cJSON *root)
return cJSON_GetObjectItemCaseSensitive(root, "packages");
}
static int pkg_metadata_parse_launch_args(
FAR cJSON *item, FAR struct pkg_manifest_s *manifest)
{
FAR cJSON *field;
FAR cJSON *arg;
size_t argc = 0;
FAR const char *value;
int ret;
field = cJSON_GetObjectItemCaseSensitive(item, "launch_args");
if (field == NULL)
{
manifest->launch_argc = 0;
return 0;
}
if (!cJSON_IsArray(field))
{
return -EINVAL;
}
cJSON_ArrayForEach(arg, field)
{
if (argc >= PKG_LAUNCH_ARGS_MAX)
{
return -E2BIG;
}
value = cJSON_GetStringValue(arg);
if (value == NULL)
{
return -EINVAL;
}
ret = pkg_copy_string(manifest->launch_args[argc],
sizeof(manifest->launch_args[argc]), value);
if (ret < 0)
{
return ret;
}
argc++;
}
manifest->launch_argc = argc;
return 0;
}
static int pkg_metadata_parse_manifest(FAR cJSON *item,
FAR struct pkg_manifest_s *manifest)
{
@ -170,6 +219,39 @@ static int pkg_metadata_parse_manifest(FAR cJSON *item,
return -EINVAL;
}
/* description/category/icon are optional and purely for UI display;
* missing fields just leave the manifest's copy empty.
*/
field = cJSON_GetObjectItemCaseSensitive(item, "description");
value = cJSON_GetStringValue(field);
if (value != NULL)
{
pkg_copy_string(manifest->description, sizeof(manifest->description),
value);
}
field = cJSON_GetObjectItemCaseSensitive(item, "category");
value = cJSON_GetStringValue(field);
if (value != NULL)
{
pkg_copy_string(manifest->category, sizeof(manifest->category),
value);
}
field = cJSON_GetObjectItemCaseSensitive(item, "icon");
value = cJSON_GetStringValue(field);
if (value != NULL)
{
pkg_copy_string(manifest->icon, sizeof(manifest->icon), value);
}
ret = pkg_metadata_parse_launch_args(item, manifest);
if (ret < 0)
{
return ret;
}
return pkg_manifest_validate(manifest);
}
@ -221,6 +303,9 @@ static int pkg_metadata_parse_installed_entry(
{
FAR cJSON *field;
FAR const char *value;
bool current_found = false;
bool previous_found = false;
size_t i;
int ret;
memset(entry, 0, sizeof(*entry));
@ -285,38 +370,82 @@ static int pkg_metadata_parse_installed_entry(
return ret;
}
if (!pkg_validate_path_component(entry->name) ||
!pkg_validate_path_component(entry->current) ||
(entry->previous[0] != '\0' &&
!pkg_validate_path_component(entry->previous)))
{
return -EINVAL;
}
for (i = 0; i < entry->version_count; i++)
{
if (!pkg_validate_path_component(entry->versions[i]))
{
return -EINVAL;
}
current_found |= strcmp(entry->versions[i], entry->current) == 0;
previous_found |= strcmp(entry->versions[i], entry->previous) == 0;
}
if (!current_found ||
(entry->previous[0] != '\0' && !previous_found))
{
return -EINVAL;
}
return 0;
}
static int pkg_metadata_version_token_cmp(FAR const char *lhs,
FAR const char *rhs)
{
long leftnum;
long rightnum;
FAR char *leftend;
FAR char *rightend;
FAR const char *cmpleft;
FAR const char *cmpright;
FAR const char *leftdigits;
FAR const char *rightdigits;
size_t leftlen;
size_t rightlen;
int ret;
leftnum = strtol(lhs, &leftend, 10);
rightnum = strtol(rhs, &rightend, 10);
if (leftend != lhs && rightend != rhs)
leftdigits = lhs;
rightdigits = rhs;
while (isdigit((unsigned char)*leftdigits))
{
if (leftnum < rightnum)
leftdigits++;
}
while (isdigit((unsigned char)*rightdigits))
{
rightdigits++;
}
if (leftdigits != lhs && rightdigits != rhs)
{
while (*lhs == '0' && lhs + 1 < leftdigits)
{
lhs++;
}
while (*rhs == '0' && rhs + 1 < rightdigits)
{
rhs++;
}
leftlen = (size_t)(leftdigits - lhs);
rightlen = (size_t)(rightdigits - rhs);
if (leftlen < rightlen)
{
return -1;
}
if (leftnum > rightnum)
if (leftlen > rightlen)
{
return 1;
}
}
else
{
int ret;
ret = pkg_string_cmp(lhs, PKG_VERSION_MAX + 1,
rhs, PKG_VERSION_MAX + 1);
ret = memcmp(lhs, rhs, leftlen);
if (ret < 0)
{
return -1;
@ -326,6 +455,26 @@ static int pkg_metadata_version_token_cmp(FAR const char *lhs,
{
return 1;
}
cmpleft = leftdigits;
cmpright = rightdigits;
}
else
{
cmpleft = lhs;
cmpright = rhs;
}
ret = pkg_string_cmp(cmpleft, PKG_VERSION_MAX + 1,
cmpright, PKG_VERSION_MAX + 1);
if (ret < 0)
{
return -1;
}
if (ret > 0)
{
return 1;
}
return 0;
@ -395,6 +544,8 @@ static FAR cJSON *pkg_metadata_manifest_to_json(
FAR const struct pkg_manifest_s *manifest)
{
FAR cJSON *root;
FAR cJSON *launch_args;
size_t i;
root = cJSON_CreateObject();
if (root == NULL)
@ -410,51 +561,55 @@ static FAR cJSON *pkg_metadata_manifest_to_json(
cJSON_AddStringToObject(root, "sha256", manifest->sha256);
cJSON_AddStringToObject(root, "type",
pkg_manifest_type_str(manifest->type));
if (manifest->description[0] != '\0')
{
cJSON_AddStringToObject(root, "description", manifest->description);
}
if (manifest->category[0] != '\0')
{
cJSON_AddStringToObject(root, "category", manifest->category);
}
if (manifest->launch_argc > 0)
{
launch_args = cJSON_AddArrayToObject(root, "launch_args");
if (launch_args == NULL)
{
cJSON_Delete(root);
return NULL;
}
for (i = 0; i < manifest->launch_argc; i++)
{
FAR cJSON *arg;
arg = cJSON_CreateString(manifest->launch_args[i]);
if (arg == NULL)
{
cJSON_Delete(root);
return NULL;
}
cJSON_AddItemToArray(launch_args, arg);
}
}
return root;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int pkg_metadata_load_index(FAR struct pkg_index_s *index)
static int pkg_metadata_parse_index_text(FAR const char *text,
FAR struct pkg_index_s *index)
{
FAR cJSON *root;
FAR cJSON *packages;
FAR cJSON *item;
FAR char *text;
char path[PATH_MAX];
size_t count = 0;
size_t textlen;
int ret;
if (index == NULL)
{
return -EINVAL;
}
memset(index, 0, sizeof(*index));
ret = pkg_store_format_index_path(path, sizeof(path));
if (ret < 0)
{
return ret;
}
pkg_info("loading index from %s", path);
ret = pkg_store_read_text(path, &text);
if (ret < 0)
{
return ret;
}
textlen = strlen(text);
pkg_info("index read complete (%zu bytes)", textlen);
root = cJSON_Parse(text);
pkg_info("cJSON_Parse returned %s", root != NULL ? "success" : "failure");
free(text);
if (root == NULL)
{
return -EINVAL;
@ -471,15 +626,27 @@ int pkg_metadata_load_index(FAR struct pkg_index_s *index)
{
if (count >= PKG_INDEX_MAX)
{
cJSON_Delete(root);
return -E2BIG;
/* Keep what's already parsed rather than discarding the whole
* index: a catalog that's grown past PKG_INDEX_MAX shouldn't
* make every other package unavailable too.
*/
pkg_error("index has more than %d packages, truncating",
PKG_INDEX_MAX);
break;
}
ret = pkg_metadata_parse_manifest(item, &index->manifests[count]);
if (ret < 0)
{
cJSON_Delete(root);
return ret;
/* Skip a malformed entry instead of discarding the entire
* index: one bad/malicious package definition shouldn't make
* every other, otherwise-valid package unavailable too.
*/
pkg_error("skipping malformed package entry %zu: %d", count,
ret);
continue;
}
pkg_info("parsed manifest %s %s",
@ -493,6 +660,84 @@ int pkg_metadata_load_index(FAR struct pkg_index_s *index)
return 0;
}
/****************************************************************************
* Public Functions
****************************************************************************/
int pkg_metadata_load_index_path(FAR const char *path,
FAR struct pkg_index_s *index)
{
FAR char *text;
size_t textlen;
int ret;
if (path == NULL || index == NULL)
{
return -EINVAL;
}
memset(index, 0, sizeof(*index));
pkg_info("loading index from %s", path);
ret = pkg_store_read_text(path, &text);
if (ret < 0)
{
return ret;
}
textlen = strlen(text);
pkg_info("index read complete (%zu bytes)", textlen);
ret = pkg_metadata_parse_index_text(text, index);
pkg_free(text);
return ret;
}
int pkg_metadata_load_index(FAR struct pkg_index_s *index)
{
char path[PATH_MAX];
int ret;
ret = pkg_store_format_index_path(path, sizeof(path));
if (ret < 0)
{
return ret;
}
return pkg_metadata_load_index_path(path, index);
}
int pkg_metadata_load_manifest_path(FAR const char *path,
FAR struct pkg_manifest_s *manifest)
{
FAR cJSON *root;
FAR char *text;
int ret;
if (path == NULL || manifest == NULL)
{
return -EINVAL;
}
ret = pkg_store_read_text(path, &text);
if (ret < 0)
{
return ret;
}
root = cJSON_Parse(text);
pkg_free(text);
if (root == NULL)
{
return -EINVAL;
}
ret = pkg_metadata_parse_manifest(root, manifest);
cJSON_Delete(root);
return ret;
}
FAR const struct pkg_manifest_s *
pkg_metadata_find_latest(FAR const struct pkg_index_s *index,
FAR const char *name)
@ -573,7 +818,7 @@ int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db)
}
root = cJSON_Parse(text);
free(text);
pkg_free(text);
if (root == NULL)
{
return -EINVAL;
@ -590,15 +835,23 @@ int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db)
{
if (count >= PKG_INSTALLED_MAX)
{
cJSON_Delete(root);
return -E2BIG;
pkg_error("installed db has more than %d entries, truncating",
PKG_INSTALLED_MAX);
break;
}
ret = pkg_metadata_parse_installed_entry(item, &db->entries[count]);
if (ret < 0)
{
cJSON_Delete(root);
return ret;
/* A single corrupted entry (plausible after a crash mid-write,
* despite the atomic-write mechanism) must not make every
* other installed package look uninstalled - that would drive
* needless reinstalls for everything else.
*/
pkg_error("skipping malformed installed entry %zu: %d", count,
ret);
continue;
}
count++;

774
system/nxpkg/pkg_repo.c Normal file
View file

@ -0,0 +1,774 @@
/****************************************************************************
* apps/system/nxpkg/pkg_repo.c
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <utime.h>
#include <sys/stat.h>
#include <nuttx/config.h>
#include <netutils/cJSON.h>
#ifdef CONFIG_NETUTILS_WEBCLIENT
# include "netutils/webclient.h"
#endif
#include "pkg.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/* Each webclient_perform() read/sink cycle costs a TCP receive plus an
* SD-card write call; at the old 512-byte size, a sub-1MB file like
* nxdoom's ~940KB ELF took ~1900 round trips and, in practice, close
* to two minutes to install - easily read as "stuck" with only a bare
* spinner for feedback. 4KB cuts that to ~230 round trips and lines
* up with typical SD card erase-block granularity, which also reduces
* write amplification. Still small enough to be a safe stack-local
* buffer against the 16KB (CLI) / 16KB (nxstore install worker) task
* stacks that call into this.
*/
#define PKG_REPO_FETCH_BUFFER_SIZE 4096
#define PKG_REPO_HTTP "http://"
#define PKG_REPO_HTTPS "https://"
#define PKG_REPO_SOURCE_KEY "_nxpkg_source"
/****************************************************************************
* Private Types
****************************************************************************/
#ifdef CONFIG_NETUTILS_WEBCLIENT
struct pkg_fetch_context_s
{
int fd;
size_t total;
/* Optional path to a lock file whose mtime should be refreshed as data
* arrives - see pkg_repo_sink()'s comment on why a lock acquired once
* up front isn't enough for a download that can run past the stale-
* lock timeout on its own. NULL if there's nothing to renew (e.g. a
* plain local-file copy, which is fast enough not to need it).
*/
FAR const char *renew_lock_path;
};
#endif
/****************************************************************************
* Private Functions
****************************************************************************/
static int pkg_repo_copy_string(FAR char *buffer, size_t size,
FAR const char *value)
{
int ret;
ret = snprintf(buffer, size, "%s", value);
if (ret < 0)
{
return ret;
}
return (size_t)ret >= size ? -ENAMETOOLONG : 0;
}
static int pkg_repo_source_base(FAR char *buffer, size_t size,
FAR const char *source)
{
FAR const char *slash;
size_t length;
slash = strrchr(source, '/');
if (slash == NULL)
{
return pkg_repo_copy_string(buffer, size, ".");
}
length = (size_t)(slash - source);
if (length == 0)
{
length = 1;
}
if (length >= size)
{
return -ENAMETOOLONG;
}
memcpy(buffer, source, length);
buffer[length] = '\0';
return 0;
}
/****************************************************************************
* Name: pkg_validate_artifact_relative
*
* Description:
* manifest->artifact is repo-relative content and gets spliced into a
* local filesystem path (or used to build a URL) unsanitized. Reject
* absolute paths outright - allowing them let a malicious index turn
* any local file with a known/predictable hash into an "installed"
* package, including making it executable - and reject any ".." path
* segment that would let the artifact escape the repo mirror directory.
*
****************************************************************************/
static bool pkg_validate_artifact_relative(FAR const char *value)
{
FAR const char *p;
if (value == NULL || value[0] == '\0' || value[0] == '/')
{
return false;
}
p = value;
while ((p = strstr(p, "..")) != NULL)
{
bool at_start = p == value || *(p - 1) == '/';
bool at_end = p[2] == '\0' || p[2] == '/';
if (at_start && at_end)
{
return false;
}
p++;
}
return true;
}
static int pkg_repo_read_source(FAR char *buffer, size_t size)
{
FAR cJSON *root;
FAR cJSON *source;
FAR char *text = NULL;
char path[PATH_MAX];
size_t length;
int ret;
ret = pkg_store_format_index_path(path, sizeof(path));
if (ret < 0)
{
return ret;
}
ret = pkg_store_read_text(path, &text);
if (ret < 0)
{
return ret;
}
root = cJSON_Parse(text);
pkg_free(text);
if (root == NULL)
{
return -EINVAL;
}
source = cJSON_GetObjectItemCaseSensitive(root, PKG_REPO_SOURCE_KEY);
if (!cJSON_IsString(source) || source->valuestring == NULL)
{
cJSON_Delete(root);
/* Read the sidecar written by older nxpkg versions once, so an
* existing cached catalog remains usable until the next sync.
*/
ret = pkg_store_format_repo_source_path(path, sizeof(path));
if (ret < 0)
{
return ret;
}
ret = pkg_store_read_text(path, &text);
if (ret < 0)
{
return ret;
}
length = strlen(text);
while (length > 0 && isspace((unsigned char)text[length - 1]))
{
text[--length] = '\0';
}
ret = pkg_repo_copy_string(buffer, size, text);
pkg_free(text);
return ret;
}
ret = pkg_repo_copy_string(buffer, size, source->valuestring);
cJSON_Delete(root);
return ret;
}
static int pkg_repo_attach_source(FAR char **text,
FAR const char *source_value)
{
FAR cJSON *root;
FAR cJSON *wrapper;
FAR char *updated;
root = cJSON_Parse(*text);
if (root == NULL)
{
return -EINVAL;
}
if (cJSON_IsArray(root))
{
wrapper = cJSON_CreateObject();
if (wrapper == NULL)
{
cJSON_Delete(root);
return -ENOMEM;
}
cJSON_AddItemToObject(wrapper, "packages", root);
if (cJSON_GetObjectItemCaseSensitive(wrapper, "packages") != root)
{
cJSON_Delete(root);
cJSON_Delete(wrapper);
return -ENOMEM;
}
root = wrapper;
}
else if (!cJSON_IsObject(root))
{
cJSON_Delete(root);
return -EINVAL;
}
while (cJSON_GetObjectItemCaseSensitive(root,
PKG_REPO_SOURCE_KEY) != NULL)
{
cJSON_DeleteItemFromObjectCaseSensitive(root, PKG_REPO_SOURCE_KEY);
}
if (cJSON_AddStringToObject(root, PKG_REPO_SOURCE_KEY,
source_value) == NULL)
{
cJSON_Delete(root);
return -ENOMEM;
}
updated = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (updated == NULL)
{
return -ENOMEM;
}
pkg_free(*text);
*text = updated;
return 0;
}
#ifdef CONFIG_NETUTILS_WEBCLIENT
static int pkg_repo_sink(FAR char **buffer, int offset, int datend,
FAR int *buflen, FAR void *arg)
{
FAR struct pkg_fetch_context_s *ctx;
size_t remaining;
FAR char *cursor;
UNUSED(buffer);
UNUSED(buflen);
ctx = arg;
cursor = &(*buffer)[offset];
remaining = (size_t)(datend - offset);
/* Cap total downloaded bytes: an unbounded/malicious response could
* otherwise exhaust all SD-card space. Checked before writing more so
* the on-disk file never exceeds the cap even mid-chunk.
*/
if (remaining > 0 &&
(ctx->total > PKG_DOWNLOAD_MAX_SIZE ||
remaining > PKG_DOWNLOAD_MAX_SIZE - ctx->total))
{
return -EFBIG;
}
ctx->total += remaining;
while (remaining > 0)
{
ssize_t nwritten;
nwritten = write(ctx->fd, cursor, remaining);
if (nwritten < 0)
{
if (errno == EINTR)
{
continue;
}
return -errno;
}
if (nwritten == 0)
{
return -EIO;
}
cursor += nwritten;
remaining -= (size_t)nwritten;
}
/* The lock this download is running under was only ever stamped once,
* at acquire time - PKG_LOCK_STALE_SECONDS then measures from that
* single timestamp regardless of how long the download actually
* takes, so a large-enough file over a slow-enough link can still be
* genuinely mid-transfer when another install for the same package
* decides the lock looks stale and reclaims it out from under this
* one. Touching it here means its age reflects time since the last
* byte actually arrived instead of total operation time - best-
* effort: a failed touch just means this one chunk didn't renew it,
* not that the download itself should fail.
*/
if (ctx->renew_lock_path != NULL)
{
utime(ctx->renew_lock_path, NULL);
}
return 0;
}
static int pkg_repo_fetch_url(FAR const char *url, FAR const char *dest,
FAR const char *renew_lock_path)
{
struct pkg_fetch_context_s fetch;
struct webclient_context client;
char reason[64];
char buffer[PKG_REPO_FETCH_BUFFER_SIZE];
int ret;
fetch.fd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fetch.fd < 0)
{
return -errno;
}
fetch.total = 0;
fetch.renew_lock_path = renew_lock_path;
webclient_set_defaults(&client);
client.method = "GET";
client.url = url;
client.buffer = buffer;
client.buflen = sizeof(buffer);
client.sink_callback = pkg_repo_sink;
client.sink_callback_arg = &fetch;
client.http_reason = reason;
client.http_reason_len = sizeof(reason);
ret = webclient_perform(&client);
if (ret < 0)
{
close(fetch.fd);
unlink(dest);
return ret;
}
if (client.http_status / 100 != 2)
{
close(fetch.fd);
unlink(dest);
return -EPROTO;
}
ret = close(fetch.fd);
if (ret < 0)
{
ret = -errno;
unlink(dest);
return ret;
}
return 0;
}
#endif
static int pkg_resolve_relative_source(FAR char *buffer, size_t size,
FAR const char *relative)
{
char source[PATH_MAX];
char base[PATH_MAX];
int ret;
if (buffer == NULL || relative == NULL || relative[0] == '\0')
{
return -EINVAL;
}
if (pkg_source_is_url(relative))
{
return pkg_repo_copy_string(buffer, size, relative);
}
if (!pkg_validate_artifact_relative(relative))
{
return -EINVAL;
}
ret = pkg_repo_read_source(source, sizeof(source));
if (ret >= 0)
{
ret = pkg_repo_source_base(base, sizeof(base), source);
if (ret < 0)
{
return ret;
}
ret = snprintf(buffer, size, "%s/%s", base, relative);
if (ret < 0)
{
return ret;
}
return (size_t)ret >= size ? -ENAMETOOLONG : 0;
}
ret = snprintf(buffer, size, "%s/%s", PKG_REPO_DIR, relative);
if (ret < 0)
{
return ret;
}
return (size_t)ret >= size ? -ENAMETOOLONG : 0;
}
/****************************************************************************
* Public Functions
****************************************************************************/
bool pkg_source_is_url(FAR const char *source)
{
if (source == NULL)
{
return false;
}
return strncasecmp(source, PKG_REPO_HTTP, strlen(PKG_REPO_HTTP)) == 0 ||
strncasecmp(source, PKG_REPO_HTTPS, strlen(PKG_REPO_HTTPS)) == 0;
}
int pkg_resolve_artifact_source(FAR char *buffer, size_t size,
FAR const struct pkg_manifest_s *manifest)
{
if (manifest == NULL)
{
return -EINVAL;
}
return pkg_resolve_relative_source(buffer, size, manifest->artifact);
}
int pkg_resolve_icon_source(FAR char *buffer, size_t size,
FAR const struct pkg_manifest_s *manifest)
{
if (manifest == NULL)
{
return -EINVAL;
}
return pkg_resolve_relative_source(buffer, size, manifest->icon);
}
int pkg_acquire_source(FAR const char *source, FAR const char *dest,
FAR const char *renew_lock_path)
{
if (source == NULL || dest == NULL)
{
return -EINVAL;
}
if (pkg_source_is_url(source))
{
#ifdef CONFIG_NETUTILS_WEBCLIENT
return pkg_repo_fetch_url(source, dest, renew_lock_path);
#else
return -ENOSYS;
#endif
}
return pkg_store_copy_file(source, dest);
}
/****************************************************************************
* Name: pkg_repo_acquire_sync_lock
*
* Description:
* Serialize catalog synchronization so concurrent downloads cannot
* commit out of order.
*
****************************************************************************/
static int pkg_repo_acquire_sync_lock(FAR char *path, size_t size)
{
int ret;
int tries;
ret = snprintf(path, size, PKG_ROOT_DIR "/sync.lk");
if (ret < 0)
{
return ret;
}
if ((size_t)ret >= size)
{
return -ENAMETOOLONG;
}
for (tries = 0; tries < 100; tries++)
{
ret = pkg_lock_create(path);
if (ret == 0)
{
return 0;
}
if (ret != -EEXIST)
{
return ret;
}
pkg_reclaim_stale_lock(path);
usleep(20 * 1000);
}
return -EBUSY;
}
int pkg_sync(FAR const char *source)
{
FAR struct pkg_index_s *index = NULL;
FAR char *text = NULL;
FAR char *tmp = NULL;
FAR char *index_path = NULL;
FAR char *lock;
bool remove_tmp = false;
int ret;
if (source == NULL || source[0] == '\0')
{
pkg_error("sync requires a non-empty index source");
return -EINVAL;
}
lock = pkg_path_alloc();
if (lock == NULL)
{
pkg_error("unable to allocate sync lock path buffer");
return -ENOMEM;
}
ret = pkg_repo_acquire_sync_lock(lock, PATH_MAX);
if (ret < 0)
{
pkg_error("unable to acquire sync lock: %d", ret);
pkg_free(lock);
return ret;
}
index = pkg_zalloc(sizeof(*index));
tmp = pkg_path_alloc();
index_path = pkg_path_alloc();
if (index == NULL || tmp == NULL || index_path == NULL)
{
pkg_error("unable to allocate index metadata buffer");
ret = -ENOMEM;
goto out;
}
ret = pkg_store_prepare_layout();
if (ret < 0)
{
pkg_error("unable to prepare package layout: %d", ret);
goto out;
}
/* Each CLI invocation has its own PID. Use it in the staging name so a
* manual sync cannot truncate the file being validated by nxstore (or a
* second shell). Keep the leaf short for short-name-only FAT mounts.
*/
ret = snprintf(tmp, PATH_MAX, "%s/s%u.jsn", PKG_TMP_DIR,
(unsigned int)getpid());
if (ret < 0 || (size_t)ret >= PATH_MAX)
{
pkg_error("temporary sync path is too long");
ret = -ENAMETOOLONG;
goto out;
}
ret = pkg_acquire_source(source, tmp, lock);
if (ret < 0)
{
pkg_error("unable to fetch index source '%s': %d", source, ret);
goto out;
}
remove_tmp = true;
ret = pkg_metadata_load_index_path(tmp, index);
if (ret < 0)
{
pkg_error("downloaded index is invalid: %d", ret);
goto out;
}
ret = pkg_store_read_text(tmp, &text);
if (ret < 0)
{
pkg_error("unable to read fetched index: %d", ret);
goto out;
}
/* Commit the catalog and its source in one atomic file replacement. */
ret = pkg_repo_attach_source(&text, source);
if (ret < 0)
{
pkg_error("unable to record repository source: %d", ret);
goto out;
}
ret = pkg_store_format_index_path(index_path, PATH_MAX);
if (ret < 0)
{
pkg_error("unable to resolve local index path: %d", ret);
goto out;
}
ret = pkg_store_write_text_atomic(index_path, text);
if (ret < 0)
{
pkg_error("unable to write local index: %d", ret);
goto out;
}
pkg_info("synced package index from %s", source);
ret = 0;
out:
if (remove_tmp)
{
pkg_store_remove_file(tmp);
}
pkg_free(text);
pkg_free(index);
pkg_free(tmp);
pkg_free(index_path);
unlink(lock);
pkg_free(lock);
return ret;
}
int pkg_available(FAR FILE *stream)
{
FAR struct pkg_index_s *index;
FAR const char *arch;
FAR const char *compat;
size_t i;
int ret;
if (stream == NULL)
{
return EXIT_FAILURE;
}
index = pkg_zalloc(sizeof(*index));
if (index == NULL)
{
pkg_error("unable to allocate index metadata buffer");
return EXIT_FAILURE;
}
ret = pkg_store_prepare_layout();
if (ret < 0)
{
pkg_free(index);
pkg_error("unable to prepare package layout: %d", ret);
return EXIT_FAILURE;
}
ret = pkg_metadata_load_index(index);
if (ret < 0)
{
pkg_free(index);
pkg_error("unable to load package index: %d", ret);
return EXIT_FAILURE;
}
arch = pkg_runtime_arch();
compat = pkg_runtime_compat();
for (i = 0; i < index->count; i++)
{
FAR const struct pkg_manifest_s *manifest = &index->manifests[i];
FAR const struct pkg_manifest_s *latest;
if (strcmp(manifest->arch, arch) != 0 ||
strcmp(manifest->compat, compat) != 0)
{
continue;
}
latest = pkg_metadata_find_latest(index, manifest->name);
if (latest != manifest)
{
continue;
}
fprintf(stream,
"%s version=%s type=%s arch=%s compat=%s artifact=%s\n",
manifest->name,
manifest->version,
pkg_manifest_type_str(manifest->type),
manifest->arch,
manifest->compat,
manifest->artifact);
}
pkg_free(index);
return EXIT_SUCCESS;
}

View file

@ -24,8 +24,13 @@
* Included Files
****************************************************************************/
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
@ -35,10 +40,84 @@
#include "pkg.h"
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
#define PKG_LOCK_RECORD_MAGIC "NXPKG1"
#define PKG_LOCK_RECORD_SIZE 64
/****************************************************************************
* Private Data
****************************************************************************/
static pthread_once_t g_pkg_lock_boot_once = PTHREAD_ONCE_INIT;
static uint64_t g_pkg_lock_boot_id;
/****************************************************************************
* Private Functions
****************************************************************************/
static void pkg_lock_init_boot_id(void)
{
arc4random_buf(&g_pkg_lock_boot_id, sizeof(g_pkg_lock_boot_id));
if (g_pkg_lock_boot_id == 0)
{
g_pkg_lock_boot_id = 1;
}
}
static uint64_t pkg_lock_get_boot_id(void)
{
if (pthread_once(&g_pkg_lock_boot_once, pkg_lock_init_boot_id) != 0)
{
return 1;
}
return g_pkg_lock_boot_id;
}
static int pkg_lock_read_owner(FAR const char *path,
FAR uint64_t *boot_id,
FAR pid_t *owner)
{
char record[PKG_LOCK_RECORD_SIZE];
unsigned long long parsed_boot;
long parsed_owner;
ssize_t nread;
int fd;
int ret;
fd = open(path, O_RDONLY);
if (fd < 0)
{
return -errno;
}
nread = read(fd, record, sizeof(record) - 1);
if (nread < 0)
{
ret = -errno;
close(fd);
return ret;
}
close(fd);
record[nread] = '\0';
ret = sscanf(record, PKG_LOCK_RECORD_MAGIC " %llx %ld",
&parsed_boot, &parsed_owner);
if (ret != 2 || parsed_owner <= 0 ||
(long)(pid_t)parsed_owner != parsed_owner)
{
return -EINVAL;
}
*boot_id = (uint64_t)parsed_boot;
*owner = (pid_t)parsed_owner;
return 0;
}
static int pkg_store_format(FAR char *buffer, size_t size,
FAR const char *fmt,
FAR const char *name,
@ -145,6 +224,11 @@ static int pkg_store_write_all(int fd, FAR const char *buffer, size_t length)
return -errno;
}
if (ret == 0)
{
return -EIO;
}
offset += (size_t)ret;
}
@ -188,6 +272,119 @@ int pkg_store_prepare_layout(void)
return pkg_store_mkdirs(PKG_TMP_PKG_DIR);
}
int pkg_lock_create(FAR const char *path)
{
char record[PKG_LOCK_RECORD_SIZE];
int fd;
int ret;
fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd < 0)
{
return -errno;
}
ret = snprintf(record, sizeof(record), PKG_LOCK_RECORD_MAGIC
" %016" PRIx64 " %ld\n",
pkg_lock_get_boot_id(), (long)getpid());
if (ret < 0 || (size_t)ret >= sizeof(record))
{
ret = ret < 0 ? ret : -ENAMETOOLONG;
goto errout;
}
ret = pkg_store_write_all(fd, record, (size_t)ret);
if (ret < 0)
{
goto errout;
}
if (fsync(fd) < 0)
{
ret = -errno;
goto errout;
}
if (close(fd) < 0)
{
ret = -errno;
unlink(path);
return ret;
}
return 0;
errout:
close(fd);
unlink(path);
return ret;
}
void pkg_reclaim_stale_lock(FAR const char *path)
{
struct stat st;
uint64_t boot_id;
pid_t owner;
time_t now;
int ret;
ret = pkg_lock_read_owner(path, &boot_id, &owner);
if (ret == -EINVAL)
{
/* A creator may have completed open(O_EXCL) but not its first write.
* Give that very small window time to close before treating the file
* as a legacy timestamp-only lock.
*/
usleep(20 * 1000);
ret = pkg_lock_read_owner(path, &boot_id, &owner);
}
if (ret == 0)
{
if (boot_id != pkg_lock_get_boot_id())
{
pkg_error("reclaiming lock from an earlier boot '%s'", path);
unlink(path);
return;
}
if (kill(owner, 0) == 0 || errno == EPERM)
{
return;
}
if (errno == ESRCH)
{
pkg_error("reclaiming lock from exited task %ld '%s'",
(long)owner, path);
unlink(path);
}
return;
}
/* Compatibility for empty lock files created by older nxpkg images.
* Their only ownership information is the filesystem timestamp.
*/
if (stat(path, &st) < 0)
{
return;
}
now = time(NULL);
if (now < st.st_mtime ||
(now - st.st_mtime) < PKG_LOCK_STALE_SECONDS)
{
return;
}
pkg_error("reclaiming legacy stale lock '%s' (age %ld s)",
path, (long)(now - st.st_mtime));
unlink(path);
}
int pkg_store_ensure_package_root(FAR const char *name)
{
char path[PATH_MAX];
@ -228,6 +425,11 @@ int pkg_store_format_index_path(FAR char *buffer, size_t size)
return pkg_store_format(buffer, size, "%s", PKG_REPO_INDEX, "");
}
int pkg_store_format_repo_source_path(FAR char *buffer, size_t size)
{
return pkg_store_format(buffer, size, "%s", PKG_REPO_SOURCE, "");
}
int pkg_store_format_installed_path(FAR char *buffer, size_t size)
{
return pkg_store_format(buffer, size, "%s", PKG_REPO_INSTALLED, "");
@ -266,21 +468,52 @@ int pkg_store_format_previous_path(FAR char *buffer, size_t size,
int pkg_store_format_txn_path(FAR char *buffer, size_t size,
FAR const char *name)
{
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/.txn", name, "");
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/txn.tx", name,
"");
}
int pkg_store_format_lock_path(FAR char *buffer, size_t size,
FAR const char *name)
{
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/.lock", name, "");
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/lock.lk", name,
"");
}
int pkg_store_format_download_path(FAR char *buffer, size_t size,
FAR const char *name,
FAR const char *version)
{
return pkg_store_format(buffer, size, PKG_TMP_PKG_DIR "/%s-%s.npkg", name,
version);
int ret;
UNUSED(name);
UNUSED(version);
/* This used to be "PKG_TMP_PKG_DIR/name-version.pkg", which breaks on
* this SD card's short-name-only FAT mount as soon as name+version
* exceeds the 8.3 8-character base-name limit - e.g. "nxdoom-1" (8
* chars) fits and installs fine, but "nxdoom-10" or "nxdoom-9.1" (9+
* chars) fails the open(O_CREAT) in pkg_repo_fetch_url() with
* -EINVAL, surfacing as "acquire source failed: -22" for any
* multi-character version - independent of name/version length here,
* unlike pkg_store_make_tmp_path()'s already-FAT-safe scheme. The
* pid is small, bounded, and unique per concurrently running install
* (each `nxpkg install` is its own process with its own per-name
* lock), so it can't collide the way a single fixed name would if
* two different packages were being installed at once.
*/
ret = snprintf(buffer, size, PKG_TMP_PKG_DIR "/dl%d.pkg", (int)getpid());
if (ret < 0)
{
return ret;
}
if ((size_t)ret >= size)
{
return -ENAMETOOLONG;
}
return 0;
}
int pkg_store_format_payload_path(FAR char *buffer, size_t size,
@ -311,16 +544,18 @@ int pkg_store_format_manifest_path(FAR char *buffer, size_t size,
FAR const char *name,
FAR const char *version)
{
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/%s/manifest.json",
return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/%s/manifest.jsn",
name, version);
}
int pkg_store_read_text(FAR const char *path, FAR char **buffer)
{
FAR FILE *stream;
FAR char *data;
long length;
struct stat st;
size_t length;
size_t nread;
size_t total;
int fd;
if (buffer == NULL)
{
@ -329,72 +564,171 @@ int pkg_store_read_text(FAR const char *path, FAR char **buffer)
*buffer = NULL;
stream = fopen(path, "rb");
if (stream == NULL)
fd = open(path, O_RDONLY);
if (fd < 0)
{
return errno == ENOENT ? -ENOENT : -errno;
}
if (fseek(stream, 0, SEEK_END) < 0)
if (fstat(fd, &st) < 0)
{
fclose(stream);
close(fd);
return -errno;
}
length = ftell(stream);
if (length < 0)
if (!S_ISREG(st.st_mode))
{
fclose(stream);
return -errno;
close(fd);
return -EINVAL;
}
if (fseek(stream, 0, SEEK_SET) < 0)
/* Reject anything unreasonably large before the size is trusted for an
* allocation: guards both against a malicious/oversized text file (this
* path is used for the network-fetched index.jsn) and against
* "length + 1" wrapping if st_size were ever attacker-influenced up to
* SIZE_MAX.
*/
if (st.st_size < 0 || st.st_size > (off_t)PKG_TEXT_MAX_SIZE)
{
fclose(stream);
return -errno;
close(fd);
return -EFBIG;
}
data = malloc((size_t)length + 1);
length = (size_t)st.st_size;
data = pkg_malloc((size_t)length + 1);
if (data == NULL)
{
fclose(stream);
close(fd);
return -ENOMEM;
}
nread = fread(data, 1, (size_t)length, stream);
if (nread != (size_t)length)
total = 0;
while (total < length)
{
int err = ferror(stream);
ssize_t ret;
fclose(stream);
free(data);
return err ? -EIO : -EINVAL;
ret = read(fd, data + total, length - total);
if (ret < 0)
{
if (errno == EINTR)
{
continue;
}
close(fd);
pkg_free(data);
return -errno;
}
if (ret == 0)
{
break;
}
total += (size_t)ret;
}
fclose(stream);
nread = total;
close(fd);
if (nread != length)
{
pkg_free(data);
return -EINVAL;
}
data[length] = '\0';
*buffer = data;
return 0;
}
int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text)
#ifndef CONFIG_PSEUDOFS_FILE
/****************************************************************************
* Name: pkg_store_make_tmp_path
*
* Description:
* Derive a staging path for an atomic write/copy to "path", under a
* short-name-compatible extension instead of appending ".tmp" (which
* would produce a second '.' in the final path component and break on
* FAT filesystems without long file name support).
*
****************************************************************************/
static int pkg_store_make_tmp_path(FAR char *tmp, size_t size,
FAR const char *path)
{
char tmp[PATH_MAX];
int fd;
FAR char *dot;
FAR char *slash;
int ret;
ret = snprintf(tmp, sizeof(tmp), "%s.tmp", path);
ret = snprintf(tmp, size, "%s", path);
if (ret < 0)
{
return ret;
}
if ((size_t)ret >= sizeof(tmp))
if ((size_t)ret >= size)
{
return -ENAMETOOLONG;
}
slash = strrchr(tmp, '/');
dot = strrchr(slash != NULL ? slash : tmp, '.');
if (dot != NULL)
{
*dot = '\0';
}
if (strlcat(tmp, ".tm", size) >= size)
{
return -ENAMETOOLONG;
}
return 0;
}
#endif
int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text)
{
#ifdef CONFIG_PSEUDOFS_FILE
int fd;
int ret;
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0)
{
return -errno;
}
ret = pkg_store_write_all(fd, text, strlen(text));
if (ret < 0)
{
close(fd);
unlink(path);
return ret;
}
ret = close(fd);
if (ret < 0)
{
ret = -errno;
unlink(path);
return ret;
}
return 0;
#else
char tmp[PATH_MAX];
int fd;
int ret;
ret = pkg_store_make_tmp_path(tmp, sizeof(tmp), path);
if (ret < 0)
{
return ret;
}
fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0)
{
@ -409,19 +743,42 @@ int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text)
return ret;
}
if (close(fd) < 0)
/* Force the write through to disk before renaming. nxpkg writes
* several small, unrelated files back-to-back during install (per-
* package txn state, then the shared installed-packages database);
* without an explicit sync here, the FAT driver's single shared
* sector cache can still hold a not-yet-committed buffer for this
* file when the very next atomic write starts touching a different
* file, corrupting one or both.
*/
ret = fsync(fd);
if (ret < 0)
{
ret = -errno;
close(fd);
unlink(tmp);
return -errno;
return ret;
}
if (rename(tmp, path) < 0)
ret = close(fd);
if (ret < 0)
{
ret = -errno;
unlink(tmp);
return -errno;
return ret;
}
ret = rename(tmp, path);
if (ret < 0)
{
ret = -errno;
unlink(tmp);
return ret;
}
return 0;
#endif
}
int pkg_store_copy_file(FAR const char *src, FAR const char *dest)
@ -430,6 +787,20 @@ int pkg_store_copy_file(FAR const char *src, FAR const char *dest)
int outfd;
int ret;
char buffer[512];
#ifndef CONFIG_PSEUDOFS_FILE
char tmp[PATH_MAX];
FAR const char *outpath;
ret = pkg_store_make_tmp_path(tmp, sizeof(tmp), dest);
if (ret < 0)
{
return ret;
}
outpath = tmp;
#else
FAR const char *outpath = dest;
#endif
infd = open(src, O_RDONLY);
if (infd < 0)
@ -437,7 +808,7 @@ int pkg_store_copy_file(FAR const char *src, FAR const char *dest)
return -errno;
}
outfd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644);
outfd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd < 0)
{
ret = -errno;
@ -475,18 +846,45 @@ int pkg_store_copy_file(FAR const char *src, FAR const char *dest)
close(infd);
if (close(outfd) < 0)
#ifndef CONFIG_PSEUDOFS_FILE
/* Force the payload through to disk before renaming - this is the
* largest write in the whole install pipeline (WAD/game-ELF-sized
* payloads), so a hard power-loss here is the scenario the atomic
* temp+rename is specifically protecting against.
*/
if (fsync(outfd) < 0)
{
unlink(dest);
return -errno;
ret = -errno;
close(outfd);
unlink(outpath);
return ret;
}
#endif
ret = close(outfd);
if (ret < 0)
{
ret = -errno;
unlink(outpath);
return ret;
}
#ifndef CONFIG_PSEUDOFS_FILE
if (rename(outpath, dest) < 0)
{
ret = -errno;
unlink(outpath);
return ret;
}
#endif
return 0;
errout:
close(infd);
close(outfd);
unlink(dest);
unlink(outpath);
return ret;
}
@ -499,3 +897,59 @@ int pkg_store_remove_file(FAR const char *path)
return 0;
}
int pkg_store_remove_version_dir(FAR const char *name,
FAR const char *version)
{
char path[PATH_MAX];
char entry_path[PATH_MAX];
FAR DIR *dir;
FAR struct dirent *ent;
int ret;
/* Generic directory-content removal (rather than unlinking the payload
* and manifest.jsn by their known names) so this same helper works both
* to reclaim a partially staged version directory after a failed
* install (pkg_install.c) and to prune/remove a fully-installed
* version, without needing to already know that version's artifact
* filename. Best-effort throughout: this runs from error/cleanup
* paths where a still-failing removal shouldn't itself abort the
* caller.
*/
ret = pkg_store_format_version_path(path, sizeof(path), name, version);
if (ret < 0)
{
return ret;
}
dir = opendir(path);
if (dir == NULL)
{
return errno == ENOENT ? 0 : -errno;
}
while ((ent = readdir(dir)) != NULL)
{
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
{
continue;
}
ret = snprintf(entry_path, sizeof(entry_path), "%s/%s", path,
ent->d_name);
if (ret > 0 && (size_t)ret < sizeof(entry_path))
{
unlink(entry_path);
}
}
closedir(dir);
if (rmdir(path) < 0)
{
return errno == ENOENT ? 0 : -errno;
}
return 0;
}

View file

@ -0,0 +1,35 @@
# ##############################################################################
# apps/system/nxstore/CMakeLists.txt
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
# license agreements. See the NOTICE file distributed with this work for
# additional information regarding copyright ownership. The ASF licenses this
# file to you under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
# ##############################################################################
if(CONFIG_SYSTEM_NXSTORE)
nuttx_add_application(
NAME
${CONFIG_SYSTEM_NXSTORE_PROGNAME}
PRIORITY
${CONFIG_SYSTEM_NXSTORE_PRIORITY}
STACKSIZE
${CONFIG_SYSTEM_NXSTORE_STACKSIZE}
MODULE
${CONFIG_SYSTEM_NXSTORE}
SRCS
nxstore_main.c)
endif()

41
system/nxstore/Kconfig Normal file
View file

@ -0,0 +1,41 @@
#
# For a description of the syntax of this configuration file,
# see the file kconfig-language.txt in the NuttX tools repository.
#
config SYSTEM_NXSTORE
tristate "Nxstore LVGL App Store"
default n
depends on GRAPHICS_LVGL && SYSTEM_NXPKG
select NETUTILS_CJSON
select SCHED_WAITPID
select LV_FONT_MONTSERRAT_12
select LV_FONT_MONTSERRAT_20
---help---
Enable the Nxstore LVGL graphical application store frontend.
Lists packages from the local nxpkg repository, installs the
selected one via nxpkg, and launches it.
if SYSTEM_NXSTORE
config SYSTEM_NXSTORE_PROGNAME
string "Program name"
default "nxstore"
config SYSTEM_NXSTORE_PRIORITY
int "nxstore task priority"
default 100
config SYSTEM_NXSTORE_STACKSIZE
int "nxstore stack size"
default 8192
config SYSTEM_NXSTORE_FBDEVPATH
string "Framebuffer device path"
default "/dev/fb0"
config SYSTEM_NXSTORE_INPUT_DEVPATH
string "Input device path"
default "/dev/input0"
endif

25
system/nxstore/Make.defs Normal file
View file

@ -0,0 +1,25 @@
############################################################################
# apps/system/nxstore/Make.defs
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
ifneq ($(CONFIG_SYSTEM_NXSTORE),)
CONFIGURED_APPS += $(APPDIR)/system/nxstore
endif

32
system/nxstore/Makefile Normal file
View file

@ -0,0 +1,32 @@
############################################################################
# apps/system/nxstore/Makefile
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
include $(APPDIR)/Make.defs
PROGNAME = $(CONFIG_SYSTEM_NXSTORE_PROGNAME)
PRIORITY = $(CONFIG_SYSTEM_NXSTORE_PRIORITY)
STACKSIZE = $(CONFIG_SYSTEM_NXSTORE_STACKSIZE)
MODULE = $(CONFIG_SYSTEM_NXSTORE)
MAINSRC = nxstore_main.c
include $(APPDIR)/Application.mk

File diff suppressed because it is too large Load diff