Compare commits

...
4 Commits
12 changed files with 1142 additions and 70 deletions

No files matched your search

+3 -3
View File
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
endif
endif
VERSION_TAG := v1.6
VERSION_TAG := v1.7
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
@@ -24,7 +24,7 @@ HOST_PKG_CONFIG ?= pkg-config
BIN := web-file-mgr.elf
LINUX_BIN := web-file-mgr-linux
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
LINUX_SRCS := $(COMMON_SRCS)
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
@@ -39,7 +39,7 @@ CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
LDFLAGS := -Wl,--gc-sections
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
LDADD += -lSceIpmi -lSceAppInstUtil
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
+3 -1
View File
@@ -39,7 +39,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
- Home screen launcher icon and browser favicon use the same embedded `icon0.png` data in the PS5 build to avoid storing the icon twice in the ELF.
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`, `.shn`.
- Preview image files ending with `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
- Install PKG files ends with `.pkg`.
- Install/Preview PKG files ends with `.pkg`.
## Build
@@ -128,6 +128,8 @@ This project was built with reference to these projects:
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behavior. License: GPLv3.
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. It is licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
- **[ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk):** Payload building foundation. License: GPLv3+.
- **[etaHEN](https://github.com/etaHEN/etaHEN):** ShellUI URI navigation used to return to the PS5 home screen before exit. License: GPLv3.
- **[ezremote](https://github.com/cy33hc/ps5-ezremote-client):** Preview PKG info. License: GPLv2.
## License
+16
View File
@@ -123,6 +123,22 @@
</section>
</div>
<div id="pkgInfoOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel pkg-info-panel" role="dialog" aria-modal="true" aria-labelledby="pkgInfoTitle">
<h2 id="pkgInfoTitle" class="pkg-info-title" data-i18n="pkgInfoTitle"></h2>
<div class="pkg-info-content">
<div class="pkg-info-image-stage">
<img id="pkgInfoImage" class="pkg-info-image" src="/icon-pkg.png" alt="">
</div>
<div id="pkgInfoFields" class="pkg-info-fields"></div>
</div>
<div class="text-editor-actions">
<button id="pkgInfoCloseBtn" class="secondary" data-i18n="close"></button>
<button id="pkgInfoInstallBtn" class="primary" data-i18n="install"></button>
</div>
</section>
</div>
<div id="permissionOverlay" class="permission-overlay" hidden>
<section class="permission-panel" role="dialog" aria-modal="true" aria-labelledby="permissionTitle">
<h2 id="permissionTitle" class="permission-title" data-i18n="permissionsTitle"></h2>
+4
View File
@@ -17,12 +17,15 @@ window.WFM_LANG = {
save: "Save",
apply: "Apply",
exit: "Exit",
exitConfirm: "Exit and stop the file manager process?",
exiting: "Exiting...",
refresh: "Refresh",
mkdir: "New Folder",
newText: "New Text",
install: "Install",
installPackage: "Install package",
pkgInfoTitle: "Package Information",
pkgInfoLoading: "Reading package information...",
pkgInstalling: "Starting package installation: {name}...",
pkgInstallStarted: "Package installation started: {name}",
file: "File",
@@ -144,6 +147,7 @@ window.WFM_LANG = {
err_invalid_mode: "Invalid permission mode",
err_chmod_failed: "Cannot change permissions: {path}",
err_pkg_type_invalid: "Only .pkg files can be installed",
err_pkg_info_failed: "Could not read package information: {path}",
err_pkg_install_failed: "Package installation failed ({arg})",
err_pkg_install_unsupported: "Package installation is only available on PS5",
err_file_not_found: "File not found",
+4
View File
@@ -17,12 +17,15 @@ window.WFM_LANG = {
save: "保存",
apply: "应用",
exit: "退出",
exitConfirm: "是否退出并关闭文件管理器进程?",
exiting: "正在退出...",
refresh: "刷新",
mkdir: "新建目录",
newText: "新建文本",
install: "安装",
installPackage: "安装 PKG",
pkgInfoTitle: "PKG 信息",
pkgInfoLoading: "正在读取 PKG 信息...",
pkgInstalling: "正在提交安装:{name}...",
pkgInstallStarted: "已开始安装:{name}",
file: "文件",
@@ -144,6 +147,7 @@ window.WFM_LANG = {
err_invalid_mode: "权限格式无效",
err_chmod_failed: "无法修改权限:{path}",
err_pkg_type_invalid: "只能安装 .pkg 文件",
err_pkg_info_failed: "无法读取 PKG 信息:{path}",
err_pkg_install_failed: "PKG 安装失败({arg})",
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
err_file_not_found: "文件不存在",
+96
View File
@@ -818,6 +818,78 @@ input[type="checkbox"] {
margin-top: 14px;
}
.pkg-info-panel {
width: 1100px;
height: auto;
max-height: calc(100vh - 96px);
}
.pkg-info-title {
margin: 0 0 18px;
color: #edf0f2;
font-size: 26px;
font-weight: 600;
}
.pkg-info-content {
min-height: 0;
display: flex;
flex: 1 1 auto;
margin-bottom: 18px;
}
.pkg-info-image-stage {
width: 340px;
flex: 0 0 340px;
display: flex;
align-items: flex-start;
justify-content: center;
margin-right: 24px;
}
.pkg-info-image {
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 340px;
pointer-events: none;
-webkit-user-drag: none;
user-select: none;
}
.pkg-info-fields {
min-width: 0;
min-height: 80px;
flex: 1 1 auto;
margin-bottom: 0;
overflow-x: hidden;
overflow-y: auto;
border-top: 1px solid #2d343d;
}
.pkg-info-row {
display: flex;
padding: 10px 0;
border-bottom: 1px solid #2d343d;
font-size: 18px;
}
.pkg-info-key {
width: 244px;
flex: 0 0 244px;
padding-right: 24px;
color: #8f9ba6;
word-break: break-all;
}
.pkg-info-value {
min-width: 0;
flex: 1 1 auto;
color: #d9e0e6;
word-break: break-all;
}
.permission-overlay {
position: fixed;
top: 0;
@@ -1147,6 +1219,30 @@ input[type="checkbox"] {
background: #8a929b;
}
@media (max-width: 900px) {
.pkg-info-panel {
width: 760px;
}
.pkg-info-content {
display: block;
}
.pkg-info-image-stage {
width: auto;
height: 260px;
margin: 0 0 18px;
}
.pkg-info-image {
max-height: 260px;
}
.pkg-info-fields {
margin-bottom: 18px;
}
}
@media (max-width: 815px) {
.top-left {
padding: 0 40px 0 0;
+172 -41
View File
@@ -28,12 +28,14 @@ let textEditorBusy = false;
let permissionItems = [];
let permissionOriginal = "";
let permissionBusy = false;
let pkgInfoItem = null;
let pkgInfoRequestId = 0;
let downloadFrame = null;
let uploadXhr = null;
let uploadTerminalAbort = false;
let L = {};
const APP_VERSION = "v1.6";
const APP_VERSION = "v1.7";
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
const SORT_KEY = "ps5-web-file-mgr:list-sort";
const LOADING_DISPLAY_DELAY = 250;
@@ -84,6 +86,12 @@ const imagePreviewOverlayEl = document.getElementById("imagePreviewOverlay");
const imagePreviewNameEl = document.getElementById("imagePreviewName");
const imagePreviewEl = document.getElementById("imagePreview");
const imagePreviewCloseBtn = document.getElementById("imagePreviewCloseBtn");
const pkgInfoOverlayEl = document.getElementById("pkgInfoOverlay");
const pkgInfoImageEl = document.getElementById("pkgInfoImage");
const pkgInfoTitleEl = document.getElementById("pkgInfoTitle");
const pkgInfoFieldsEl = document.getElementById("pkgInfoFields");
const pkgInfoCloseBtn = document.getElementById("pkgInfoCloseBtn");
const pkgInfoInstallBtn = document.getElementById("pkgInfoInstallBtn");
const permissionOverlayEl = document.getElementById("permissionOverlay");
const permissionPathEl = document.getElementById("permissionPath");
const permissionModeEl = document.getElementById("permissionMode");
@@ -338,21 +346,25 @@ function displayPath(path) {
return decodeFsText(path);
}
function renderAddressPath() {
const path = displayPath(cwd);
pathEl.title = path;
pathEl.textContent = path;
if (pathEl.scrollWidth <= pathEl.clientWidth) return;
function renderEndOfPath(element, path) {
element.textContent = path;
if (element.scrollWidth <= element.clientWidth) return;
let low = 0;
let high = path.length;
while (low < high) {
const count = Math.ceil((low + high) / 2);
pathEl.textContent = "..." + path.slice(-count);
if (pathEl.scrollWidth <= pathEl.clientWidth) low = count;
element.textContent = "..." + path.slice(path.length - count);
if (element.scrollWidth <= element.clientWidth) low = count;
else high = count - 1;
}
pathEl.textContent = "..." + path.slice(-low);
element.textContent = "..." + path.slice(path.length - low);
}
function renderAddressPath() {
const path = displayPath(cwd);
pathEl.title = path;
renderEndOfPath(pathEl, path);
}
async function fetchText(path, params) {
@@ -420,11 +432,11 @@ function taskElapsed(task) {
}
function opLabel(op) {
return { copy: t("copy"), move: t("move"), delete: t("delete"), chmod: t("permissionsTitle"), download: t("download"), upload: t("upload") }[op] || op;
return { copy: t("copy"), move: t("move"), delete: t("delete"), chmod: t("permissionsTitle"), download: t("download"), upload: t("upload"), pkg_install: t("installPackage") }[op] || op;
}
function taskOpLabel(op) {
return { copy: t("copying"), move: t("moving"), delete: t("deleting"), chmod: t("changingPermissions"), download: t("downloading"), upload: t("uploading") }[op] || op;
return { copy: t("copying"), move: t("moving"), delete: t("deleting"), chmod: t("changingPermissions"), download: t("downloading"), upload: t("uploading"), pkg_install: t("installPackage") }[op] || op;
}
function isPlayStationBrowser() {
@@ -479,6 +491,14 @@ function taskFailureMessage(task) {
}
function handleTerminalTask(task) {
if (task.op === "pkg_install") {
if (task.state === "failed") {
const message = taskFailureMessage(task);
setStatus(message);
alert(message);
}
return;
}
if (task.state === "failed") {
clearTrackedTask();
const message = taskFailureMessage(task);
@@ -593,7 +613,7 @@ function historyBlocked() {
pendingOverlayText || taskOverlayTimer || !overlayEl.hidden ||
!contentLoadingEl.hidden || contentEl.classList.contains("loading") ||
!textEditorOverlayEl.hidden || !imagePreviewOverlayEl.hidden ||
!permissionOverlayEl.hidden);
!pkgInfoOverlayEl.hidden || !permissionOverlayEl.hidden);
}
function savePath(path) {
@@ -729,40 +749,32 @@ function itemTypeLabel(item) {
return t("file");
}
async function installPkg(item) {
if (busy || loadingPath) return;
async function queuePkgInstall(items) {
if (busy || loadingPath || !items.length) return;
try {
localActionBusy = true;
setBusy(true);
setStatus(t("pkgInstalling", { name: displayName(item) }));
await api("/api/install-pkg", { path: item.path });
setStatus(t("pkgInstallStarted", { name: displayName(item) }));
setStatus(t("pkgInstalling", { name: itemTitle(items) }));
await apiForm("/api/install-pkg", {
paths: items.map(item => item.path).join("\n")
});
clearSelection();
setStatus(t("pkgInstallStarted", { name: itemTitle(items) }));
await pollTasks();
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
if (overlayEl.hidden && !taskOverlayTimer) setBusy(false);
}
}
async function actionInstallSelectedPkgs() {
if (busy || loadingPath) return;
const items = selectedEntries().filter(isPkgPackage);
if (!items.length) return;
try {
localActionBusy = true;
setBusy(true);
for (let i = 0; i < items.length; i++) {
setStatus(t("pkgInstalling", { name: displayName(items[i]) }));
await api("/api/install-pkg", { path: items[i].path });
}
setStatus(t("pkgInstallStarted", { name: itemTitle(items) }));
} catch (err) {
showActionFailed(t("installPackage"), err.message);
} finally {
localActionBusy = false;
setBusy(false);
}
function installPkg(item) {
return queuePkgInstall([item]);
}
function actionInstallSelectedPkgs() {
return queuePkgInstall(selectedEntries().filter(isPkgPackage));
}
function openImagePreview(item) {
@@ -783,6 +795,98 @@ function closeImagePreview() {
setModalBackgroundLocked(false);
}
function addPkgInfoRow(name, value) {
if (value === undefined || value === null || String(value) === "") return;
const row = document.createElement("div");
row.className = "pkg-info-row";
const key = document.createElement("div");
key.className = "pkg-info-key";
key.textContent = name;
const text = document.createElement("div");
text.className = "pkg-info-value";
text.textContent = String(value);
appendChildren(row, key, text);
pkgInfoFieldsEl.appendChild(row);
}
function pkgHex(value) {
return "0x" + ("00000000" + (Number(value || 0) >>> 0).toString(16).toUpperCase()).slice(-8);
}
async function openPkgInfo(item) {
if (busy || loadingPath) return;
const requestId = ++pkgInfoRequestId;
pkgInfoItem = item;
setModalBackgroundLocked(true);
pkgInfoFieldsEl.innerHTML = "";
pkgInfoImageEl.src = "/icon-pkg.png";
pkgInfoInstallBtn.disabled = true;
setStatus(t("pkgInfoLoading"));
try {
const data = await api("/api/pkg-info", { path: item.path }, { method: "GET" });
if (requestId !== pkgInfoRequestId || pkgInfoItem !== item) return;
pkgInfoFieldsEl.innerHTML = "";
const platform = data.platform === "PS5" ? "PS5" : "PS4";
pkgInfoTitleEl.textContent = platform + " " + t("pkgInfoTitle");
const fields = data.fields || [];
const priority = [
"TITLE", "titleName", "TITLE_ID", "titleId",
"VERSION", "masterVersion", "APP_VER", "contentVersion"
];
const shown = {};
for (const name of priority) {
const field = fields.find(item => item.name === name);
if (field) {
addPkgInfoRow(field.name, field.value);
shown[name] = true;
}
}
addPkgInfoRow("PKG_SIZE", formatBytes(data.size, true));
for (const field of fields) {
if (!shown[field.name]) addPkgInfoRow(field.name, field.value);
}
if (!fields.some(field => field.name === "CONTENT_ID" || field.name === "contentId")) {
addPkgInfoRow("PKG_CONTENT_ID", data.content_id);
}
addPkgInfoRow("PKG_CONTENT_TYPE", pkgHex(data.content_type));
addPkgInfoRow("PKG_CONTENT_FLAGS", pkgHex(data.content_flags));
if (data.has_icon) {
pkgInfoImageEl.src = "/api/pkg-icon?path=" + encodeURIComponent(item.path) +
"&mtime=" + encodeURIComponent(item.mtime || 0);
}
pkgInfoInstallBtn.disabled = false;
pkgInfoOverlayEl.hidden = false;
pkgInfoFieldsEl.scrollTop = 0;
pkgInfoInstallBtn.focus();
await nextPaint();
if (requestId === pkgInfoRequestId) pkgInfoFieldsEl.scrollTop = 0;
} catch (err) {
if (requestId !== pkgInfoRequestId) return;
pkgInfoItem = null;
pkgInfoFieldsEl.innerHTML = "";
setModalBackgroundLocked(false);
showActionFailed(t("pkgInfoTitle"), err.message);
}
}
function closePkgInfo() {
pkgInfoRequestId++;
pkgInfoOverlayEl.hidden = true;
pkgInfoItem = null;
pkgInfoTitleEl.textContent = t("pkgInfoTitle");
pkgInfoFieldsEl.innerHTML = "";
pkgInfoImageEl.src = "/icon-pkg.png";
pkgInfoInstallBtn.disabled = true;
setModalBackgroundLocked(false);
}
function installPkgFromInfo() {
const item = pkgInfoItem;
if (!item || pkgInfoInstallBtn.disabled) return;
closePkgInfo();
installPkg(item);
}
function permissionModeText(mode) {
const octal = (Number(mode || 0) & 0x1ff).toString(8);
return "0" + ("000" + octal).slice(-3);
@@ -845,6 +949,7 @@ function openPermissionDialog(item) {
syncPermissionChecks(permissionModeEl.value);
setPermissionBusy(false);
permissionOverlayEl.hidden = false;
renderSinglePermissionPath();
permissionModeEl.focus();
permissionModeEl.select();
}
@@ -1051,6 +1156,12 @@ function itemListTitle(items, limit) {
return items.length > limit ? t("selectedItems", { name: shown, count: items.length }) : shown;
}
function renderSinglePermissionPath() {
if (permissionOverlayEl.hidden || permissionItems.length !== 1) return;
const namesEl = permissionPathEl.querySelector(".permission-path-names");
if (namesEl) renderEndOfPath(namesEl, displayPath(permissionItems[0].path));
}
function renderPermissionItems(items) {
const names = items.length === 1 ? displayPath(items[0].path) :
items.map(displayName).join(", ");
@@ -1720,7 +1831,8 @@ async function actionPaste() {
}
function renderTasks(tasks) {
const active = tasks.find(task => task.state === "queued" || task.state === "running");
const fileTasks = tasks.filter(task => task.op !== "pkg_install");
const active = fileTasks.find(task => task.state === "queued" || task.state === "running");
if (!active && pendingOverlayText) {
renderPendingOverlay(pendingOverlayText, pendingOverlayLabel);
showTaskOverlay();
@@ -1827,6 +1939,7 @@ async function pollTasks() {
const data = await api("/api/tasks");
taskPollFailedAlertShown = false;
const tasks = data.tasks || [];
const fileTasks = tasks.filter(task => task.op !== "pkg_install");
const completion = data.completion;
if (lastCompletionId === null) {
lastCompletionId = completion ? completion.id : 0;
@@ -1856,7 +1969,7 @@ async function pollTasks() {
trackedTask.state = task.state;
}
if (isTerminalTask(task)) {
shouldRefresh = true;
if (task.op !== "pkg_install") shouldRefresh = true;
handleTerminalTask(task);
}
}
@@ -1866,7 +1979,7 @@ async function pollTasks() {
shouldRefresh = true;
clearTrackedTask();
}
if (taskBecameIdle && !tasks.length) shouldRefresh = true;
if (taskBecameIdle && !fileTasks.length) shouldRefresh = true;
if (shouldRefresh) {
if (taskBecameIdle) startContentLoadingTimer();
clearSelection(false);
@@ -2103,6 +2216,7 @@ function toggleUploadMenu() {
}
function actionExit() {
if (!confirm(t("exitConfirm"))) return;
setStatus(t("exiting"));
exitBtn.disabled = true;
api("/api/exit").catch(err => {
@@ -2138,6 +2252,13 @@ textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
textEditorSaveBtn.addEventListener("click", saveTextEditor);
newTextBtn.addEventListener("click", actionNewText);
imagePreviewCloseBtn.addEventListener("click", closeImagePreview);
pkgInfoCloseBtn.addEventListener("click", closePkgInfo);
pkgInfoInstallBtn.addEventListener("click", installPkgFromInfo);
pkgInfoImageEl.addEventListener("error", () => {
if (pkgInfoImageEl.getAttribute("src") !== "/icon-pkg.png") {
pkgInfoImageEl.src = "/icon-pkg.png";
}
});
permissionCancelBtn.addEventListener("click", requestClosePermissionDialog);
permissionApplyBtn.addEventListener("click", applyPermissionMode);
permissionModeEl.addEventListener("input", () => {
@@ -2145,7 +2266,10 @@ permissionModeEl.addEventListener("input", () => {
});
permissionModeEl.addEventListener("change", validatePermissionMode);
for (const checkbox of permissionChecks) checkbox.addEventListener("change", syncPermissionMode);
window.addEventListener("resize", renderAddressPath);
window.addEventListener("resize", () => {
renderAddressPath();
renderSinglePermissionPath();
});
window.addEventListener("popstate", event => {
if (historyBlocked()) {
writeHistoryPath(cwd, false);
@@ -2157,6 +2281,10 @@ window.addEventListener("popstate", event => {
requestClosePermissionDialog();
return;
}
if (!pkgInfoOverlayEl.hidden) {
closePkgInfo();
return;
}
setStatus(t("activeTask"));
return;
}
@@ -2183,6 +2311,9 @@ textEditorOverlayEl.addEventListener("click", event => {
imagePreviewOverlayEl.addEventListener("click", event => {
if (event.target === imagePreviewOverlayEl) closeImagePreview();
});
pkgInfoOverlayEl.addEventListener("click", event => {
if (event.target === pkgInfoOverlayEl) closePkgInfo();
});
permissionOverlayEl.addEventListener("click", event => {
if (event.target === permissionOverlayEl) requestClosePermissionDialog();
});
@@ -2228,7 +2359,7 @@ filesEl.addEventListener("click", event => {
} else await load(cwd, false, true);
}, 0);
}
else if (isPkgPackage(item)) installPkg(item);
else if (isPkgPackage(item)) openPkgInfo(item);
else if (isPreviewableImage(item)) openImagePreview(item);
else if (isEditableText(item)) openTextEditor(item);
else togglePath(item.path, !selected.has(item.path));
+241 -23
View File
@@ -7,6 +7,7 @@
#include <limits.h>
#include <pthread.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
@@ -15,9 +16,14 @@
#include <time.h>
#include <unistd.h>
#ifndef __linux__
#include <ps5/kernel.h>
#endif
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
#include "pkg_info.h"
#include "pkg_installer.h"
#include "websrv.h"
@@ -30,6 +36,59 @@
#define LARGE_FILE_THRESHOLD (256LL * 1024 * 1024)
#define TRANSFER_ALERT_THRESHOLD (10 * 60)
#ifndef __linux__
typedef struct shell_ui_uri_param {
uint32_t size;
uint32_t user_id;
} shell_ui_uri_param_t;
int sceKernelLoadStartModule(const char *, size_t, const void *, uint32_t,
void *, int *);
int sceUserServiceInitialize(const int *);
int sceUserServiceGetForegroundUser(int *);
static int
navigate_to_home(void) {
int (*initialize)(void);
int (*launch_by_uri)(const char *, shell_ui_uri_param_t *);
shell_ui_uri_param_t param = {.size = sizeof(param)};
const char *module_path =
"/system_ex/common_ex/lib/libSceShellUIUtil.sprx";
const char *uri = "pshomeui:navigateToHome?bootCondition=psButton";
const int priority = 256;
int module;
int result;
(void)sceUserServiceInitialize(&priority);
module = sceKernelLoadStartModule(module_path, 0, NULL, 0, NULL, NULL);
if(module < 0) {
printf("load libSceShellUIUtil: 0x%08X\n", (unsigned int)module);
return module;
}
initialize = (void *)kernel_dynlib_dlsym(
-1, (uint32_t)module, "sceShellUIUtilInitialize");
launch_by_uri = (void *)kernel_dynlib_dlsym(
-1, (uint32_t)module, "sceShellUIUtilLaunchByUri");
if(!initialize || !launch_by_uri) {
printf("resolve libSceShellUIUtil URI functions failed\n");
return -1;
}
result = initialize();
if(result < 0) {
printf("sceShellUIUtilInitialize: 0x%08X\n", (unsigned int)result);
}
result = sceUserServiceGetForegroundUser((int *)&param.user_id);
if(result < 0) {
printf("sceUserServiceGetForegroundUser: 0x%08X\n",
(unsigned int)result);
}
result = launch_by_uri(uri, &param);
printf("sceShellUIUtilLaunchByUri: 0x%08X\n", (unsigned int)result);
return result;
}
#endif
typedef struct task_completion {
unsigned long id;
task_op_t op;
@@ -90,6 +149,11 @@ typedef struct copy_queue {
#endif
static task_completion_t g_last_completion;
#ifndef __linux__
static pthread_cond_t g_pkg_tasks_cond = PTHREAD_COND_INITIALIZER;
static pthread_t g_pkg_worker_thread;
static int g_pkg_worker_started;
#endif
void
record_task_completion_locked(file_task_t *task, time_t completed_at) {
@@ -1861,6 +1925,7 @@ api_tasks(struct MHD_Connection *conn) {
task->transfer_started_at ? (long long)(now - task->transfer_started_at) : 0LL,
task->created_at ? (long long)(now - task->created_at) : 0LL,
(long long)task->updated_at);
if(!task_is_active(task)) task->reported = 1;
}
strbuf_printf(&b, "],\"now\":%lld,\"completion\":", (long long)now);
if(g_last_completion.id) {
@@ -1890,7 +1955,7 @@ api_cancel(struct MHD_Connection *conn) {
free(idstr);
pthread_mutex_lock(&g_tasks_lock);
for(task = g_tasks; task; task = task->next) {
if(task->id == id && task_is_active(task)) {
if(task->id == id && task->op != TASK_PKG_INSTALL && task_is_active(task)) {
task->cancel_requested = 1;
if(task->op == TASK_DOWNLOAD && task->state == TASK_QUEUED) {
task->state = TASK_CANCELED;
@@ -1911,6 +1976,11 @@ static void *
stop_websrv_later(void *arg) {
(void)arg;
usleep(250000);
#ifndef __linux__
(void)navigate_to_home();
/* Keep this process alive while ShellUI handles the asynchronous URI. */
usleep(1000000);
#endif
websrv_stop();
return NULL;
}
@@ -2147,40 +2217,178 @@ api_chmod(struct MHD_Connection *conn, const char *body, size_t body_size) {
return ret;
}
#ifndef __linux__
static file_task_t *
next_pkg_task_locked(void) {
file_task_t *task;
file_task_t *next = NULL;
for(task = g_tasks; task; task = task->next) {
if(task->op == TASK_PKG_INSTALL && task->state == TASK_QUEUED &&
(!next || task->id < next->id)) next = task;
}
return next;
}
static void *
pkg_task_worker(void *arg) {
(void)arg;
while(1) {
file_task_t *task;
int result;
pthread_mutex_lock(&g_tasks_lock);
while(!(task = next_pkg_task_locked())) {
pthread_cond_wait(&g_pkg_tasks_cond, &g_tasks_lock);
}
task->state = TASK_RUNNING;
snprintf(task->current, sizeof(task->current), "%s", task->src);
task->updated_at = time(NULL);
pthread_mutex_unlock(&g_tasks_lock);
result = pkg_installer_install(task->srcs[0]);
if(result) {
char error_code[16];
snprintf(error_code, sizeof(error_code), "0x%08X", (unsigned int)result);
task_set_error_code(task, "pkg_install_failed", error_code);
task_update(task, TASK_FAILED, task->src, 0,
"package installation failed");
} else {
task_update(task, TASK_DONE, task->src, 0, NULL);
}
}
return NULL;
}
static file_task_t *
active_pkg_task_locked(const char *path) {
file_task_t *task;
for(task = g_tasks; task; task = task->next) {
if(task->op == TASK_PKG_INSTALL && task_is_active(task) &&
!strcmp(task->srcs[0], path)) return task;
}
return NULL;
}
#endif
static int
enqueue_pkg_tasks(char **paths, size_t count, unsigned long *ids) {
#ifdef __linux__
(void)paths; (void)count; (void)ids;
return PKG_INSTALL_UNSUPPORTED;
#else
file_task_t **pending = calloc(count, sizeof(*pending));
int result = 0;
if(!pending) return -1;
for(size_t i = 0; i < count; i++) {
file_task_t *task = calloc(1, sizeof(*task));
if(!task || !(task->srcs = calloc(1, sizeof(*task->srcs))) ||
!(task->srcs[0] = strdup(paths[i]))) {
free_task(task);
result = -1;
goto done;
}
task->op = TASK_PKG_INSTALL;
task->state = TASK_QUEUED;
task->src_count = 1;
snprintf(task->src, sizeof(task->src), "%s", paths[i]);
task->created_at = time(NULL);
task->updated_at = task->created_at;
pending[i] = task;
}
pthread_mutex_lock(&g_tasks_lock);
if(!g_pkg_worker_started) {
result = pthread_create(&g_pkg_worker_thread, NULL, pkg_task_worker, NULL);
if(!result) {
pthread_detach(g_pkg_worker_thread);
g_pkg_worker_started = 1;
}
}
if(!result) {
for(size_t i = 0; i < count; i++) {
file_task_t *existing = active_pkg_task_locked(paths[i]);
if(existing) {
ids[i] = existing->id;
free_task(pending[i]);
} else {
pending[i]->id = g_next_task_id++;
ids[i] = pending[i]->id;
pending[i]->next = g_tasks;
g_tasks = pending[i];
}
pending[i] = NULL;
}
pthread_cond_signal(&g_pkg_tasks_cond);
}
pthread_mutex_unlock(&g_tasks_lock);
done:
for(size_t i = 0; i < count; i++) free_task(pending[i]);
free(pending);
return result;
#endif
}
static enum MHD_Result
api_install_pkg(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
const char *extension;
struct stat st;
char error_code[16];
api_install_pkg(struct MHD_Connection *conn, const char *body,
size_t body_size) {
char *paths_raw = body_form_value(body, body_size, "paths");
char **paths = NULL;
unsigned long *ids = NULL;
size_t count = 0;
strbuf_t json = {0};
int result;
if(!path || !(extension = strrchr(path, '.')) ||
strcasecmp(extension, ".pkg")) {
free(path);
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
"file is not a PKG package",
"pkg_type_invalid", NULL);
if(!paths_raw || parse_paths(paths_raw, &paths, &count) ||
!(ids = calloc(count, sizeof(*ids)))) {
free(paths_raw); free_paths(paths, count); free(ids);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
for(size_t i = 0; i < count; i++) {
const char *extension = strrchr(paths[i], '.');
struct stat st;
if(!extension || strcasecmp(extension, ".pkg")) {
enum MHD_Result ret = send_json_error_detail(
conn, MHD_HTTP_BAD_REQUEST, "file is not a PKG package",
"pkg_type_invalid", paths[i]);
free(paths_raw); free_paths(paths, count); free(ids);
return ret;
}
if(stat(paths[i], &st) || !S_ISREG(st.st_mode)) {
free(paths_raw); free_paths(paths, count); free(ids);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
}
}
result = pkg_installer_install(path);
free(path);
result = enqueue_pkg_tasks(paths, count, ids);
free(paths_raw); free_paths(paths, count);
if(result == PKG_INSTALL_UNSUPPORTED) {
free(ids);
return send_json_error_detail(conn, MHD_HTTP_NOT_IMPLEMENTED,
"package installation is only available on PS5",
"pkg_install_unsupported", NULL);
}
if(result) {
snprintf(error_code, sizeof(error_code), "0x%08X", (unsigned int)result);
return send_json_error_detail(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"package installation failed",
"pkg_install_failed", error_code);
free(ids);
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"could not queue package installation");
}
return send_json_ok(conn);
/* Keep the action from completing instantly after an accidental press. */
usleep(600000);
strbuf_append(&json, "{\"ok\":true,\"task_ids\":[");
for(size_t i = 0; i < count; i++) {
if(i) strbuf_append(&json, ",");
strbuf_printf(&json, "%lu", ids[i]);
}
strbuf_append(&json, "]}");
free(ids);
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
}
enum MHD_Result
@@ -2209,10 +2417,20 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url,
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_chmod(conn, body, body_size);
}
if(!strcmp(url, "/api/pkg-info")) {
return strcmp(method, MHD_HTTP_METHOD_GET) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_pkg_info(conn);
}
if(!strcmp(url, "/api/pkg-icon")) {
return strcmp(method, MHD_HTTP_METHOD_GET) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_pkg_icon(conn);
}
if(!strcmp(url, "/api/install-pkg")) {
return strcmp(method, MHD_HTTP_METHOD_POST) ?
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
api_install_pkg(conn);
api_install_pkg(conn, body, body_size);
}
if(!strcmp(url, "/api/text")) {
return strcmp(method, MHD_HTTP_METHOD_GET) ?
+2
View File
@@ -16,6 +16,7 @@ typedef enum task_op {
TASK_CHMOD,
TASK_DOWNLOAD,
TASK_UPLOAD,
TASK_PKG_INSTALL,
} task_op_t;
typedef enum task_state {
@@ -58,6 +59,7 @@ typedef struct file_task {
unsigned int eta_sample_next;
unsigned int eta_sample_count;
int cancel_requested;
int reported; /* Terminal state has been included in /api/tasks. */
unsigned int active_streams;
time_t created_at;
time_t transfer_started_at;
+591
View File
@@ -0,0 +1,591 @@
#include "pkg_info.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "filemgr_internal.h"
#include "json_util.h"
#include "path_util.h"
#include "websrv.h"
#define PKG_CNT_MAGIC 0x7f434e54u
#define PKG_FIH_MAGIC 0x7f464948u
#define PKG_LIH_MAGIC 0x7f4c4948u
#define PKG_HEADER_SIZE 0xa0
#define PKG_ENTRY_SIZE 0x20
#define PKG_ENTRY_PARAM_SFO 0x1000u
#define PKG_ENTRY_ICON0_PNG 0x1200u
#define PKG_ENTRY_ICON0_LOCALIZED_LAST 0x121fu
#define PKG_ENTRY_PARAM_JSON 0x2000u
#define PKG_ENTRY_FLAG_ENCRYPTED 0x80000000u
#define PKG_ENTRY_MAX 65536u
#define PKG_PARAM_MAX (4u * 1024u * 1024u)
#define PKG_ICON_MAX (32u * 1024u * 1024u)
#define SFO_MAGIC 0x46535000u
#define SFO_ENTRY_SIZE 0x10
#define SFO_ENTRY_MAX 256u
#define JSON_TOKEN_MAX 4096u
typedef enum pkg_param_type {
PKG_PARAM_SFO,
PKG_PARAM_JSON,
} pkg_param_type_t;
typedef struct pkg_source {
int fd;
uint64_t size;
uint32_t content_type;
uint32_t content_flags;
char content_id[37];
uint64_t param_offset;
uint32_t param_size;
pkg_param_type_t param_type;
uint64_t icon_offset;
uint32_t icon_size;
} pkg_source_t;
typedef enum json_token_type {
JSON_OBJECT,
JSON_ARRAY,
JSON_STRING,
JSON_PRIMITIVE,
} json_token_type_t;
typedef struct json_token {
json_token_type_t type;
size_t start;
size_t end;
int parent;
} json_token_t;
static uint16_t
read_le16(const unsigned char *p) {
return (uint16_t)p[0] | (uint16_t)p[1] << 8;
}
static uint32_t
read_le32(const unsigned char *p) {
return (uint32_t)p[0] | (uint32_t)p[1] << 8 |
(uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}
static uint64_t
read_le64(const unsigned char *p) {
return (uint64_t)read_le32(p) | (uint64_t)read_le32(p + 4) << 32;
}
static uint32_t
read_be32(const unsigned char *p) {
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 |
(uint32_t)p[2] << 8 | (uint32_t)p[3];
}
static int
range_valid(uint64_t offset, uint64_t size, uint64_t file_size) {
return offset <= file_size && size <= file_size - offset;
}
static int
read_at(int fd, void *buffer, size_t size, uint64_t offset) {
unsigned char *p = buffer;
size_t done = 0;
while(done < size) {
ssize_t n = pread(fd, p + done, size - done, (off_t)(offset + done));
if(n <= 0) {
if(n < 0 && errno == EINTR) continue;
return -1;
}
done += (size_t)n;
}
return 0;
}
static int
png_signature_valid(const unsigned char *data, size_t size) {
static const unsigned char signature[] = "\x89PNG\r\n\x1a\n";
return size >= sizeof(signature) - 1 &&
!memcmp(data, signature, sizeof(signature) - 1);
}
static int
pkg_icon_is_png(const pkg_source_t *pkg, uint64_t offset, uint32_t size) {
unsigned char signature[8];
return size >= sizeof(signature) &&
!read_at(pkg->fd, signature, sizeof(signature), offset) &&
png_signature_valid(signature, sizeof(signature));
}
static void
pkg_source_close(pkg_source_t *pkg) {
if(pkg->fd >= 0) close(pkg->fd);
pkg->fd = -1;
}
static int
pkg_source_open(const char *path, pkg_source_t *pkg) {
unsigned char header[PKG_HEADER_SIZE];
unsigned char *table = NULL;
struct stat st;
uint64_t container_offset = 0;
uint64_t container_size;
uint32_t magic;
uint32_t entry_count;
uint32_t table_offset;
size_t table_size;
int ret = -1;
memset(pkg, 0, sizeof(*pkg));
pkg->fd = -1;
if((pkg->fd = open(path, O_RDONLY)) < 0 || fstat(pkg->fd, &st) ||
!S_ISREG(st.st_mode) || st.st_size < PKG_HEADER_SIZE ||
read_at(pkg->fd, header, sizeof(header), 0)) goto done;
pkg->size = (uint64_t)st.st_size;
magic = read_be32(header);
if(magic == PKG_FIH_MAGIC) {
container_offset = read_le64(header + 0x58);
} else if(magic == PKG_LIH_MAGIC) {
container_offset = read_le64(header + 0x30);
} else if(magic != PKG_CNT_MAGIC) {
goto done;
}
if(container_offset) {
if(!range_valid(container_offset, sizeof(header), pkg->size) ||
read_at(pkg->fd, header, sizeof(header), container_offset) ||
read_be32(header) != PKG_CNT_MAGIC) goto done;
}
container_size = pkg->size - container_offset;
memcpy(pkg->content_id, header + 0x40, 36);
pkg->content_id[36] = 0;
pkg->content_type = read_be32(header + 0x74);
pkg->content_flags = read_be32(header + 0x78);
entry_count = read_be32(header + 0x10);
table_offset = read_be32(header + 0x18);
if(!entry_count || entry_count > PKG_ENTRY_MAX) goto done;
table_size = (size_t)entry_count * PKG_ENTRY_SIZE;
if(!range_valid(table_offset, table_size, container_size) ||
!(table = malloc(table_size)) ||
read_at(pkg->fd, table, table_size, container_offset + table_offset)) {
goto done;
}
for(uint32_t i = 0; i < entry_count; i++) {
const unsigned char *entry = table + (size_t)i * PKG_ENTRY_SIZE;
uint32_t id = read_be32(entry);
uint32_t flags1 = read_be32(entry + 0x08);
uint32_t offset = read_be32(entry + 0x10);
uint32_t size = read_be32(entry + 0x14);
int encrypted = (flags1 & PKG_ENTRY_FLAG_ENCRYPTED) != 0;
if(!range_valid(offset, size, container_size)) goto done;
if(encrypted || !size) continue;
if(id == PKG_ENTRY_PARAM_SFO && size <= PKG_PARAM_MAX) {
pkg->param_offset = container_offset + offset;
pkg->param_size = size;
pkg->param_type = PKG_PARAM_SFO;
} else if(id == PKG_ENTRY_PARAM_JSON && size <= PKG_PARAM_MAX) {
pkg->param_offset = container_offset + offset;
pkg->param_size = size;
pkg->param_type = PKG_PARAM_JSON;
} else if(id >= PKG_ENTRY_ICON0_PNG &&
id <= PKG_ENTRY_ICON0_LOCALIZED_LAST &&
size <= PKG_ICON_MAX &&
(id == PKG_ENTRY_ICON0_PNG || !pkg->icon_size) &&
pkg_icon_is_png(pkg, container_offset + offset, size)) {
/* Prefer icon0.png; otherwise keep the first valid localized icon. */
pkg->icon_offset = container_offset + offset;
pkg->icon_size = size;
}
}
if(!pkg->param_size) goto done;
ret = 0;
done:
free(table);
if(ret) {
errno = EINVAL;
pkg_source_close(pkg);
}
return ret;
}
static int
append_sfo_fields(strbuf_t *json, const unsigned char *sfo, size_t size) {
uint32_t key_offset;
uint32_t value_offset;
uint32_t count;
uint64_t index_end;
int first = 1;
if(size < 20 || read_le32(sfo) != SFO_MAGIC) return -1;
key_offset = read_le32(sfo + 8);
value_offset = read_le32(sfo + 12);
count = read_le32(sfo + 16);
index_end = 20 + (uint64_t)count * SFO_ENTRY_SIZE;
if(count > SFO_ENTRY_MAX || index_end > size || key_offset < index_end ||
value_offset < key_offset || value_offset > size) return -1;
strbuf_append(json, "[");
for(uint32_t i = 0; i < count; i++) {
const unsigned char *entry = sfo + 20 + (size_t)i * SFO_ENTRY_SIZE;
uint16_t name_offset = read_le16(entry);
uint16_t format = read_le16(entry + 2);
uint32_t value_size = read_le32(entry + 4);
uint32_t value_max = read_le32(entry + 8);
uint32_t data_offset = read_le32(entry + 12);
uint64_t key_pos = (uint64_t)key_offset + name_offset;
uint64_t value_pos = (uint64_t)value_offset + data_offset;
const unsigned char *key_end;
char number[32];
char *value = NULL;
if(key_pos >= value_offset || value_pos > size ||
(value_max && value_size > value_max)) continue;
key_end = memchr(sfo + key_pos, 0, value_offset - (size_t)key_pos);
if(!key_end) continue;
if(format == 0x0204) {
size_t length;
if(!value_size || !range_valid(value_pos, value_size, size)) continue;
length = strnlen((const char *)sfo + value_pos, value_size);
if(!(value = malloc(length + 1))) continue;
memcpy(value, sfo + value_pos, length);
value[length] = 0;
} else if(format == 0x0404 && range_valid(value_pos, 4, size)) {
snprintf(number, sizeof(number), "%u", read_le32(sfo + value_pos));
value = strdup(number);
}
if(!value) continue;
if(!first) strbuf_append(json, ",");
first = 0;
strbuf_append(json, "{\"name\":");
json_escape(json, (const char *)sfo + key_pos);
strbuf_append(json, ",\"value\":");
json_escape(json, value);
strbuf_append(json, "}");
free(value);
}
strbuf_append(json, "]");
return 0;
}
static int
json_add_token(json_token_t *tokens, size_t *count, json_token_type_t type,
size_t start, int parent) {
if(*count >= JSON_TOKEN_MAX) return -1;
tokens[*count] = (json_token_t){
.type = type, .start = start, .end = 0, .parent = parent
};
return (int)(*count)++;
}
static int
parse_json_tokens(const unsigned char *data, size_t size, json_token_t *tokens,
size_t *token_count) {
int parent = -1;
size_t count = 0;
for(size_t i = 0; i < size; i++) {
unsigned char c = data[i];
if(c == '{' || c == '[') {
int index = json_add_token(tokens, &count,
c == '{' ? JSON_OBJECT : JSON_ARRAY,
i, parent);
if(index < 0) return -1;
parent = index;
} else if(c == '}' || c == ']') {
json_token_type_t type = c == '}' ? JSON_OBJECT : JSON_ARRAY;
if(parent < 0 || tokens[parent].type != type) return -1;
tokens[parent].end = i + 1;
parent = tokens[parent].parent;
} else if(c == '"') {
int index = json_add_token(tokens, &count, JSON_STRING, i + 1, parent);
if(index < 0) return -1;
for(i++; i < size && data[i] != '"'; i++) {
if(data[i] < 0x20) return -1;
if(data[i] == '\\') {
if(++i >= size || !strchr("\"\\/bfnrtu", data[i])) return -1;
if(data[i] == 'u') {
for(unsigned int n = 0; n < 4; n++) {
if(++i >= size || !((data[i] >= '0' && data[i] <= '9') ||
(data[i] >= 'a' && data[i] <= 'f') ||
(data[i] >= 'A' && data[i] <= 'F'))) return -1;
}
}
}
}
if(i >= size) return -1;
tokens[index].end = i;
} else if(c == ':' || c == ',' || c == ' ' || c == '\t' ||
c == '\r' || c == '\n') {
continue;
} else {
int index = json_add_token(tokens, &count, JSON_PRIMITIVE, i, parent);
if(index < 0) return -1;
while(i < size && data[i] != ',' && data[i] != ']' && data[i] != '}' &&
data[i] != ' ' && data[i] != '\t' && data[i] != '\r' &&
data[i] != '\n') i++;
if(tokens[index].start == i) return -1;
tokens[index].end = i;
i--;
}
}
if(parent >= 0 || !count || tokens[0].type != JSON_OBJECT ||
!tokens[0].end) return -1;
*token_count = count;
return 0;
}
static int
json_token_equals(const unsigned char *data, const json_token_t *token,
const char *text) {
size_t length = strlen(text);
return token->type == JSON_STRING && token->end - token->start == length &&
!memcmp(data + token->start, text, length);
}
static int
json_next_child(const json_token_t *tokens, size_t count, int parent,
size_t start) {
for(size_t i = start; i < count; i++) {
if(tokens[i].parent == parent) return (int)i;
}
return -1;
}
static int
json_object_value(const unsigned char *data, const json_token_t *tokens,
size_t count, int object, const char *key) {
int item = json_next_child(tokens, count, object, (size_t)object + 1);
while(item >= 0) {
int value = json_next_child(tokens, count, object, (size_t)item + 1);
if(value < 0) return -1;
if(json_token_equals(data, &tokens[item], key)) return value;
item = json_next_child(tokens, count, object, (size_t)value + 1);
}
return -1;
}
static int
json_object_value_token(const unsigned char *data, const json_token_t *tokens,
size_t count, int object, const json_token_t *key) {
int item = json_next_child(tokens, count, object, (size_t)object + 1);
size_t key_size = key->end - key->start;
while(item >= 0) {
int value = json_next_child(tokens, count, object, (size_t)item + 1);
if(value < 0) return -1;
if(tokens[item].type == JSON_STRING &&
tokens[item].end - tokens[item].start == key_size &&
!memcmp(data + tokens[item].start, data + key->start, key_size)) {
return value;
}
item = json_next_child(tokens, count, object, (size_t)value + 1);
}
return -1;
}
static void
append_json_token_string(strbuf_t *json, const unsigned char *data,
const json_token_t *token) {
strbuf_append(json, "\"");
strbuf_printf(json, "%.*s", (int)(token->end - token->start),
data + token->start);
strbuf_append(json, "\"");
}
static void
append_json_field(strbuf_t *json, const unsigned char *data,
const json_token_t *key, const json_token_t *value,
int *first) {
if(!*first) strbuf_append(json, ",");
*first = 0;
strbuf_append(json, "{\"name\":");
append_json_token_string(json, data, key);
strbuf_append(json, ",\"value\":");
append_json_token_string(json, data, value);
strbuf_append(json, "}");
}
static void
append_named_json_field(strbuf_t *json, const char *name,
const unsigned char *data, const json_token_t *value,
int *first) {
if(!*first) strbuf_append(json, ",");
*first = 0;
strbuf_append(json, "{\"name\":");
json_escape(json, name);
strbuf_append(json, ",\"value\":");
append_json_token_string(json, data, value);
strbuf_append(json, "}");
}
static int
append_param_json_fields(strbuf_t *json, const unsigned char *data,
size_t size) {
json_token_t *tokens = calloc(JSON_TOKEN_MAX, sizeof(*tokens));
size_t count = 0;
int first = 1;
int localized;
int title = -1;
if(!tokens || parse_json_tokens(data, size, tokens, &count)) {
free(tokens);
return -1;
}
strbuf_append(json, "[");
localized = json_object_value(data, tokens, count, 0, "localizedParameters");
if(localized >= 0 && tokens[localized].type == JSON_OBJECT) {
int language = json_object_value(data, tokens, count, localized,
"defaultLanguage");
int language_data = language >= 0 && tokens[language].type == JSON_STRING ?
json_object_value_token(data, tokens, count, localized,
&tokens[language]) : -1;
if(language_data >= 0 && tokens[language_data].type == JSON_OBJECT) {
title = json_object_value(data, tokens, count, language_data,
"titleName");
}
if(title < 0) {
int english = json_object_value(data, tokens, count, localized, "en-US");
if(english >= 0 && tokens[english].type == JSON_OBJECT) {
title = json_object_value(data, tokens, count, english, "titleName");
}
}
if(title < 0) {
int item = json_next_child(tokens, count, localized,
(size_t)localized + 1);
while(item >= 0 && title < 0) {
int value = json_next_child(tokens, count, localized,
(size_t)item + 1);
if(value < 0) break;
if(tokens[value].type == JSON_OBJECT) {
title = json_object_value(data, tokens, count, value, "titleName");
}
item = json_next_child(tokens, count, localized, (size_t)value + 1);
}
}
}
if(title >= 0 && tokens[title].type == JSON_STRING) {
append_named_json_field(json, "titleName", data, &tokens[title], &first);
}
for(int item = json_next_child(tokens, count, 0, 1); item >= 0;) {
int value = json_next_child(tokens, count, 0, (size_t)item + 1);
if(value < 0) break;
if(tokens[item].type == JSON_STRING &&
(tokens[value].type == JSON_STRING ||
tokens[value].type == JSON_PRIMITIVE)) {
append_json_field(json, data, &tokens[item], &tokens[value], &first);
}
item = json_next_child(tokens, count, 0, (size_t)value + 1);
}
strbuf_append(json, "]");
free(tokens);
return 0;
}
static enum MHD_Result
pkg_error(struct MHD_Connection *conn, const char *path) {
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
"could not read package information",
"pkg_info_failed", path);
}
enum MHD_Result
api_pkg_info(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
pkg_source_t pkg = {.fd = -1};
unsigned char *param;
strbuf_t json = {0};
if(!path || pkg_source_open(path, &pkg)) {
enum MHD_Result ret = pkg_error(conn, path);
free(path);
return ret;
}
if(!(param = malloc(pkg.param_size)) ||
read_at(pkg.fd, param, pkg.param_size, pkg.param_offset)) {
enum MHD_Result ret = pkg_error(conn, path);
free(param);
pkg_source_close(&pkg);
free(path);
return ret;
}
strbuf_printf(&json,
"{\"ok\":true,\"size\":%llu,\"content_id\":",
(unsigned long long)pkg.size);
json_escape(&json, pkg.content_id);
strbuf_printf(&json,
",\"platform\":\"%s\",\"content_type\":%u,"
"\"content_flags\":%u,\"has_icon\":%s,\"fields\":",
pkg.param_type == PKG_PARAM_JSON ? "PS5" : "PS4",
pkg.content_type, pkg.content_flags,
pkg.icon_size ? "true" : "false");
if((pkg.param_type == PKG_PARAM_JSON &&
append_param_json_fields(&json, param, pkg.param_size)) ||
(pkg.param_type == PKG_PARAM_SFO &&
append_sfo_fields(&json, param, pkg.param_size))) {
enum MHD_Result ret;
free(json.data);
ret = pkg_error(conn, path);
free(param);
pkg_source_close(&pkg);
free(path);
return ret;
}
strbuf_append(&json, "}");
free(param);
pkg_source_close(&pkg);
free(path);
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
}
enum MHD_Result
api_pkg_icon(struct MHD_Connection *conn) {
char *path = fs_path_value(query_value(conn, "path"));
pkg_source_t pkg = {.fd = -1};
unsigned char *icon;
struct MHD_Response *response;
enum MHD_Result ret;
if(!path || pkg_source_open(path, &pkg) || !pkg.icon_size ||
!(icon = malloc(pkg.icon_size)) ||
read_at(pkg.fd, icon, pkg.icon_size, pkg.icon_offset)) {
if(path && pkg.fd >= 0) pkg_source_close(&pkg);
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "package icon not found");
}
if(!png_signature_valid(icon, pkg.icon_size)) {
free(icon);
pkg_source_close(&pkg);
free(path);
return send_json_error(conn, MHD_HTTP_NOT_FOUND,
"package icon not found");
}
pkg_source_close(&pkg);
free(path);
response = MHD_create_response_from_buffer(pkg.icon_size, icon,
MHD_RESPMEM_MUST_FREE);
if(!response) {
free(icon);
return MHD_NO;
}
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, "image/png");
ret = websrv_queue_response(conn, MHD_HTTP_OK, response);
MHD_destroy_response(response);
return ret;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <microhttpd.h>
enum MHD_Result api_pkg_info(struct MHD_Connection *conn);
enum MHD_Result api_pkg_icon(struct MHD_Connection *conn);
+4 -2
View File
@@ -23,6 +23,7 @@ task_op_name(task_op_t op) {
case TASK_CHMOD: return "chmod";
case TASK_DOWNLOAD: return "download";
case TASK_UPLOAD: return "upload";
case TASK_PKG_INSTALL: return "pkg_install";
default: return "unknown";
}
}
@@ -49,7 +50,7 @@ has_active_task_locked(void) {
file_task_t *task;
for(task = g_tasks; task; task = task->next) {
if(task_is_active(task)) {
if(task->op != TASK_PKG_INSTALL && task_is_active(task)) {
return 1;
}
}
@@ -82,7 +83,8 @@ remove_finished_tasks_locked(void) {
while(*link) {
file_task_t *task = *link;
if(task_is_active(task) || task->active_streams) {
if(task_is_active(task) || task->active_streams ||
(task->op == TASK_PKG_INSTALL && !task->reported)) {
link = &task->next;
continue;
}