diff --git a/system/nxpkg/CMakeLists.txt b/system/nxpkg/CMakeLists.txt new file mode 100644 index 00000000000..8ac3e358532 --- /dev/null +++ b/system/nxpkg/CMakeLists.txt @@ -0,0 +1,43 @@ +# ############################################################################## +# apps/system/nxpkg/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_NXPKG) + nuttx_add_application( + MODULE + ${CONFIG_SYSTEM_NXPKG} + NAME + ${CONFIG_SYSTEM_NXPKG_PROGNAME} + STACKSIZE + ${CONFIG_SYSTEM_NXPKG_STACKSIZE} + PRIORITY + ${CONFIG_SYSTEM_NXPKG_PRIORITY} + SRCS + pkg_main.c + pkg_compat.c + pkg_hash.c + pkg_install.c + pkg_log.c + pkg_manifest.c + pkg_metadata.c + pkg_store.c + pkg_txn.c) +endif() diff --git a/system/nxpkg/Kconfig b/system/nxpkg/Kconfig new file mode 100644 index 00000000000..afc7fa32670 --- /dev/null +++ b/system/nxpkg/Kconfig @@ -0,0 +1,32 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config SYSTEM_NXPKG + bool "'nxpkg' lifecycle helper" + default n + select CRYPTO + select NETUTILS_CJSON + ---help--- + Enable the thin package lifecycle helper proposed for the + Dynamic ELF distribution work. + +if SYSTEM_NXPKG + +config SYSTEM_NXPKG_PROGNAME + string "Program name" + default "nxpkg" + ---help--- + This is the name of the program that will be used when the + nxpkg tool is installed. + +config SYSTEM_NXPKG_PRIORITY + int "'nxpkg' task priority" + default 100 + +config SYSTEM_NXPKG_STACKSIZE + int "'nxpkg' stack size" + default 16384 + +endif diff --git a/system/nxpkg/Make.defs b/system/nxpkg/Make.defs new file mode 100644 index 00000000000..bdb799080fe --- /dev/null +++ b/system/nxpkg/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/system/nxpkg/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_NXPKG),) +CONFIGURED_APPS += $(APPDIR)/system/nxpkg +endif diff --git a/system/nxpkg/Makefile b/system/nxpkg/Makefile new file mode 100644 index 00000000000..757f9fc896e --- /dev/null +++ b/system/nxpkg/Makefile @@ -0,0 +1,34 @@ +############################################################################ +# apps/system/nxpkg/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_NXPKG_PROGNAME) +PRIORITY = $(CONFIG_SYSTEM_NXPKG_PRIORITY) +STACKSIZE = $(CONFIG_SYSTEM_NXPKG_STACKSIZE) +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 +MAINSRC = pkg_main.c + +include $(APPDIR)/Application.mk diff --git a/system/nxpkg/pkg.h b/system/nxpkg/pkg.h new file mode 100644 index 00000000000..39a4bf0c21d --- /dev/null +++ b/system/nxpkg/pkg.h @@ -0,0 +1,187 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg.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_SYSTEM_NXPKG_PKG_H +#define __APPS_SYSTEM_NXPKG_PKG_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include +#include +#include + +/**************************************************************************** + * 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_NAME_MAX 63 +#define PKG_VERSION_MAX 31 +#define PKG_ARCH_MAX 31 +#define PKG_COMPAT_MAX 63 +#define PKG_HASH_HEX_LEN 64 +#define PKG_INDEX_MAX 32 +#define PKG_INSTALLED_MAX 16 +#define PKG_INSTALLED_VERSIONS_MAX 8 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +enum pkg_payload_type_e +{ + PKG_PAYLOAD_ELF = 0, + PKG_PAYLOAD_SHARED_LIB +}; + +enum pkg_txn_state_e +{ + PKG_TXN_IDLE = 0, + PKG_TXN_FETCHING, + PKG_TXN_VERIFIED, + PKG_TXN_STAGED, + PKG_TXN_COMPAT_OK, + PKG_TXN_ACTIVATED, + PKG_TXN_CLEANUP, + PKG_TXN_FAILED, + PKG_TXN_RESTORE +}; + +struct pkg_manifest_s +{ + char name[PKG_NAME_MAX + 1]; + char version[PKG_VERSION_MAX + 1]; + char arch[PKG_ARCH_MAX + 1]; + char compat[PKG_COMPAT_MAX + 1]; + char artifact[PATH_MAX]; + char sha256[PKG_HASH_HEX_LEN + 1]; + enum pkg_payload_type_e type; +}; + +struct pkg_index_s +{ + struct pkg_manifest_s manifests[PKG_INDEX_MAX]; + size_t count; +}; + +struct pkg_installed_entry_s +{ + char name[PKG_NAME_MAX + 1]; + char current[PKG_VERSION_MAX + 1]; + char previous[PKG_VERSION_MAX + 1]; + char arch[PKG_ARCH_MAX + 1]; + char compat[PKG_COMPAT_MAX + 1]; + char versions[PKG_INSTALLED_VERSIONS_MAX][PKG_VERSION_MAX + 1]; + enum pkg_payload_type_e type; + size_t version_count; +}; + +struct pkg_installed_db_s +{ + struct pkg_installed_entry_s entries[PKG_INSTALLED_MAX]; + size_t count; +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +const char *pkg_manifest_type_str(enum pkg_payload_type_e type); +int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest); +int pkg_manifest_parse_type(FAR const char *value, + FAR enum pkg_payload_type_e *type); + +int pkg_store_prepare_layout(void); +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_installed_path(FAR char *buffer, size_t size); +int pkg_store_format_package_root(FAR char *buffer, size_t size, + FAR const char *name); +int pkg_store_format_version_path(FAR char *buffer, size_t size, + FAR const char *name, + FAR const char *version); +int pkg_store_format_current_path(FAR char *buffer, size_t size, + FAR const char *name); +int pkg_store_format_previous_path(FAR char *buffer, size_t size, + FAR const char *name); +int pkg_store_format_txn_path(FAR char *buffer, size_t size, + FAR const char *name); +int pkg_store_format_lock_path(FAR char *buffer, size_t size, + FAR const char *name); +int pkg_store_format_download_path(FAR char *buffer, size_t size, + FAR const char *name, + FAR const char *version); +int pkg_store_format_payload_path(FAR char *buffer, size_t size, + FAR const char *name, + FAR const char *version, + FAR const char *artifact); +int pkg_store_format_manifest_path(FAR char *buffer, size_t size, + FAR const char *name, + FAR const char *version); +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); + +const char *pkg_runtime_arch(void); +const char *pkg_runtime_compat(void); +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(FAR struct pkg_index_s *index); +FAR const struct pkg_manifest_s * +pkg_metadata_find_latest(FAR const struct pkg_index_s *index, + FAR const char *name); +int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db); +int pkg_metadata_save_installed(FAR const struct pkg_installed_db_s *db); +FAR struct pkg_installed_entry_s * +pkg_metadata_find_installed(FAR struct pkg_installed_db_s *db, + FAR const char *name); +int pkg_metadata_write_manifest(FAR const char *path, + FAR const struct pkg_manifest_s *manifest); +int pkg_metadata_print_installed(FAR FILE *stream, + FAR const struct pkg_installed_db_s *db); + +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); + +int pkg_install(FAR const char *name); +int pkg_list(FAR FILE *stream); + +void pkg_error(FAR const char *fmt, ...); +void pkg_info(FAR const char *fmt, ...); + +#endif /* __APPS_SYSTEM_NXPKG_PKG_H */ diff --git a/system/nxpkg/pkg_compat.c b/system/nxpkg/pkg_compat.c new file mode 100644 index 00000000000..301fb07aacf --- /dev/null +++ b/system/nxpkg/pkg_compat.c @@ -0,0 +1,64 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_compat.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 +#include + +#include "pkg.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +const char *pkg_runtime_arch(void) +{ + return CONFIG_ARCH; +} + +const char *pkg_runtime_compat(void) +{ + return CONFIG_ARCH_BOARD; +} + +int pkg_compat_check(FAR const struct pkg_manifest_s *manifest) +{ + if (manifest == NULL) + { + return -EINVAL; + } + + if (strcmp(manifest->arch, pkg_runtime_arch()) != 0) + { + return -ENOEXEC; + } + + if (strcmp(manifest->compat, pkg_runtime_compat()) != 0) + { + return -EXDEV; + } + + return 0; +} diff --git a/system/nxpkg/pkg_hash.c b/system/nxpkg/pkg_hash.c new file mode 100644 index 00000000000..8ea1889f554 --- /dev/null +++ b/system/nxpkg/pkg_hash.c @@ -0,0 +1,98 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_hash.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 +#include +#include +#include + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static void pkg_hex_encode(FAR const uint8_t *input, size_t length, + FAR char *output) +{ + static const char g_hex[] = "0123456789abcdef"; + size_t i; + + for (i = 0; i < length; i++) + { + output[i * 2] = g_hex[input[i] >> 4]; + output[i * 2 + 1] = g_hex[input[i] & 0x0f]; + } + + output[length * 2] = '\0'; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int pkg_hash_file_sha256(FAR const char *path, + FAR char digest[PKG_HASH_HEX_LEN + 1]) +{ + SHA2_CTX ctx; + FAR FILE *stream; + uint8_t raw[SHA256_DIGEST_LENGTH]; + uint8_t buffer[512]; + size_t nread; + + stream = fopen(path, "rb"); + if (stream == NULL) + { + return -errno; + } + + sha256init(&ctx); + + for (; ; ) + { + nread = fread(buffer, 1, sizeof(buffer), stream); + if (nread > 0) + { + sha256update(&ctx, buffer, nread); + } + + if (nread < sizeof(buffer)) + { + if (ferror(stream)) + { + fclose(stream); + return -EIO; + } + + break; + } + } + + fclose(stream); + sha256final(raw, &ctx); + pkg_hex_encode(raw, sizeof(raw), digest); + return 0; +} diff --git a/system/nxpkg/pkg_install.c b/system/nxpkg/pkg_install.c new file mode 100644 index 00000000000..12338b068d3 --- /dev/null +++ b/system/nxpkg/pkg_install.c @@ -0,0 +1,493 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_install.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 +#include +#include +#include +#include +#include + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static int pkg_install_resolve_artifact(FAR char *buffer, size_t size, + FAR const struct pkg_manifest_s *manifest) +{ + int ret; + + if (manifest->artifact[0] == '/') + { + ret = snprintf(buffer, size, "%s", manifest->artifact); + if (ret < 0) + { + return ret; + } + + return (size_t)ret >= size ? -ENAMETOOLONG : 0; + } + + ret = snprintf(buffer, size, "%s/%s", PKG_REPO_DIR, manifest->artifact); + if (ret < 0) + { + return ret; + } + + return (size_t)ret >= size ? -ENAMETOOLONG : 0; +} + +static int pkg_install_acquire_lock(FAR const char *name, FAR char *path, + size_t size) +{ + int fd; + int ret; + + ret = pkg_store_ensure_package_root(name); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_format_lock_path(path, size, name); + if (ret < 0) + { + return ret; + } + + fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd < 0) + { + return errno == EEXIST ? -EBUSY : -errno; + } + + close(fd); + return 0; +} + +static bool pkg_install_has_version(FAR const struct pkg_installed_entry_s *entry, + FAR const char *version) +{ + size_t i; + + for (i = 0; i < entry->version_count; i++) + { + if (strcmp(entry->versions[i], version) == 0) + { + return true; + } + } + + return false; +} + +static int pkg_install_add_version(FAR struct pkg_installed_entry_s *entry, + FAR const char *version) +{ + int ret; + + if (pkg_install_has_version(entry, version)) + { + return 0; + } + + if (entry->version_count >= PKG_INSTALLED_VERSIONS_MAX) + { + return -E2BIG; + } + + ret = snprintf(entry->versions[entry->version_count], + sizeof(entry->versions[entry->version_count]), "%s", version); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= sizeof(entry->versions[entry->version_count])) + { + return -ENAMETOOLONG; + } + + entry->version_count++; + return 0; +} + +static int pkg_install_update_installed(FAR struct pkg_installed_db_s *db, + FAR const struct pkg_manifest_s *manifest) +{ + FAR struct pkg_installed_entry_s *entry; + int ret; + + entry = pkg_metadata_find_installed(db, manifest->name); + if (entry == NULL) + { + if (db->count >= PKG_INSTALLED_MAX) + { + return -E2BIG; + } + + entry = &db->entries[db->count++]; + memset(entry, 0, sizeof(*entry)); + ret = snprintf(entry->name, sizeof(entry->name), "%s", manifest->name); + if (ret < 0 || (size_t)ret >= sizeof(entry->name)) + { + return -ENAMETOOLONG; + } + } + + if (entry->current[0] != '\0' && + strcmp(entry->current, manifest->version) != 0) + { + ret = snprintf(entry->previous, sizeof(entry->previous), "%s", + entry->current); + if (ret < 0 || (size_t)ret >= sizeof(entry->previous)) + { + return -ENAMETOOLONG; + } + } + + ret = snprintf(entry->current, sizeof(entry->current), "%s", + manifest->version); + if (ret < 0 || (size_t)ret >= sizeof(entry->current)) + { + return -ENAMETOOLONG; + } + + ret = snprintf(entry->arch, sizeof(entry->arch), "%s", manifest->arch); + if (ret < 0 || (size_t)ret >= sizeof(entry->arch)) + { + return -ENAMETOOLONG; + } + + ret = snprintf(entry->compat, sizeof(entry->compat), "%s", manifest->compat); + if (ret < 0 || (size_t)ret >= sizeof(entry->compat)) + { + return -ENAMETOOLONG; + } + + entry->type = manifest->type; + return pkg_install_add_version(entry, manifest->version); +} + +static int pkg_install_write_pointers(FAR const struct pkg_installed_db_s *db, + FAR const struct pkg_manifest_s *manifest) +{ + FAR struct pkg_installed_entry_s *entry; + char current[PATH_MAX]; + char previous[PATH_MAX]; + int ret; + + entry = pkg_metadata_find_installed((FAR struct pkg_installed_db_s *)db, + manifest->name); + if (entry == NULL) + { + return -ENOENT; + } + + ret = pkg_store_format_current_path(current, sizeof(current), manifest->name); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_format_previous_path(previous, sizeof(previous), + manifest->name); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_write_text_atomic(current, entry->current); + if (ret < 0) + { + return ret; + } + + return pkg_store_write_text_atomic(previous, entry->previous); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int pkg_install(FAR const char *name) +{ + FAR struct pkg_index_s *index; + FAR struct pkg_installed_db_s *installed; + FAR const struct pkg_manifest_s *manifest; + char source[PATH_MAX]; + char tmp[PATH_MAX] = ""; + char payload[PATH_MAX]; + char manifest_path[PATH_MAX]; + char lock[PATH_MAX] = ""; + char digest[PKG_HASH_HEX_LEN + 1]; + int ret; + + index = malloc(sizeof(*index)); + installed = malloc(sizeof(*installed)); + if (index == NULL || installed == NULL) + { + free(index); + free(installed); + pkg_error("unable to allocate package metadata buffers"); + return EXIT_FAILURE; + } + + ret = pkg_store_prepare_layout(); + if (ret < 0) + { + free(index); + free(installed); + pkg_error("unable to prepare package layout: %d", ret); + return EXIT_FAILURE; + } + + ret = pkg_metadata_load_index(index); + if (ret < 0) + { + free(index); + free(installed); + pkg_error("unable to load local index metadata: %d", ret); + return EXIT_FAILURE; + } + + manifest = pkg_metadata_find_latest(index, name); + if (manifest == NULL) + { + free(index); + free(installed); + pkg_error("package '%s' not found in local index", name); + return EXIT_FAILURE; + } + + ret = pkg_install_resolve_artifact(source, sizeof(source), manifest); + if (ret < 0) + { + pkg_error("artifact path for '%s' is too long", name); + return EXIT_FAILURE; + } + + ret = pkg_install_acquire_lock(name, lock, sizeof(lock)); + if (ret < 0) + { + pkg_error("unable to acquire package lock for '%s': %d", name, ret); + return EXIT_FAILURE; + } + + ret = pkg_txn_write_state(name, PKG_TXN_FETCHING); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_format_download_path(tmp, sizeof(tmp), manifest->name, + manifest->version); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_copy_file(source, tmp); + if (ret < 0) + { + goto errout; + } + + ret = pkg_hash_file_sha256(tmp, digest); + if (ret < 0) + { + goto errout; + } + + if (strcasecmp(digest, manifest->sha256) != 0) + { + ret = -EILSEQ; + goto errout; + } + + ret = pkg_txn_write_state(name, PKG_TXN_VERIFIED); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_ensure_version_dir(manifest->name, manifest->version); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_format_payload_path(payload, sizeof(payload), manifest->name, + manifest->version, manifest->artifact); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_copy_file(tmp, payload); + if (ret < 0) + { + goto errout; + } + + ret = pkg_store_format_manifest_path(manifest_path, sizeof(manifest_path), + manifest->name, manifest->version); + if (ret < 0) + { + goto errout; + } + + ret = pkg_metadata_write_manifest(manifest_path, manifest); + if (ret < 0) + { + goto errout; + } + + ret = pkg_txn_write_state(name, PKG_TXN_STAGED); + if (ret < 0) + { + goto errout; + } + + ret = pkg_compat_check(manifest); + if (ret < 0) + { + goto errout; + } + + ret = pkg_txn_write_state(name, PKG_TXN_COMPAT_OK); + if (ret < 0) + { + goto errout; + } + + ret = pkg_metadata_load_installed(installed); + if (ret < 0) + { + goto errout; + } + + ret = pkg_install_update_installed(installed, manifest); + if (ret < 0) + { + goto errout; + } + + ret = pkg_install_write_pointers(installed, manifest); + if (ret < 0) + { + goto errout; + } + + ret = pkg_metadata_save_installed(installed); + if (ret < 0) + { + goto errout; + } + + ret = pkg_txn_write_state(name, PKG_TXN_ACTIVATED); + if (ret < 0) + { + goto errout; + } + + pkg_txn_write_state(name, PKG_TXN_CLEANUP); + if (tmp[0] != '\0') + { + pkg_store_remove_file(tmp); + } + + pkg_txn_clear_state(name); + if (lock[0] != '\0') + { + pkg_store_remove_file(lock); + } + + pkg_info("installed %s version %s", manifest->name, manifest->version); + free(index); + free(installed); + return EXIT_SUCCESS; + +errout: + pkg_txn_write_state(name, PKG_TXN_FAILED); + if (tmp[0] != '\0') + { + pkg_store_remove_file(tmp); + } + + pkg_txn_clear_state(name); + if (lock[0] != '\0') + { + pkg_store_remove_file(lock); + } + + free(index); + free(installed); + pkg_error("install failed for '%s': %d", name, ret); + return EXIT_FAILURE; +} + +int pkg_list(FAR FILE *stream) +{ + FAR struct pkg_installed_db_s *db; + int ret; + + db = malloc(sizeof(*db)); + if (db == NULL) + { + pkg_error("unable to allocate installed metadata buffer"); + return EXIT_FAILURE; + } + + ret = pkg_store_prepare_layout(); + if (ret < 0) + { + free(db); + pkg_error("unable to prepare package layout: %d", ret); + return EXIT_FAILURE; + } + + ret = pkg_metadata_load_installed(db); + if (ret < 0) + { + free(db); + pkg_error("unable to load installed metadata: %d", ret); + return EXIT_FAILURE; + } + + ret = pkg_metadata_print_installed(stream, db); + if (ret < 0) + { + free(db); + pkg_error("unable to print installed metadata: %d", ret); + return EXIT_FAILURE; + } + + free(db); + return EXIT_SUCCESS; +} diff --git a/system/nxpkg/pkg_log.c b/system/nxpkg/pkg_log.c new file mode 100644 index 00000000000..bed06b13ffb --- /dev/null +++ b/system/nxpkg/pkg_log.c @@ -0,0 +1,65 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_log.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 +#include + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static void pkg_vlog(FAR FILE *stream, 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); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +void pkg_error(FAR const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + pkg_vlog(stderr, "error", fmt, ap); + va_end(ap); +} + +void pkg_info(FAR const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + pkg_vlog(stdout, "info", fmt, ap); + va_end(ap); +} diff --git a/system/nxpkg/pkg_main.c b/system/nxpkg/pkg_main.c new file mode 100644 index 00000000000..c049592c5b6 --- /dev/null +++ b/system/nxpkg/pkg_main.c @@ -0,0 +1,106 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_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 +#include +#include +#include + +#include "pkg.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + FAR const char *cmd; + + if (argc < 2) + { + fprintf(stderr, + "Usage: %s [args]\n", + argv[0]); + return EXIT_FAILURE; + } + + cmd = argv[1]; + + if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 || + strcmp(cmd, "-h") == 0) + { + fprintf(stdout, + "Usage: %s [args]\n", + argv[0]); + return EXIT_SUCCESS; + } + + if (strcmp(cmd, "install") == 0) + { + if (argc != 3) + { + pkg_error("install expects exactly one package name"); + fprintf(stderr, + "Usage: %s [args]\n", + argv[0]); + return EXIT_FAILURE; + } + + return pkg_install(argv[2]); + } + + if (strcmp(cmd, "update") == 0) + { + pkg_error("'update' is not implemented yet in the current unit"); + return EXIT_FAILURE; + } + + if (strcmp(cmd, "list") == 0) + { + if (argc != 2) + { + pkg_error("list does not take additional arguments"); + fprintf(stderr, + "Usage: %s [args]\n", + argv[0]); + return EXIT_FAILURE; + } + + return pkg_list(stdout); + } + + if (strcmp(cmd, "rollback") == 0) + { + pkg_error("'rollback' is not implemented yet in the current unit"); + return EXIT_FAILURE; + } + + fprintf(stderr, "ERROR: Unknown subcommand '%s'\n", cmd); + fprintf(stderr, + "Usage: %s [args]\n", + argv[0]); + return EXIT_FAILURE; +} diff --git a/system/nxpkg/pkg_manifest.c b/system/nxpkg/pkg_manifest.c new file mode 100644 index 00000000000..3e7b56a2637 --- /dev/null +++ b/system/nxpkg/pkg_manifest.c @@ -0,0 +1,155 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_manifest.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 +#include +#include + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static bool pkg_has_nonspace(FAR const char *value) +{ + while (*value != '\0') + { + if (!isspace((unsigned char)*value)) + { + return true; + } + + value++; + } + + return false; +} + +static int pkg_validate_required(FAR const char *value) +{ + if (value == NULL || value[0] == '\0' || !pkg_has_nonspace(value)) + { + return -EINVAL; + } + + return 0; +} + +static bool pkg_validate_hex(FAR const char *value) +{ + while (*value != '\0') + { + if (!isxdigit((unsigned char)*value)) + { + return false; + } + + value++; + } + + return true; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +const char *pkg_manifest_type_str(enum pkg_payload_type_e type) +{ + switch (type) + { + case PKG_PAYLOAD_ELF: + return "elf"; + + case PKG_PAYLOAD_SHARED_LIB: + return "shared-lib"; + + default: + return "unknown"; + } +} + +int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest) +{ + if (manifest == NULL) + { + return -EINVAL; + } + + if (pkg_validate_required(manifest->name) < 0 || + pkg_validate_required(manifest->version) < 0 || + pkg_validate_required(manifest->arch) < 0 || + pkg_validate_required(manifest->compat) < 0 || + pkg_validate_required(manifest->artifact) < 0 || + pkg_validate_required(manifest->sha256) < 0) + { + return -EINVAL; + } + + if (strlen(manifest->sha256) != PKG_HASH_HEX_LEN) + { + return -EINVAL; + } + + if (!pkg_validate_hex(manifest->sha256)) + { + return -EINVAL; + } + + if (manifest->type != PKG_PAYLOAD_ELF && + manifest->type != PKG_PAYLOAD_SHARED_LIB) + { + return -EINVAL; + } + + return 0; +} + +int pkg_manifest_parse_type(FAR const char *value, + FAR enum pkg_payload_type_e *type) +{ + if (value == NULL || type == NULL) + { + return -EINVAL; + } + + if (strcmp(value, "elf") == 0) + { + *type = PKG_PAYLOAD_ELF; + return 0; + } + + if (strcmp(value, "shared-lib") == 0 || + strcmp(value, "shared_lib") == 0 || + strcmp(value, "shared") == 0) + { + *type = PKG_PAYLOAD_SHARED_LIB; + return 0; + } + + return -EINVAL; +} diff --git a/system/nxpkg/pkg_metadata.c b/system/nxpkg/pkg_metadata.c new file mode 100644 index 00000000000..9b17c9047a0 --- /dev/null +++ b/system/nxpkg/pkg_metadata.c @@ -0,0 +1,757 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_metadata.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 +#include +#include + +#include "netutils/cJSON.h" + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static int pkg_copy_string(FAR char *dest, size_t size, FAR const char *src) +{ + int ret; + + ret = snprintf(dest, size, "%s", src); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= size) + { + return -ENAMETOOLONG; + } + + return 0; +} + +static bool pkg_string_equal(FAR const char *lhs, size_t lhs_size, + FAR const char *rhs, size_t rhs_size) +{ + size_t lhs_len; + size_t rhs_len; + + lhs_len = strnlen(lhs, lhs_size); + rhs_len = strnlen(rhs, rhs_size); + + if (lhs_len >= lhs_size || rhs_len >= rhs_size) + { + return false; + } + + return lhs_len == rhs_len && strncmp(lhs, rhs, lhs_len) == 0; +} + +static int pkg_string_cmp(FAR const char *lhs, size_t lhs_size, + FAR const char *rhs, size_t rhs_size) +{ + size_t lhs_len; + size_t rhs_len; + size_t cmp_len; + + lhs_len = strnlen(lhs, lhs_size); + rhs_len = strnlen(rhs, rhs_size); + cmp_len = lhs_len > rhs_len ? lhs_len : rhs_len; + + if (lhs_len >= lhs_size || rhs_len >= rhs_size) + { + return strncmp(lhs, rhs, cmp_len); + } + + return strncmp(lhs, rhs, cmp_len + 1); +} + +static FAR cJSON *pkg_metadata_packages_array(FAR cJSON *root) +{ + if (cJSON_IsArray(root)) + { + return root; + } + + return cJSON_GetObjectItemCaseSensitive(root, "packages"); +} + +static int pkg_metadata_parse_manifest(FAR cJSON *item, + FAR struct pkg_manifest_s *manifest) +{ + FAR cJSON *field; + FAR const char *value; + int ret; + + memset(manifest, 0, sizeof(*manifest)); + + field = cJSON_GetObjectItemCaseSensitive(item, "name"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(manifest->name, sizeof(manifest->name), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "version"); + value = cJSON_GetStringValue(field); + if (value == NULL || (ret = pkg_copy_string(manifest->version, + sizeof(manifest->version), + value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "arch"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(manifest->arch, sizeof(manifest->arch), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "compat"); + value = cJSON_GetStringValue(field); + if (value == NULL || (ret = pkg_copy_string(manifest->compat, + sizeof(manifest->compat), + value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "artifact"); + value = cJSON_GetStringValue(field); + if (value == NULL || (ret = pkg_copy_string(manifest->artifact, + sizeof(manifest->artifact), + value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "sha256"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(manifest->sha256, sizeof(manifest->sha256), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "type"); + value = cJSON_GetStringValue(field); + if (pkg_manifest_parse_type(value, &manifest->type) < 0) + { + return -EINVAL; + } + + return pkg_manifest_validate(manifest); +} + +static int pkg_metadata_parse_versions(FAR cJSON *versions, + FAR struct pkg_installed_entry_s *entry) +{ + FAR cJSON *item; + size_t count = 0; + + if (!cJSON_IsArray(versions)) + { + return -EINVAL; + } + + cJSON_ArrayForEach(item, versions) + { + FAR const char *value; + int ret; + + if (count >= PKG_INSTALLED_VERSIONS_MAX) + { + return -E2BIG; + } + + value = cJSON_GetStringValue(item); + if (value == NULL) + { + return -EINVAL; + } + + ret = pkg_copy_string(entry->versions[count], + sizeof(entry->versions[count]), value); + if (ret < 0) + { + return ret; + } + + count++; + } + + entry->version_count = count; + return 0; +} + +static int pkg_metadata_parse_installed_entry(FAR cJSON *item, + FAR struct pkg_installed_entry_s *entry) +{ + FAR cJSON *field; + FAR const char *value; + int ret; + + memset(entry, 0, sizeof(*entry)); + + field = cJSON_GetObjectItemCaseSensitive(item, "name"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(entry->name, sizeof(entry->name), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "current"); + value = cJSON_GetStringValue(field); + if (value == NULL || (ret = pkg_copy_string(entry->current, + sizeof(entry->current), + value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "previous"); + value = cJSON_GetStringValue(field); + if (value != NULL) + { + ret = pkg_copy_string(entry->previous, sizeof(entry->previous), value); + if (ret < 0) + { + return ret; + } + } + + field = cJSON_GetObjectItemCaseSensitive(item, "arch"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(entry->arch, sizeof(entry->arch), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "compat"); + value = cJSON_GetStringValue(field); + if (value == NULL || + (ret = pkg_copy_string(entry->compat, sizeof(entry->compat), value)) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "type"); + value = cJSON_GetStringValue(field); + if (pkg_manifest_parse_type(value, &entry->type) < 0) + { + return -EINVAL; + } + + field = cJSON_GetObjectItemCaseSensitive(item, "versions"); + ret = pkg_metadata_parse_versions(field, entry); + if (ret < 0) + { + return ret; + } + + 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; + + leftnum = strtol(lhs, &leftend, 10); + rightnum = strtol(rhs, &rightend, 10); + + if (leftend != lhs && rightend != rhs) + { + if (leftnum < rightnum) + { + return -1; + } + + if (leftnum > rightnum) + { + return 1; + } + } + else + { + int ret; + + ret = pkg_string_cmp(lhs, PKG_VERSION_MAX + 1, rhs, PKG_VERSION_MAX + 1); + if (ret < 0) + { + return -1; + } + + if (ret > 0) + { + return 1; + } + } + + return 0; +} + +static int pkg_metadata_version_cmp(FAR const char *lhs, FAR const char *rhs) +{ + while (*lhs != '\0' || *rhs != '\0') + { + char leftbuf[16]; + char rightbuf[16]; + size_t leftlen = 0; + size_t rightlen = 0; + int ret; + + while (lhs[leftlen] != '\0' && lhs[leftlen] != '.') + { + if (leftlen + 1 >= sizeof(leftbuf)) + { + return pkg_string_cmp(lhs, PKG_VERSION_MAX + 1, + rhs, PKG_VERSION_MAX + 1); + } + + leftbuf[leftlen] = lhs[leftlen]; + leftlen++; + } + + while (rhs[rightlen] != '\0' && rhs[rightlen] != '.') + { + if (rightlen + 1 >= sizeof(rightbuf)) + { + return pkg_string_cmp(lhs, PKG_VERSION_MAX + 1, + rhs, PKG_VERSION_MAX + 1); + } + + rightbuf[rightlen] = rhs[rightlen]; + rightlen++; + } + + leftbuf[leftlen] = '\0'; + rightbuf[rightlen] = '\0'; + + ret = pkg_metadata_version_token_cmp(leftbuf, rightbuf); + if (ret != 0) + { + return ret; + } + + lhs += leftlen; + rhs += rightlen; + + if (*lhs == '.') + { + lhs++; + } + + if (*rhs == '.') + { + rhs++; + } + } + + return 0; +} + +static FAR cJSON *pkg_metadata_manifest_to_json(FAR const struct pkg_manifest_s *manifest) +{ + FAR cJSON *root; + + root = cJSON_CreateObject(); + if (root == NULL) + { + return NULL; + } + + cJSON_AddStringToObject(root, "name", manifest->name); + cJSON_AddStringToObject(root, "version", manifest->version); + cJSON_AddStringToObject(root, "arch", manifest->arch); + cJSON_AddStringToObject(root, "compat", manifest->compat); + cJSON_AddStringToObject(root, "artifact", manifest->artifact); + cJSON_AddStringToObject(root, "sha256", manifest->sha256); + cJSON_AddStringToObject(root, "type", pkg_manifest_type_str(manifest->type)); + return root; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int pkg_metadata_load_index(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; + } + + packages = pkg_metadata_packages_array(root); + if (!cJSON_IsArray(packages)) + { + cJSON_Delete(root); + return -EINVAL; + } + + cJSON_ArrayForEach(item, packages) + { + if (count >= PKG_INDEX_MAX) + { + cJSON_Delete(root); + return -E2BIG; + } + + ret = pkg_metadata_parse_manifest(item, &index->manifests[count]); + if (ret < 0) + { + cJSON_Delete(root); + return ret; + } + + pkg_info("parsed manifest %s %s", + index->manifests[count].name, + index->manifests[count].version); + count++; + } + + index->count = count; + cJSON_Delete(root); + return 0; +} + +FAR const struct pkg_manifest_s * +pkg_metadata_find_latest(FAR const struct pkg_index_s *index, + FAR const char *name) +{ + FAR const struct pkg_manifest_s *best = NULL; + FAR const char *arch; + FAR const char *compat; + size_t i; + + if (index == NULL || name == NULL) + { + return NULL; + } + + arch = pkg_runtime_arch(); + compat = pkg_runtime_compat(); + + for (i = 0; i < index->count; i++) + { + FAR const struct pkg_manifest_s *candidate = &index->manifests[i]; + + if (!pkg_string_equal(candidate->name, sizeof(candidate->name), + name, PKG_NAME_MAX + 1)) + { + continue; + } + + if (!pkg_string_equal(candidate->arch, sizeof(candidate->arch), + arch, PKG_ARCH_MAX + 1) || + !pkg_string_equal(candidate->compat, sizeof(candidate->compat), + compat, PKG_COMPAT_MAX + 1)) + { + continue; + } + + if (best == NULL || + pkg_metadata_version_cmp(candidate->version, best->version) > 0) + { + best = candidate; + } + } + + return best; +} + +int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db) +{ + FAR cJSON *root; + FAR cJSON *packages; + FAR cJSON *item; + FAR char *text; + char path[PATH_MAX]; + size_t count = 0; + int ret; + + if (db == NULL) + { + return -EINVAL; + } + + memset(db, 0, sizeof(*db)); + + ret = pkg_store_format_installed_path(path, sizeof(path)); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_read_text(path, &text); + if (ret == -ENOENT) + { + return 0; + } + + if (ret < 0) + { + return ret; + } + + root = cJSON_Parse(text); + free(text); + if (root == NULL) + { + return -EINVAL; + } + + packages = pkg_metadata_packages_array(root); + if (!cJSON_IsArray(packages)) + { + cJSON_Delete(root); + return -EINVAL; + } + + cJSON_ArrayForEach(item, packages) + { + if (count >= PKG_INSTALLED_MAX) + { + cJSON_Delete(root); + return -E2BIG; + } + + ret = pkg_metadata_parse_installed_entry(item, &db->entries[count]); + if (ret < 0) + { + cJSON_Delete(root); + return ret; + } + + count++; + } + + db->count = count; + cJSON_Delete(root); + return 0; +} + +int pkg_metadata_save_installed(FAR const struct pkg_installed_db_s *db) +{ + FAR cJSON *root; + FAR cJSON *packages; + FAR char *text; + char path[PATH_MAX]; + size_t i; + int ret; + + if (db == NULL) + { + return -EINVAL; + } + + root = cJSON_CreateObject(); + if (root == NULL) + { + return -ENOMEM; + } + + packages = cJSON_AddArrayToObject(root, "packages"); + if (packages == NULL) + { + cJSON_Delete(root); + return -ENOMEM; + } + + for (i = 0; i < db->count; i++) + { + FAR const struct pkg_installed_entry_s *entry = &db->entries[i]; + FAR cJSON *item = cJSON_CreateObject(); + FAR cJSON *versions = cJSON_AddArrayToObject(item, "versions"); + size_t j; + + if (item == NULL || versions == NULL) + { + cJSON_Delete(root); + return -ENOMEM; + } + + cJSON_AddStringToObject(item, "name", entry->name); + cJSON_AddStringToObject(item, "current", entry->current); + cJSON_AddStringToObject(item, "previous", entry->previous); + cJSON_AddStringToObject(item, "arch", entry->arch); + cJSON_AddStringToObject(item, "compat", entry->compat); + cJSON_AddStringToObject(item, "type", + pkg_manifest_type_str(entry->type)); + + for (j = 0; j < entry->version_count; j++) + { + cJSON_AddItemToArray(versions, + cJSON_CreateString(entry->versions[j])); + } + + cJSON_AddItemToArray(packages, item); + } + + text = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (text == NULL) + { + return -ENOMEM; + } + + ret = pkg_store_format_installed_path(path, sizeof(path)); + if (ret < 0) + { + cJSON_free(text); + return ret; + } + + ret = pkg_store_write_text_atomic(path, text); + cJSON_free(text); + return ret; +} + +FAR struct pkg_installed_entry_s * +pkg_metadata_find_installed(FAR struct pkg_installed_db_s *db, + FAR const char *name) +{ + size_t i; + + if (db == NULL || name == NULL) + { + return NULL; + } + + for (i = 0; i < db->count; i++) + { + if (pkg_string_equal(db->entries[i].name, sizeof(db->entries[i].name), + name, PKG_NAME_MAX + 1)) + { + return &db->entries[i]; + } + } + + return NULL; +} + +int pkg_metadata_write_manifest(FAR const char *path, + FAR const struct pkg_manifest_s *manifest) +{ + FAR cJSON *root; + FAR char *text; + int ret; + + root = pkg_metadata_manifest_to_json(manifest); + if (root == NULL) + { + return -ENOMEM; + } + + text = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (text == NULL) + { + return -ENOMEM; + } + + ret = pkg_store_write_text_atomic(path, text); + cJSON_free(text); + return ret; +} + +int pkg_metadata_print_installed(FAR FILE *stream, + FAR const struct pkg_installed_db_s *db) +{ + size_t i; + + if (stream == NULL || db == NULL) + { + return -EINVAL; + } + + for (i = 0; i < db->count; i++) + { + FAR const struct pkg_installed_entry_s *entry = &db->entries[i]; + size_t j; + + fprintf(stream, + "%s current=%s previous=%s type=%s arch=%s compat=%s versions=", + entry->name, + entry->current[0] != '\0' ? entry->current : "-", + entry->previous[0] != '\0' ? entry->previous : "-", + pkg_manifest_type_str(entry->type), entry->arch, entry->compat); + + for (j = 0; j < entry->version_count; j++) + { + fprintf(stream, "%s%s", j == 0 ? "" : ",", entry->versions[j]); + } + + fputc('\n', stream); + } + + return 0; +} diff --git a/system/nxpkg/pkg_store.c b/system/nxpkg/pkg_store.c new file mode 100644 index 00000000000..84680ef3a12 --- /dev/null +++ b/system/nxpkg/pkg_store.c @@ -0,0 +1,496 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_store.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 +#include +#include +#include +#include +#include +#include +#include + +#include "pkg.h" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static int pkg_store_format(FAR char *buffer, size_t size, + FAR const char *fmt, + FAR const char *name, + FAR const char *version) +{ + int ret; + + ret = snprintf(buffer, size, fmt, name, version); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= size) + { + return -ENAMETOOLONG; + } + + return 0; +} + +static int pkg_store_mkdir(FAR const char *path) +{ + struct stat st; + int ret; + + ret = stat(path, &st); + if (ret == 0) + { + if (!S_ISDIR(st.st_mode)) + { + return -ENOTDIR; + } + + return 0; + } + + if (errno != ENOENT) + { + return -errno; + } + + ret = mkdir(path, 0755); + if (ret < 0 && errno != EEXIST) + { + return -errno; + } + + return 0; +} + +static int pkg_store_mkdirs(FAR const char *path) +{ + char buffer[PATH_MAX]; + FAR char *cursor; + int ret; + + ret = snprintf(buffer, sizeof(buffer), "%s", path); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= sizeof(buffer)) + { + return -ENAMETOOLONG; + } + + for (cursor = buffer + 1; *cursor != '\0'; cursor++) + { + if (*cursor != '/') + { + continue; + } + + *cursor = '\0'; + ret = pkg_store_mkdir(buffer); + *cursor = '/'; + if (ret < 0) + { + return ret; + } + } + + return pkg_store_mkdir(buffer); +} + +static int pkg_store_write_all(int fd, FAR const char *buffer, size_t length) +{ + size_t offset = 0; + + while (offset < length) + { + ssize_t ret; + + ret = write(fd, buffer + offset, length - offset); + if (ret < 0) + { + if (errno == EINTR) + { + continue; + } + + return -errno; + } + + offset += (size_t)ret; + } + + return 0; +} + +static FAR const char *pkg_store_basename(FAR const char *path) +{ + FAR const char *leaf; + + leaf = strrchr(path, '/'); + return leaf != NULL ? leaf + 1 : path; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int pkg_store_prepare_layout(void) +{ + int ret; + + ret = pkg_store_mkdirs(PKG_REPO_DIR); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_mkdirs(PKG_STORE_DIR); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_mkdirs(PKG_TMP_DIR); + if (ret < 0) + { + return ret; + } + + return pkg_store_mkdirs(PKG_TMP_PKG_DIR); +} + +int pkg_store_ensure_package_root(FAR const char *name) +{ + char path[PATH_MAX]; + int ret; + + ret = pkg_store_format_package_root(path, sizeof(path), name); + if (ret < 0) + { + return ret; + } + + return pkg_store_mkdirs(path); +} + +int pkg_store_ensure_version_dir(FAR const char *name, + FAR const char *version) +{ + char path[PATH_MAX]; + int ret; + + ret = pkg_store_ensure_package_root(name); + if (ret < 0) + { + return ret; + } + + ret = pkg_store_format_version_path(path, sizeof(path), name, version); + if (ret < 0) + { + return ret; + } + + return pkg_store_mkdirs(path); +} + +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_installed_path(FAR char *buffer, size_t size) +{ + return pkg_store_format(buffer, size, "%s", PKG_REPO_INSTALLED, ""); +} + +int pkg_store_format_package_root(FAR char *buffer, size_t size, + FAR const char *name) +{ + return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s", name, ""); +} + +int pkg_store_format_version_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", name, version); +} + +int pkg_store_format_current_path(FAR char *buffer, size_t size, + FAR const char *name) +{ + return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/current", name, ""); +} + +int pkg_store_format_previous_path(FAR char *buffer, size_t size, + FAR const char *name) +{ + return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/previous", name, + ""); +} + +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, ""); +} + +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, ""); +} + +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 pkg_store_format_payload_path(FAR char *buffer, size_t size, + FAR const char *name, + FAR const char *version, + FAR const char *artifact) +{ + FAR const char *leaf; + int ret; + + leaf = pkg_store_basename(artifact); + ret = snprintf(buffer, size, PKG_STORE_DIR "/%s/%s/%s", name, version, leaf); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= size) + { + return -ENAMETOOLONG; + } + + return 0; +} + +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", + name, version); +} + +int pkg_store_read_text(FAR const char *path, FAR char **buffer) +{ + FAR FILE *stream; + FAR char *data; + long length; + size_t nread; + + if (buffer == NULL) + { + return -EINVAL; + } + + *buffer = NULL; + + stream = fopen(path, "rb"); + if (stream == NULL) + { + return errno == ENOENT ? -ENOENT : -errno; + } + + if (fseek(stream, 0, SEEK_END) < 0) + { + fclose(stream); + return -errno; + } + + length = ftell(stream); + if (length < 0) + { + fclose(stream); + return -errno; + } + + if (fseek(stream, 0, SEEK_SET) < 0) + { + fclose(stream); + return -errno; + } + + data = malloc((size_t)length + 1); + if (data == NULL) + { + fclose(stream); + return -ENOMEM; + } + + nread = fread(data, 1, (size_t)length, stream); + if (nread != (size_t)length) + { + int err = ferror(stream); + + fclose(stream); + free(data); + return err ? -EIO : -EINVAL; + } + + fclose(stream); + + data[length] = '\0'; + *buffer = data; + return 0; +} + +int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text) +{ + char tmp[PATH_MAX]; + int fd; + int ret; + + ret = snprintf(tmp, sizeof(tmp), "%s.tmp", path); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= sizeof(tmp)) + { + return -ENAMETOOLONG; + } + + fd = open(tmp, 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(tmp); + return ret; + } + + if (close(fd) < 0) + { + unlink(tmp); + return -errno; + } + + if (rename(tmp, path) < 0) + { + unlink(tmp); + return -errno; + } + + return 0; +} + +int pkg_store_copy_file(FAR const char *src, FAR const char *dest) +{ + int infd; + int outfd; + int ret; + char buffer[512]; + + infd = open(src, O_RDONLY); + if (infd < 0) + { + return -errno; + } + + outfd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (outfd < 0) + { + ret = -errno; + close(infd); + return ret; + } + + for (; ; ) + { + ssize_t nread; + + nread = read(infd, buffer, sizeof(buffer)); + if (nread < 0) + { + if (errno == EINTR) + { + continue; + } + + ret = -errno; + goto errout; + } + + if (nread == 0) + { + break; + } + + ret = pkg_store_write_all(outfd, buffer, (size_t)nread); + if (ret < 0) + { + goto errout; + } + } + + close(infd); + + if (close(outfd) < 0) + { + unlink(dest); + return -errno; + } + + return 0; + +errout: + close(infd); + close(outfd); + unlink(dest); + return ret; +} + +int pkg_store_remove_file(FAR const char *path) +{ + if (unlink(path) < 0) + { + return errno == ENOENT ? 0 : -errno; + } + + return 0; +} diff --git a/system/nxpkg/pkg_txn.c b/system/nxpkg/pkg_txn.c new file mode 100644 index 00000000000..e20a8700f44 --- /dev/null +++ b/system/nxpkg/pkg_txn.c @@ -0,0 +1,110 @@ +/**************************************************************************** + * apps/system/nxpkg/pkg_txn.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 +#include + +#include "pkg.h" + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +const char *pkg_txn_state_str(enum pkg_txn_state_e state) +{ + switch (state) + { + case PKG_TXN_IDLE: + return "IDLE"; + + case PKG_TXN_FETCHING: + return "FETCHING"; + + case PKG_TXN_VERIFIED: + return "VERIFIED"; + + case PKG_TXN_STAGED: + return "STAGED"; + + case PKG_TXN_COMPAT_OK: + return "COMPAT_OK"; + + case PKG_TXN_ACTIVATED: + return "ACTIVATED"; + + case PKG_TXN_CLEANUP: + return "CLEANUP"; + + case PKG_TXN_FAILED: + return "FAILED"; + + case PKG_TXN_RESTORE: + return "RESTORE"; + + default: + return "UNKNOWN"; + } +} + +int pkg_txn_write_state(FAR const char *name, enum pkg_txn_state_e state) +{ + char path[PATH_MAX]; + char text[32]; + int ret; + + ret = pkg_store_format_txn_path(path, sizeof(path), name); + if (ret < 0) + { + return ret; + } + + ret = snprintf(text, sizeof(text), "%s\n", pkg_txn_state_str(state)); + if (ret < 0) + { + return ret; + } + + if ((size_t)ret >= sizeof(text)) + { + return -ENAMETOOLONG; + } + + return pkg_store_write_text_atomic(path, text); +} + +int pkg_txn_clear_state(FAR const char *name) +{ + char path[PATH_MAX]; + int ret; + + ret = pkg_store_format_txn_path(path, sizeof(path), name); + if (ret < 0) + { + return ret; + } + + return pkg_store_remove_file(path); +} diff --git a/tools/export_pkg_repo.py b/tools/export_pkg_repo.py new file mode 100644 index 00000000000..719312664b3 --- /dev/null +++ b/tools/export_pkg_repo.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# +# 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. + +"""Export built binaries into a package repository layout. + +The script copies built application or library artifacts into a server-friendly +repository directory, computes SHA-256 digests, and emits an `index.json` +compatible with the current `nxpkg` metadata parser. + +The repository can then be served by FTP, HTTP, or any static file transport. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import Dict +from typing import List + + +VALID_TYPES = {"elf", "shared-lib"} + + +@dataclass +class PackageSpec: + name: str + version: str + payload_type: str + source: Path + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as infile: + while True: + chunk = infile.read(65536) + if not chunk: + break + + digest.update(chunk) + + return digest.hexdigest() + + +def parse_package_spec(value: str) -> PackageSpec: + parts = value.split(":", 3) + if len(parts) != 4: + raise argparse.ArgumentTypeError( + "package must look like :::" + ) + + name, version, payload_type, source = parts + if payload_type not in VALID_TYPES: + raise argparse.ArgumentTypeError( + f"unsupported package type '{payload_type}', expected one of " + f"{', '.join(sorted(VALID_TYPES))}" + ) + + source_path = Path(source).expanduser().resolve() + if not source_path.is_file(): + raise argparse.ArgumentTypeError(f"source does not exist: {source_path}") + + return PackageSpec(name=name, version=version, payload_type=payload_type, + source=source_path) + + +def artifact_relpath(arch: str, chip: str, compat: str, + spec: PackageSpec) -> Path: + filename = spec.source.name + return (Path("artifacts") / arch / chip / compat / + spec.name / spec.version / filename) + + +def package_identity(package: Dict[str, str]) -> tuple: + return ( + package["name"], + package["version"], + package["arch"], + package["compat"], + package["type"], + ) + + +def load_existing_packages(repo_dir: Path) -> List[dict]: + index_path = repo_dir / "index.json" + + if not index_path.exists(): + return [] + + root = json.loads(index_path.read_text(encoding="utf-8")) + if isinstance(root, list): + packages = root + else: + packages = root.get("packages") + + if not isinstance(packages, list): + raise ValueError(f"invalid repository index format in {index_path}") + + for item in packages: + if not isinstance(item, dict): + raise ValueError(f"invalid repository index entry in {index_path}") + + return packages + + +def emit_index(repo_dir: Path, packages: List[dict]) -> None: + index_path = repo_dir / "index.json" + payload = {"packages": packages} + index_path.write_text(json.dumps(payload, indent=2, sort_keys=False) + "\n", + encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("repo_dir", type=Path, + help="Destination repository directory") + parser.add_argument("--arch", required=True, + help="Target architecture string, for example xtensa") + parser.add_argument("--chip", required=True, + help="Target chip/family string, for example esp32s3") + parser.add_argument("--compat", required=True, + help="Target board/runtime identity, for example esp32s3-xiao") + parser.add_argument("--artifact-prefix", default="", + help="Optional prefix prepended to artifact paths in index.json") + parser.add_argument("--package", action="append", required=True, + type=parse_package_spec, + help=("Package spec in the form " + ":::")) + args = parser.parse_args() + + repo_dir = args.repo_dir.expanduser().resolve() + repo_dir.mkdir(parents=True, exist_ok=True) + + packages = load_existing_packages(repo_dir) + packages_by_id = {package_identity(package): package for package in packages} + prefix = args.artifact_prefix.rstrip("/") + + for spec in args.package: + relpath = artifact_relpath(args.arch, args.chip, args.compat, spec) + destination = repo_dir / relpath + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(spec.source, destination) + + artifact = relpath.as_posix() + if prefix: + artifact = f"{prefix}/{artifact}" + + package = { + "name": spec.name, + "version": spec.version, + "arch": args.arch, + "compat": args.compat, + "artifact": artifact, + "sha256": sha256_file(destination), + "type": spec.payload_type, + } + packages_by_id[package_identity(package)] = package + + packages = sorted( + packages_by_id.values(), + key=lambda item: ( + item["name"], + item["version"], + item["arch"], + item["compat"], + item["type"], + ), + ) + emit_index(repo_dir, packages) + + print(f"exported {len(packages)} package(s) to {repo_dir}") + for package in packages: + print( + f"- {package['name']} {package['version']} " + f"[{package['type']}] -> {package['artifact']}" + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())