This commit is contained in:
Aviral Garg 2026-07-27 19:01:30 +05:30 committed by GitHub
commit 3cc10132f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 6021 additions and 280 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

@ -4,7 +4,7 @@
#
config GAMES_BRICKMATCH
bool "Brickmatch Game"
tristate "Brickmatch Game"
default n
---help---
Enable Brickmatch games. Brickmatch is like a mix
@ -88,6 +88,10 @@ config GAMES_BRICKMATCH_USE_GESTURE
config GAMES_BRICKMATCH_USE_GPIO
bool "GPIO pins as Input"
config GAMES_BRICKMATCH_USE_TOUCHSCREEN
bool "Touchscreen swipe as input"
depends on INPUT_TOUCHSCREEN
endchoice
if GAMES_BRICKMATCH_USE_GPIO
@ -118,4 +122,34 @@ config GAMES_BRICKMATCH_RIGHT_KEY_PATH
endif #GAMES_BRICKMATCH_USE_GPIO
if GAMES_BRICKMATCH_USE_TOUCHSCREEN
config GAMES_BRICKMATCH_TOUCH_DEVPATH
string "Touchscreen device path"
default "/dev/input0"
---help---
Path of the touchscreen device to read swipe gestures from.
config GAMES_BRICKMATCH_TOUCH_THRESHOLD
int "Minimum swipe distance"
range 1 32767
default 24
---help---
Minimum movement in calibrated touchscreen coordinate units before
a drag is reported as a direction.
endif #GAMES_BRICKMATCH_USE_TOUCHSCREEN
if !GAMES_BRICKMATCH_USE_LED_MATRIX
config GAMES_BRICKMATCH_YOFFSET
int "Top framebuffer rows to reserve"
range 0 500000
default 0
---help---
Leave this many rows at the top of the framebuffer unchanged. This
allows a launcher or supervisor to reserve a status or control bar.
endif # !GAMES_BRICKMATCH_USE_LED_MATRIX
endif

View file

@ -0,0 +1,183 @@
/****************************************************************************
* apps/games/brickmatch/bm_input_touch.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_GAMES_BRICKMATCH_BM_INPUT_TOUCH_H
#define __APPS_GAMES_BRICKMATCH_BM_INPUT_TOUCH_H
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <nuttx/input/touchscreen.h>
#include "bm_inputs.h"
/****************************************************************************
* Preprocessor Definitions
****************************************************************************/
/* Tracks the in-progress drag so each dev_read_input() call only has to
* look at the latest sample, not replay history. Reset on TOUCH_UP so a
* finger lift always starts the next swipe's baseline fresh; re-baselined
* (rather than requiring a lift) after firing a direction, so a single
* continued drag can fire more than one swipe.
*/
static bool g_bm_touch_active;
static int32_t g_bm_touch_origin_x;
static int32_t g_bm_touch_origin_y;
/****************************************************************************
* Name: dev_input_init
*
* Description:
* Initialize input method.
*
* Parameters:
* dev - Input state data
*
* Returned Value:
* Zero (OK) is returned on success. A negated errno value is returned on
* failure.
*
****************************************************************************/
int dev_input_init(FAR struct input_state_s *dev)
{
dev->fd_touch = open(CONFIG_GAMES_BRICKMATCH_TOUCH_DEVPATH,
O_RDONLY | O_NONBLOCK);
if (dev->fd_touch < 0)
{
int errcode = errno;
fprintf(stderr, "ERROR: Failed to open %s: %d\n",
CONFIG_GAMES_BRICKMATCH_TOUCH_DEVPATH, errcode);
return -errcode;
}
g_bm_touch_active = false;
return OK;
}
/****************************************************************************
* Name: dev_read_input
*
* Description:
* Read inputs and returns result in input state data. Interprets a
* drag/swipe gesture rather than an absolute tap position, since
* brickmatch's board layout has no natural on-screen buttons to tap -
* this mirrors how the gesture-sensor backend (bm_input_gesture.h)
* reports discrete directions, just derived from a touchscreen instead
* of a dedicated gesture sensor.
*
* Parameters:
* dev - Input state data
*
* Returned Value:
* Zero (OK)
*
****************************************************************************/
int dev_read_input(FAR struct input_state_s *dev)
{
struct touch_sample_s sample;
int32_t dx;
int32_t dy;
ssize_t nbytes;
dev->dir = DIR_NONE;
nbytes = read(dev->fd_touch, &sample, sizeof(sample));
if (nbytes < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
usleep(10000);
return OK;
}
return -errno;
}
if (nbytes != sizeof(sample) || sample.npoints < 1)
{
return OK;
}
if (sample.point[0].flags & TOUCH_UP)
{
g_bm_touch_active = false;
return OK;
}
if ((sample.point[0].flags & TOUCH_POS_VALID) == 0)
{
return OK;
}
if (sample.point[0].flags & TOUCH_DOWN)
{
g_bm_touch_active = true;
g_bm_touch_origin_x = sample.point[0].x;
g_bm_touch_origin_y = sample.point[0].y;
return OK;
}
if (!g_bm_touch_active)
{
return OK;
}
dx = sample.point[0].x - g_bm_touch_origin_x;
dy = sample.point[0].y - g_bm_touch_origin_y;
if (abs(dx) < CONFIG_GAMES_BRICKMATCH_TOUCH_THRESHOLD &&
abs(dy) < CONFIG_GAMES_BRICKMATCH_TOUCH_THRESHOLD)
{
return OK;
}
if (abs(dx) > abs(dy))
{
dev->dir = dx > 0 ? DIR_RIGHT : DIR_LEFT;
}
else
{
dev->dir = dy > 0 ? DIR_DOWN : DIR_UP;
}
g_bm_touch_origin_x = sample.point[0].x;
g_bm_touch_origin_y = sample.point[0].y;
return OK;
}
#endif /* __APPS_GAMES_BRICKMATCH_BM_INPUT_TOUCH_H */

View file

@ -64,6 +64,9 @@ struct input_state_s
#ifdef CONFIG_GAMES_BRICKMATCH_USE_GPIO
int fd_gpio;
#endif
#ifdef CONFIG_GAMES_BRICKMATCH_USE_TOUCHSCREEN
int fd_touch;
#endif
int dir; /* Direction to move the blocks */
};

View file

@ -28,6 +28,7 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@ -58,6 +59,10 @@
#include "bm_input_gpio.h"
#endif
#ifdef CONFIG_GAMES_BRICKMATCH_USE_TOUCHSCREEN
#include "bm_input_touch.h"
#endif
/****************************************************************************
* Preprocessor Definitions
****************************************************************************/
@ -93,6 +98,17 @@ struct screen_state_s
struct fb_overlayinfo_s oinfo;
#endif
FAR void *fbmem;
#ifndef CONFIG_GAMES_BRICKMATCH_USE_LED_MATRIX
/* Absolute placement of the game_screen_s board within the physical
* framebuffer. draw_rect() and update_screen() add these to every
* board-relative area, so the rest of the coordinate math stays in
* board-local coordinates that start at (0, 0).
*/
int abs_xoff;
int abs_yoff;
#endif
};
struct game_screen_s
@ -112,6 +128,19 @@ struct game_screen_s
* Private Data
****************************************************************************/
/* Set only by quit_signal_handler(), an async-signal-safe flag. Actual
* shutdown happens from the main loop so a launcher can ask either a
* built-in application or loadable module to stop cooperatively.
*/
static volatile sig_atomic_t g_quit_requested;
static void quit_signal_handler(int signo)
{
(void)signo;
g_quit_requested = 1;
}
#ifndef CONFIG_GAMES_BRICKMATCH_USE_LED_MATRIX
static const char g_default_fbdev[] = "/dev/fb0";
#else
@ -133,7 +162,7 @@ int prev_board[ROW][COL] =
/* Colors used in the game plus Black */
static const uint16_t pallete[NCOLORS + 1] =
static const uint16_t palette[NCOLORS + 1] =
{
RGB16_BLACK,
RGB16_BLUE,
@ -173,13 +202,14 @@ static void draw_rect(FAR struct screen_state_s *state,
int x;
int y;
row = (FAR uint8_t *)state->fbmem + state->pinfo.stride * area->y;
row = (FAR uint8_t *)state->fbmem +
state->pinfo.stride * (area->y + state->abs_yoff);
for (y = 0; y < area->h; y++)
{
dest = ((FAR uint16_t *)row) + area->x;
dest = ((FAR uint16_t *)row) + area->x + state->abs_xoff;
for (x = 0; x < area->w; x++)
{
*dest++ = pallete[color];
*dest++ = palette[color];
}
row += state->pinfo.stride;
@ -204,11 +234,17 @@ static void draw_rect(FAR struct screen_state_s *state,
void update_screen(FAR struct screen_state_s *state,
FAR struct fb_area_s *area)
{
#ifdef CONFIG_FB_UPDATE
struct fb_area_s abs_area;
int ret;
#ifdef CONFIG_FB_UPDATE
abs_area.x = area->x + state->abs_xoff;
abs_area.y = area->y + state->abs_yoff;
abs_area.w = area->w;
abs_area.h = area->h;
ret = ioctl(state->fd_fb, FBIO_UPDATE,
(unsigned long)((uintptr_t)area));
(unsigned long)((uintptr_t)&abs_area));
if (ret < 0)
{
int errcode = errno;
@ -410,7 +446,7 @@ void draw_board(FAR struct screen_state_s *state,
{
for (y = 1; y <= BOARDY_SIZE; y++)
{
rgb_val = RGB16TO24(pallete[board[x][y]]);
rgb_val = RGB16TO24(palette[board[x][y]]);
tmp = dim_color(rgb_val, 0.15);
*bp++ = ws2812_gamma_correct(tmp);
}
@ -990,7 +1026,23 @@ int main(int argc, FAR char *argv[])
struct input_state_s input;
struct screen_state_s state;
struct fb_area_s area;
struct sigaction sa;
int ret;
int status = EXIT_SUCCESS;
#ifndef CONFIG_GAMES_BRICKMATCH_USE_LED_MATRIX
int usable_yres;
#endif
g_quit_requested = 0;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = quit_signal_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGTERM, &sa, NULL) < 0)
{
fprintf(stderr, "ERROR: Failed to install SIGTERM handler: %d\n",
errno);
return EXIT_FAILURE;
}
init_board();
@ -1024,12 +1076,23 @@ int main(int argc, FAR char *argv[])
printf(" yres: %u\n", state.vinfo.yres);
printf(" nplanes: %u\n", state.vinfo.nplanes);
/* Setup the screen information */
/* Setup the screen information. The board is square and sized to the
* smaller display dimension after the configured top rows are reserved.
* Place that square below the reserved rows and horizontally center it.
*/
if (state.vinfo.yres <= CONFIG_GAMES_BRICKMATCH_YOFFSET)
{
fprintf(stderr, "ERROR: Framebuffer is shorter than the y offset\n");
close(state.fd_fb);
return EXIT_FAILURE;
}
usable_yres = state.vinfo.yres - CONFIG_GAMES_BRICKMATCH_YOFFSET;
screen.xres = state.vinfo.xres > usable_yres ?
usable_yres : state.vinfo.xres;
screen.yres = screen.xres;
screen.xres = state.vinfo.xres > state.vinfo.yres ?
state.vinfo.yres : state.vinfo.xres;
screen.yres = state.vinfo.yres > state.vinfo.xres ?
state.vinfo.xres : state.vinfo.yres;
screen.xoff = (screen.xres % NCOLORS) / 2;
screen.yoff = (screen.yres % NCOLORS) / 2;
screen.steps = 2;
@ -1037,6 +1100,9 @@ int main(int argc, FAR char *argv[])
screen.blklen = (screen.xres / NCOLORS);
screen.ncolors = NCOLORS;
state.abs_xoff = (state.vinfo.xres - screen.xres) / 2;
state.abs_yoff = CONFIG_GAMES_BRICKMATCH_YOFFSET;
/* Get the display planeinfo */
ret = ioctl(state.fd_fb, FBIOGET_PLANEINFO,
@ -1057,6 +1123,14 @@ int main(int argc, FAR char *argv[])
printf(" display: %u\n", state.pinfo.display);
printf(" bpp: %u\n", state.pinfo.bpp);
if (state.pinfo.xres_virtual < state.abs_xoff + screen.xres ||
state.pinfo.yres_virtual < state.abs_yoff + screen.yres)
{
fprintf(stderr, "ERROR: Virtual framebuffer is too small\n");
close(state.fd_fb);
return EXIT_FAILURE;
}
state.fbmem = mmap(NULL, state.pinfo.fblen, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FILE, state.fd_fb, 0);
if (state.fbmem == MAP_FAILED)
@ -1088,15 +1162,37 @@ int main(int argc, FAR char *argv[])
draw_board(&state, &area, &screen);
dev_input_init(&input);
ret = dev_input_init(&input);
if (ret < 0)
{
fprintf(stderr, "ERROR: Failed to initialize input: %d\n", ret);
#ifndef CONFIG_GAMES_BRICKMATCH_USE_LED_MATRIX
munmap(state.fbmem, state.pinfo.fblen);
#else
free(state.fbmem);
#endif
close(state.fd_fb);
return EXIT_FAILURE;
}
input.dir = DIR_NONE;
while (1)
while (!g_quit_requested)
{
while (input.dir == DIR_NONE)
while (input.dir == DIR_NONE && !g_quit_requested)
{
ret = dev_read_input(&input);
if (ret < 0)
{
fprintf(stderr, "ERROR: Failed to read input: %d\n", ret);
status = EXIT_FAILURE;
g_quit_requested = 1;
}
}
if (g_quit_requested)
{
break;
}
screen.dir = input.dir;
@ -1168,5 +1264,16 @@ int main(int argc, FAR char *argv[])
fill_edge();
}
return 0;
#ifndef CONFIG_GAMES_BRICKMATCH_USE_LED_MATRIX
munmap(state.fbmem, state.pinfo.fblen);
#else
free(state.fbmem);
#endif
#ifdef CONFIG_GAMES_BRICKMATCH_USE_TOUCHSCREEN
close(input.fd_touch);
#endif
close(state.fd_fb);
return status;
}

View file

@ -4,7 +4,7 @@
#
config GAMES_CGOL
bool "Conway's Game of Life"
tristate "Conway's Game of Life"
depends on VIDEO_FB
default n
---help---
@ -25,7 +25,7 @@ config GAMES_CGOL_PRIORITY
config GAMES_CGOL_STACKSIZE
int "CGOL stack size"
default DEFAULT_TASK_STACKSIZE
default 4096
config GAMES_CGOL_FBDEV
string "CGOL frame buffer device path"
@ -34,6 +34,14 @@ config GAMES_CGOL_FBDEV
The frame buffer device path that will be used by default for rendering.
Device path can still be selected from first command line argument.
config GAMES_CGOL_YOFFSET
int "Top framebuffer rows to reserve"
range 0 500000
default 0
---help---
Leave this many rows at the top of the framebuffer unchanged. This
allows a launcher or supervisor to reserve a status or control bar.
config GAMES_CGOL_DBLBUF
bool "CGOL double buffered"
default n

View file

@ -82,6 +82,7 @@
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -174,6 +175,14 @@ struct fb_state_s
struct fb_videoinfo_s vinfo;
struct fb_planeinfo_s pinfo;
unsigned int scale;
/* Rows [0, abs_yoff) are reserved for a launcher or supervisor and are
* never cleared or drawn to.
*/
unsigned int abs_yoff;
unsigned int usable_yres;
int fd;
void *fb; /* Real frame buffer */
#ifdef CONFIG_GAMES_CGOL_DBLBUF
@ -190,10 +199,27 @@ typedef void (*cell_render_f)(const struct fb_state_s *, uint32_t x,
* Private Data
****************************************************************************/
/* Set only by quit_signal_handler(), an async-signal-safe flag - the
* actual shutdown happens from the main render loop, a known-safe
* point, rather than from the handler itself. cgol has no natural
* exit condition of its own (it renders forever), so without this, an
* external supervisor (nxstore) has no way to ask this process to
* stop - see games/NXDoom's i_install_quit_signal()/
* i_poll_quit_signal() for the same pattern.
*/
static volatile sig_atomic_t g_quit_requested;
/****************************************************************************
* Private Functions
****************************************************************************/
static void quit_signal_handler(int signo)
{
(void)signo;
g_quit_requested = 1;
}
/****************************************************************************
* Name: cgol_init
*
@ -515,28 +541,35 @@ static void cgol_advance(unsigned int *map)
static void cgol_render_update(const struct fb_state_s *state)
{
#ifdef CONFIG_FB_UPDATE
struct fb_area_s *full_screen;
struct fb_area_s playfield;
int err;
/* Create an area with the dimensions of the full screen for updating the
* frame buffer after render operations are complete.
/* Only the area below the configured offset is cleared or drawn to.
* Restrict the update area to match.
*/
full_screen.x = 0;
full_screen.y = 0;
full_screen.w = fb_state.vinfo.xres;
full_screen.h = fb_state.vinfo.yres;
playfield.x = 0;
playfield.y = state->abs_yoff;
playfield.w = state->vinfo.xres;
playfield.h = state->usable_yres;
#endif
/* If double buffering, copy the RAM buffer to the frame buffer */
/* If double buffering, copy the RAM buffer to the frame buffer. Skip
* reserved rows at the top of both buffers because rambuf never clears
* or draws into them.
*/
#ifdef CONFIG_GAMES_CGOL_DBLBUF
memcpy(state->fb, state->rambuf, state->pinfo.fblen);
memcpy((FAR uint8_t *)state->fb + state->pinfo.stride * state->abs_yoff,
(FAR uint8_t *)state->rambuf +
state->pinfo.stride * state->abs_yoff,
state->pinfo.stride * state->usable_yres);
#endif
/* If the frame buffer on this device needs explicit updates, do that */
#ifdef CONFIG_FB_UPDATE
err = ioctl(fb_state.fd, FBIO_UPDATE, (uintptr_t)&full_screen);
err = ioctl(state->fd, FBIO_UPDATE, (uintptr_t)&playfield);
if (err < 0)
{
fprintf(stderr, "Couldn't update screen: %d\n", errno);
@ -562,14 +595,18 @@ static void cgol_render_clear(const struct fb_state_s *state)
{
/* TODO: we can do this more efficiently if we just blot out the pixels
* used for live cells last frame.
*
* Rows [0, abs_yoff) are reserved. Every case below starts at abs_yoff
* and only clears usable_yres rows.
*/
switch (state->pinfo.bpp)
{
case 32:
for (uint32_t y = 0; y < state->pinfo.yres_virtual; y++)
for (uint32_t y = 0; y < state->usable_yres; y++)
{
uint8_t *row = render_buf(state) + state->pinfo.stride * y;
uint8_t *row = render_buf(state) +
state->pinfo.stride * (y + state->abs_yoff);
for (uint32_t x = 0; x < state->pinfo.xres_virtual; x++)
{
((uint32_t *)(row))[x] = BG32;
@ -578,9 +615,10 @@ static void cgol_render_clear(const struct fb_state_s *state)
break;
case 24:
for (uint32_t y = 0; y < state->pinfo.yres_virtual; y++)
for (uint32_t y = 0; y < state->usable_yres; y++)
{
uint8_t *row = render_buf(state) + state->pinfo.stride * y;
uint8_t *row = render_buf(state) +
state->pinfo.stride * (y + state->abs_yoff);
for (uint32_t x = 0; x < state->pinfo.xres_virtual; x++)
{
*row++ = RGB24BLUE(BG24);
@ -592,9 +630,10 @@ static void cgol_render_clear(const struct fb_state_s *state)
case 16:
{
for (uint32_t y = 0; y < state->pinfo.yres_virtual; y++)
for (uint32_t y = 0; y < state->usable_yres; y++)
{
uint8_t *row = render_buf(state) + state->pinfo.stride * y;
uint8_t *row = render_buf(state) +
state->pinfo.stride * (y + state->abs_yoff);
for (uint32_t x = 0; x < state->pinfo.xres_virtual; x++)
{
((uint16_t *)(row))[x] = BG16;
@ -604,7 +643,12 @@ static void cgol_render_clear(const struct fb_state_s *state)
break;
case 8:
memset(render_buf(state), BG8, state->pinfo.fblen);
for (uint32_t y = 0; y < state->usable_yres; y++)
{
uint8_t *row = render_buf(state) +
state->pinfo.stride * (y + state->abs_yoff);
memset(row, BG8, state->pinfo.xres_virtual);
}
break;
}
}
@ -633,6 +677,10 @@ static void cgol_render_cell8(const struct fb_state_s *state, uint32_t x,
x *= state->scale;
y *= state->scale;
/* Shift down past the reserved rows */
y += state->abs_yoff;
/* Starting at the (x, y) pair, we draw `scale` cells in each direction */
for (uint8_t yy = 0; yy < state->scale; yy++)
@ -671,6 +719,10 @@ static void cgol_render_cell16(const struct fb_state_s *state, uint32_t x,
x *= state->scale;
y *= state->scale;
/* Shift down past the reserved rows */
y += state->abs_yoff;
/* Starting at the (x, y) pair, we draw `scale` cells in each direction */
for (uint8_t yy = 0; yy < state->scale; yy++)
@ -709,6 +761,10 @@ static void cgol_render_cell24(const struct fb_state_s *state, uint32_t x,
x *= state->scale;
y *= state->scale;
/* Shift down past the reserved rows */
y += state->abs_yoff;
/* Starting at the (x, y) pair, we draw `scale` cells in each direction */
for (uint8_t yy = 0; yy < state->scale; yy++)
@ -749,6 +805,10 @@ static void cgol_render_cell32(const struct fb_state_s *state, uint32_t x,
x *= state->scale;
y *= state->scale;
/* Shift down past the reserved rows */
y += state->abs_yoff;
/* Starting at the (x, y) pair, we draw `scale` cells in each direction */
for (uint8_t yy = 0; yy < state->scale; yy++)
@ -914,6 +974,17 @@ int main(int argc, FAR char *argv[])
unsigned int map[WORD_COUNT];
unsigned int yscale;
unsigned int xscale;
struct sigaction sa;
g_quit_requested = 0;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = quit_signal_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGTERM, &sa, NULL) < 0)
{
fprintf(stderr, "Failed to install SIGTERM handler: %d\n", errno);
return EXIT_FAILURE;
}
if (argc == 2)
{
@ -949,16 +1020,27 @@ int main(int argc, FAR char *argv[])
return EXIT_FAILURE;
}
/* Leave the configured top rows untouched and fold that into the minimum
* resolution check below.
*/
fb_state.abs_yoff = CONFIG_GAMES_CGOL_YOFFSET;
/* If the frame buffer resolution is too small to support our game at its
* lowest resolution (one pixel per cell), we can't play :(
*/
if (fb_state.vinfo.xres < CONFIG_GAMES_CGOL_MAPWIDTH ||
fb_state.vinfo.yres < CONFIG_GAMES_CGOL_MAPHEIGHT)
fb_state.vinfo.yres <
CONFIG_GAMES_CGOL_MAPHEIGHT + fb_state.abs_yoff ||
fb_state.pinfo.xres_virtual < CONFIG_GAMES_CGOL_MAPWIDTH ||
fb_state.pinfo.yres_virtual <
CONFIG_GAMES_CGOL_MAPHEIGHT + fb_state.abs_yoff)
{
fprintf(stderr,
"Needed at least %u x %u px resolution, but got %u x %u",
CONFIG_GAMES_CGOL_MAPWIDTH, CONFIG_GAMES_CGOL_MAPHEIGHT,
"Needed at least %u x %u px resolution, but got %u x %u\n",
CONFIG_GAMES_CGOL_MAPWIDTH,
CONFIG_GAMES_CGOL_MAPHEIGHT + fb_state.abs_yoff,
fb_state.vinfo.xres, fb_state.vinfo.yres);
close(fb_state.fd);
return EXIT_FAILURE;
@ -1001,6 +1083,7 @@ int main(int argc, FAR char *argv[])
if (fb_state.rambuf == NULL)
{
fprintf(stderr, "Couldn't allocate double buffer: %d\n", errno);
munmap(fb_state.fb, fb_state.pinfo.fblen);
close(fb_state.fd);
return EXIT_FAILURE;
}
@ -1012,8 +1095,10 @@ int main(int argc, FAR char *argv[])
* This is selected by picking the minimum of the scale options.
*/
fb_state.usable_yres = fb_state.pinfo.yres_virtual - fb_state.abs_yoff;
xscale = fb_state.pinfo.xres_virtual / CONFIG_GAMES_CGOL_MAPWIDTH;
yscale = fb_state.pinfo.yres_virtual / CONFIG_GAMES_CGOL_MAPHEIGHT;
yscale = fb_state.usable_yres / CONFIG_GAMES_CGOL_MAPHEIGHT;
fb_state.scale = xscale < yscale ? xscale : yscale;
/* Now we can seed the game with some random starting cells */
@ -1024,9 +1109,9 @@ int main(int argc, FAR char *argv[])
cgol_render_clear(&fb_state);
/* Loop the game forever */
/* Loop the game until asked to quit */
for (; ; )
while (!g_quit_requested)
{
/* Render the freshly calculated cells */
@ -1052,6 +1137,7 @@ int main(int argc, FAR char *argv[])
#ifdef CONFIG_GAMES_CGOL_DBLBUF
free(fb_state.rambuf);
#endif
munmap(fb_state.fb, fb_state.pinfo.fblen);
close(fb_state.fd);
return EXIT_SUCCESS;
}

View file

@ -4,7 +4,7 @@
#
config GAMES_MATCH4
bool "Match 4 Game"
tristate "Match 4 Game"
default n
---help---
Enable Match 4 game.

View file

@ -4,7 +4,7 @@
#
config GAMES_SNAKE
bool "Snake Game"
tristate "Snake Game"
default n
---help---
Enable Snake game.

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

View file

@ -32,6 +32,7 @@ import argparse
import hashlib
import json
import shutil
import struct
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List
@ -41,6 +42,21 @@ PACKAGE_SPEC_HELP = (
"package must look like " "<name>:<version>:<elf|shared-lib>:<source>"
)
# Fixed square icon size nxstore renders app-list icons at (see
# nxstore_load_icon() in system/nxstore/nxstore_main.c) - small enough to
# stay a quick download over the board's WiFi, large enough to read as
# actual artwork rather than a favicon.
ICON_SIZE = 48
# Matches lv_image_header_t's own bit layout (LVGL v9, see
# graphics/lvgl/lvgl/src/draw/lv_image_dsc.h): magic/cf/flags packed into
# the first 4 bytes, then w/h, then stride/reserved - all little-endian,
# 12 bytes total, immediately followed by raw pixel data. This is
# deliberately the simplest format LVGL can render directly with no
# decoder, since the board has no PNG/JPEG decode capability wired up.
LV_IMAGE_HEADER_MAGIC = 0x19
LV_COLOR_FORMAT_RGB565 = 0x12
@dataclass
class PackageSpec:
@ -88,6 +104,24 @@ def parse_package_spec(value: str) -> PackageSpec:
)
def parse_name_value(value: str) -> tuple:
parts = value.split("=", 1)
if len(parts) != 2 or not parts[0] or not parts[1]:
raise argparse.ArgumentTypeError("value must look like <name>=<text>")
return parts[0], parts[1]
def parse_icon_spec(value: str) -> tuple:
name, source = parse_name_value(value)
source_path = Path(source).expanduser().resolve()
if not source_path.is_file():
msg = f"icon source does not exist: {source_path}"
raise argparse.ArgumentTypeError(msg)
return name, source_path
def artifact_relpath(arch: str, chip: str, compat: str,
spec: PackageSpec) -> Path: # fmt: skip
filename = spec.source.name
@ -96,6 +130,47 @@ def artifact_relpath(arch: str, chip: str, compat: str,
return path
def icon_relpath(arch: str, chip: str, compat: str, name: str) -> Path:
# Keep one repository object per package. nxstore keys its local cache
# by package name and version, so a new catalog version fetches this
# object again without requiring versioned repository duplication.
return Path("icons") / arch / chip / compat / f"{name}.bin"
def encode_icon_rgb565(source: Path, size: int = ICON_SIZE) -> bytes:
"""Convert an arbitrary source image into nxstore's raw icon format:
a 12-byte lv_image_header_t-compatible header (see the module
docstring comment above) followed by size*size RGB565 pixel data.
"""
from PIL import Image
with Image.open(source) as img:
img = img.convert("RGB").resize((size, size), Image.Resampling.LANCZOS)
pixels = list(img.getdata())
stride = size * 2
header = struct.pack(
"<BBHHHHH",
LV_IMAGE_HEADER_MAGIC,
LV_COLOR_FORMAT_RGB565,
0, # flags
size, # w
size, # h
stride,
0, # reserved_2
)
body = bytearray(stride * size)
offset = 0
for r, g, b in pixels:
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
struct.pack_into("<H", body, offset, rgb565)
offset += 2
return header + bytes(body)
def package_identity(package: Dict[str, str]) -> tuple:
return (
package["name"],
@ -171,6 +246,30 @@ def main() -> int:
type=parse_package_spec,
help=PACKAGE_SPEC_HELP,
)
parser.add_argument(
"--package-description",
action="append",
default=[],
type=parse_name_value,
metavar="NAME=TEXT",
help="Optional package description, keyed by package name",
)
parser.add_argument(
"--package-category",
action="append",
default=[],
type=parse_name_value,
metavar="NAME=TEXT",
help="Optional package category, keyed by package name",
)
parser.add_argument(
"--package-icon",
action="append",
default=[],
type=parse_icon_spec,
metavar="NAME=PATH",
help="Optional source image for a package icon",
)
args = parser.parse_args()
repo_dir = args.repo_dir.expanduser().resolve()
@ -181,6 +280,9 @@ def main() -> int:
for package in packages:
packages_by_id[package_identity(package)] = package
prefix = args.artifact_prefix.rstrip("/")
descriptions = dict(args.package_description)
categories = dict(args.package_category)
icons = dict(args.package_icon)
for spec in args.package:
relpath = artifact_relpath(args.arch, args.chip, args.compat, spec)
@ -201,6 +303,26 @@ def main() -> int:
"sha256": sha256_file(destination),
"type": spec.payload_type,
}
if spec.name in descriptions:
package["description"] = descriptions[spec.name]
if spec.name in categories:
package["category"] = categories[spec.name]
if spec.name in icons:
icon_bytes = encode_icon_rgb565(icons[spec.name])
icon_dest = repo_dir / icon_relpath(
args.arch, args.chip, args.compat, spec.name
)
icon_dest.parent.mkdir(parents=True, exist_ok=True)
icon_dest.write_bytes(icon_bytes)
icon_rel = icon_relpath(
args.arch, args.chip, args.compat, spec.name
).as_posix()
package["icon"] = f"{prefix}/{icon_rel}" if prefix else icon_rel
packages_by_id[package_identity(package)] = package
packages = sorted(