mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
Merge 9372de597b into 6ca1272b99
This commit is contained in:
commit
2cab69aee0
23 changed files with 5434 additions and 236 deletions
37
examples/calculator/CMakeLists.txt
Normal file
37
examples/calculator/CMakeLists.txt
Normal 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()
|
||||
35
examples/calculator/Kconfig
Normal file
35
examples/calculator/Kconfig
Normal 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
|
||||
25
examples/calculator/Make.defs
Normal file
25
examples/calculator/Make.defs
Normal 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
|
||||
32
examples/calculator/Makefile
Normal file
32
examples/calculator/Makefile
Normal 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
|
||||
518
examples/calculator/calculator_main.c
Normal file
518
examples/calculator/calculator_main.c
Normal 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;
|
||||
}
|
||||
37
include/system/nxstore_chrome.h
Normal file
37
include/system/nxstore_chrome.h
Normal 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 */
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, ...);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
774
system/nxpkg/pkg_repo.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
35
system/nxstore/CMakeLists.txt
Normal file
35
system/nxstore/CMakeLists.txt
Normal 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
41
system/nxstore/Kconfig
Normal 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
25
system/nxstore/Make.defs
Normal 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
32
system/nxstore/Makefile
Normal 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
|
||||
2056
system/nxstore/nxstore_main.c
Normal file
2056
system/nxstore/nxstore_main.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue