mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 07:00:28 +02:00
feat: 解压目标目录可选 + PKG 安装 ABI/权限修复
- 前端:解压前弹出目标目录对话框,支持手改路径与「浏览目录」导航 选择器逐级选目录;冲突策略、7z 密码、大文件模式一并纳入。后端 /api/extract 原本就收 dst_dir,无需改动。上传即解压仍落当前路径。 - pkg_installer.c:元数据 ABI 由 0x38 改为原生 0x30(移除 Mono 托管层 多算的 slot/is_playgo_enabled 两个字段,旧布局会让 InstallByPackage 参数错位);安装前补 kernel_set_ucred_authid(0x4800000000000006), 权限失败以 PKG_INSTALL_PRIV_FAILED 区分;Makefile 链接 -lkernel_sys。 - i18n / HTML / CSS 同步新增解压对话框与目录选择器文案与样式。 注:PS5 专有改动(ABI/authid/链接)未经真机验证,需在 kstuff/etaHEN 环境实测安装路径是否真跑通。
This commit is contained in:
1 parent
c46f5969c8
commit
ce55d08299
7 files changed
+390
-23
No files matched your search
@@ -151,7 +151,7 @@ CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
|
||||
# ~850 KiB; see docs/SIZE-OPTIMIZATION.md.
|
||||
LDFLAGS := -Wl,--gc-sections -Wl,--icf=all
|
||||
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService -lkernel_sys
|
||||
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
|
||||
|
||||
@@ -142,6 +142,58 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="extractDialog" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel extract-panel" role="dialog" aria-modal="true" aria-labelledby="extractTitle">
|
||||
<h2 id="extractTitle" class="extract-title" data-i18n="extractDialogTitle"></h2>
|
||||
<div class="extract-row">
|
||||
<span class="extract-label" data-i18n="extractArchive"></span>
|
||||
<span id="extractArchiveName" class="extract-archive-name"></span>
|
||||
</div>
|
||||
<label class="extract-field">
|
||||
<span data-i18n="extractDestination"></span>
|
||||
<span class="extract-dest-row">
|
||||
<input id="extractDestInput" class="extract-dest-input" type="text"
|
||||
spellcheck="false" autocomplete="off" autocapitalize="off">
|
||||
<button id="extractBrowseBtn" type="button" class="secondary" data-i18n="browseFolder"></button>
|
||||
</span>
|
||||
</label>
|
||||
<label class="extract-field">
|
||||
<span data-i18n="extractConflict"></span>
|
||||
<select id="extractConflictSelect" class="extract-select">
|
||||
<option value="fail" data-i18n="extractConflictFail"></option>
|
||||
<option value="overwrite" data-i18n="extractConflictOverwrite"></option>
|
||||
<option value="merge" data-i18n="extractConflictMerge"></option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="extractPasswordField" class="extract-field" hidden>
|
||||
<span data-i18n="extractPasswordLabel"></span>
|
||||
<input id="extractPasswordInput" class="extract-password-input" type="text"
|
||||
spellcheck="false" autocomplete="off" autocapitalize="off">
|
||||
</label>
|
||||
<label class="extract-field extract-large-field">
|
||||
<input id="extractLargeCheck" type="checkbox">
|
||||
<span data-i18n="extractLargeMode"></span>
|
||||
</label>
|
||||
<div class="text-editor-actions">
|
||||
<button id="extractCancelBtn" type="button" class="secondary" data-i18n="cancel"></button>
|
||||
<button id="extractConfirmBtn" type="button" class="primary" data-i18n="extract"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="folderPickerOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel folder-picker-panel" role="dialog" aria-modal="true" aria-labelledby="folderPickerTitle">
|
||||
<h2 id="folderPickerTitle" class="folder-picker-title" data-i18n="selectFolder"></h2>
|
||||
<div id="folderPickerPath" class="folder-picker-path"></div>
|
||||
<div id="folderPickerList" class="folder-picker-list"></div>
|
||||
<div class="text-editor-actions">
|
||||
<button id="folderPickerCancelBtn" type="button" class="secondary" data-i18n="cancel"></button>
|
||||
<button id="folderPickerUpBtn" type="button" class="secondary" data-i18n="parent"></button>
|
||||
<button id="folderPickerOkBtn" type="button" class="primary" data-i18n="selectHere"></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>
|
||||
|
||||
@@ -118,6 +118,19 @@ window.WFM_LANG = {
|
||||
extractSelectArchive: "Select one archive to extract (ZIP / RAR / 7z)",
|
||||
extractOneAtATime: "Only one archive can be extracted at a time",
|
||||
extractSelectMainVolume: "Please select the main volume (.rar or .part01.rar)",
|
||||
extractDialogTitle: "Extract archive",
|
||||
extractArchive: "Archive",
|
||||
extractDestination: "Extract into (destination directory)",
|
||||
browseFolder: "Browse folders",
|
||||
extractConflict: "File conflict policy",
|
||||
extractConflictFail: "Fail if name exists",
|
||||
extractConflictOverwrite: "Overwrite files",
|
||||
extractConflictMerge: "Merge directories",
|
||||
extractPasswordLabel: "Password (7z encryption; leave empty if none)",
|
||||
extractLargeMode: "Large-file mode (max 1 TiB file / 4 TiB total)",
|
||||
extractDestEmpty: "Please enter a destination directory",
|
||||
selectFolder: "Select folder",
|
||||
selectHere: "Select this folder",
|
||||
extractArchivePending: "Preparing to extract {name}",
|
||||
sameSourceTarget: "Source and destination are the same. Cannot {label} {name}",
|
||||
removeConflictFirst: "A {existingType} named {name} already exists. To {label} this {sourceType}, delete that {existingType} first.",
|
||||
|
||||
@@ -118,6 +118,19 @@ window.WFM_LANG = {
|
||||
extractSelectArchive: "选中一个压缩包后才能解压(ZIP / RAR / 7z)",
|
||||
extractOneAtATime: "一次只能解压一个压缩包",
|
||||
extractSelectMainVolume: "请改选主卷(如 .rar 或 .part01.rar)",
|
||||
extractDialogTitle: "解压压缩包",
|
||||
extractArchive: "压缩包",
|
||||
extractDestination: "解压到(目标目录)",
|
||||
browseFolder: "浏览目录",
|
||||
extractConflict: "文件冲突策略",
|
||||
extractConflictFail: "同名则失败",
|
||||
extractConflictOverwrite: "覆盖同名文件",
|
||||
extractConflictMerge: "合并目录",
|
||||
extractPasswordLabel: "密码(7z 加密,可留空)",
|
||||
extractLargeMode: "大文件模式(单文件上限 1 TiB / 总计 4 TiB)",
|
||||
extractDestEmpty: "请填写目标目录",
|
||||
selectFolder: "选择目录",
|
||||
selectHere: "选择此目录",
|
||||
extractArchivePending: "正在准备解压 {name}",
|
||||
sameSourceTarget: "源和目标相同,不能{label} {name}",
|
||||
removeConflictFirst: "目标中已存在同名{existingType} {name}。要{label}{sourceType},请先删除该{existingType}才能继续。",
|
||||
|
||||
+139
@@ -959,6 +959,145 @@ input[type="checkbox"] {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* --- Extract dialog + folder picker --- */
|
||||
.extract-panel {
|
||||
width: 600px;
|
||||
max-width: calc(100vw - 48px);
|
||||
}
|
||||
|
||||
.extract-title {
|
||||
margin: 0 0 16px;
|
||||
color: #edf0f2;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.extract-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.extract-label {
|
||||
flex: 0 0 auto;
|
||||
color: #8f9ba6;
|
||||
}
|
||||
|
||||
.extract-archive-name {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
color: #d9e0e6;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.extract-field {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.extract-field > span:first-child,
|
||||
.extract-large-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.extract-field > span[data-i18n] {
|
||||
color: #8f9ba6;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.extract-dest-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.extract-dest-input,
|
||||
.extract-password-input,
|
||||
.extract-select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
color: #d9e0e6;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.extract-dest-input:focus,
|
||||
.extract-password-input:focus,
|
||||
.extract-select:focus {
|
||||
outline: none;
|
||||
border-color: #6fb1ff;
|
||||
}
|
||||
|
||||
.extract-large-field {
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-picker-panel {
|
||||
width: 600px;
|
||||
max-width: calc(100vw - 48px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.folder-picker-title {
|
||||
margin: 0 0 14px;
|
||||
color: #edf0f2;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.folder-picker-path {
|
||||
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
font-size: 16px;
|
||||
color: #aab4be;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.folder-picker-list {
|
||||
max-height: calc(100vh - 320px);
|
||||
min-height: 120px;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.folder-picker-row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
font-size: 18px;
|
||||
color: #d9e0e6;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-picker-row:hover,
|
||||
.folder-picker-row:focus {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.folder-picker-empty {
|
||||
padding: 14px 12px;
|
||||
color: #8f9ba6;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.permission-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
+145
-17
@@ -100,6 +100,22 @@ const pkgInfoTitleEl = document.getElementById("pkgInfoTitle");
|
||||
const pkgInfoFieldsEl = document.getElementById("pkgInfoFields");
|
||||
const pkgInfoCloseBtn = document.getElementById("pkgInfoCloseBtn");
|
||||
const pkgInfoInstallBtn = document.getElementById("pkgInfoInstallBtn");
|
||||
const extractDialogEl = document.getElementById("extractDialog");
|
||||
const extractArchiveNameEl = document.getElementById("extractArchiveName");
|
||||
const extractDestInputEl = document.getElementById("extractDestInput");
|
||||
const extractBrowseBtnEl = document.getElementById("extractBrowseBtn");
|
||||
const extractConflictSelectEl = document.getElementById("extractConflictSelect");
|
||||
const extractPasswordFieldEl = document.getElementById("extractPasswordField");
|
||||
const extractPasswordInputEl = document.getElementById("extractPasswordInput");
|
||||
const extractLargeCheckEl = document.getElementById("extractLargeCheck");
|
||||
const extractCancelBtnEl = document.getElementById("extractCancelBtn");
|
||||
const extractConfirmBtnEl = document.getElementById("extractConfirmBtn");
|
||||
const folderPickerOverlayEl = document.getElementById("folderPickerOverlay");
|
||||
const folderPickerPathEl = document.getElementById("folderPickerPath");
|
||||
const folderPickerListEl = document.getElementById("folderPickerList");
|
||||
const folderPickerCancelBtnEl = document.getElementById("folderPickerCancelBtn");
|
||||
const folderPickerUpBtnEl = document.getElementById("folderPickerUpBtn");
|
||||
const folderPickerOkBtnEl = document.getElementById("folderPickerOkBtn");
|
||||
const permissionOverlayEl = document.getElementById("permissionOverlay");
|
||||
const permissionPathEl = document.getElementById("permissionPath");
|
||||
const permissionModeEl = document.getElementById("permissionMode");
|
||||
@@ -1028,25 +1044,137 @@ function actionExtract() {
|
||||
if (busy || loadingPath) return;
|
||||
const archives = selectedEntries().filter(isExtractableArchive);
|
||||
if (archives.length !== 1) return;
|
||||
const item = archives[0];
|
||||
if (!confirm(t("extractConfirm", { name: displayName(item), path: displayPath(cwd) }))) return;
|
||||
const conflict = confirm(t("extractOverwriteAsk")) ? "overwrite" : "fail";
|
||||
const large = shouldPromptLargeMode(item.size) ? promptLargeMode(item.size) : false;
|
||||
// 7z archives can be encrypted (7zAES); ask up front so an unprotected
|
||||
// archive doesn't pay a wasted scan + folder parse. An empty submission is
|
||||
// fine — the engine returns ZIPX_ERR_PASSWORD and the user retries.
|
||||
// ZIP and RAR are not asked here: their headers are readable either way, so
|
||||
// an empty password costs nothing and a failed attempt is retried through
|
||||
// retryExtractWithPassword() instead of interrupting every extraction.
|
||||
let password = "";
|
||||
if (isSevenZipArchive(item) || isSevenZipSplitVolume(item)) {
|
||||
const asked = prompt(t("extractPasswordAsk"), "");
|
||||
if (asked === null) return;
|
||||
password = asked;
|
||||
}
|
||||
startExtractTask(item.path, cwd, conflict, false, displayName(item), large, password);
|
||||
openExtractDialog(archives[0]);
|
||||
}
|
||||
|
||||
// Opens the extract dialog. The destination defaults to the current browsing
|
||||
// directory, but the user may type any path or pick one with the folder
|
||||
// browser. Confirming hands the chosen destination to startExtractTask, so the
|
||||
// backend /api/extract dst_dir is whatever the user selected (or the current
|
||||
// path when they leave it untouched).
|
||||
function openExtractDialog(item) {
|
||||
if (busy || loadingPath) return;
|
||||
extractDialogArchive = item;
|
||||
extractArchiveNameEl.textContent = displayName(item);
|
||||
extractDestInputEl.value = cwd;
|
||||
extractConflictSelectEl.value = "fail";
|
||||
extractPasswordInputEl.value = "";
|
||||
const wantsPassword = isSevenZipArchive(item) || isSevenZipSplitVolume(item);
|
||||
extractPasswordFieldEl.hidden = !wantsPassword;
|
||||
// Large archives default to large-file mode (matching the old "ask + likely
|
||||
// yes" behaviour) but the user can still toggle it off.
|
||||
extractLargeCheckEl.checked = shouldPromptLargeMode(item.size);
|
||||
extractDialogEl.hidden = false;
|
||||
extractDestInputEl.focus();
|
||||
extractDestInputEl.select();
|
||||
}
|
||||
|
||||
function closeExtractDialog() {
|
||||
extractDialogEl.hidden = true;
|
||||
extractDialogArchive = null;
|
||||
}
|
||||
|
||||
let extractDialogArchive = null;
|
||||
|
||||
extractCancelBtnEl.addEventListener("click", closeExtractDialog);
|
||||
extractConfirmBtnEl.addEventListener("click", () => {
|
||||
const item = extractDialogArchive;
|
||||
if (!item) return;
|
||||
const dest = extractDestInputEl.value.trim();
|
||||
if (!dest) {
|
||||
alert(t("extractDestEmpty"));
|
||||
extractDestInputEl.focus();
|
||||
return;
|
||||
}
|
||||
const conflict = extractConflictSelectEl.value;
|
||||
const large = extractLargeCheckEl.checked;
|
||||
const password = extractPasswordFieldEl.hidden ? "" : extractPasswordInputEl.value;
|
||||
closeExtractDialog();
|
||||
startExtractTask(item.path, dest, conflict, false, displayName(item), large, password);
|
||||
});
|
||||
extractDialogEl.addEventListener("click", event => {
|
||||
if (event.target === extractDialogEl) closeExtractDialog();
|
||||
});
|
||||
|
||||
// --- Folder picker -------------------------------------------------------
|
||||
// A minimal directory browser reused by the extract dialog's "Browse folders"
|
||||
// button. It lists subdirectories of the current navigation path and lets the
|
||||
// user drill down or step up, then confirms the highlighted directory as the
|
||||
// destination.
|
||||
let folderPickerOnPick = null;
|
||||
let folderPickerCurrent = "/";
|
||||
|
||||
function openFolderPicker(initialPath, onPick) {
|
||||
folderPickerOnPick = onPick;
|
||||
folderPickerCurrent = initialPath || cwd;
|
||||
folderPickerOverlayEl.hidden = false;
|
||||
folderPickerRefresh();
|
||||
}
|
||||
|
||||
async function folderPickerRefresh() {
|
||||
folderPickerPathEl.textContent = folderPickerCurrent;
|
||||
folderPickerListEl.innerHTML = "";
|
||||
const loading = document.createElement("div");
|
||||
loading.className = "folder-picker-empty";
|
||||
loading.textContent = t("ready");
|
||||
folderPickerListEl.appendChild(loading);
|
||||
try {
|
||||
const data = await api("/api/list", { path: folderPickerCurrent });
|
||||
const dirs = (data.entries || [])
|
||||
.filter(e => e.type === "d")
|
||||
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||
folderPickerListEl.innerHTML = "";
|
||||
if (!dirs.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "folder-picker-empty";
|
||||
empty.textContent = t("empty");
|
||||
folderPickerListEl.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
for (const d of dirs) {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "folder-picker-row";
|
||||
row.textContent = d.name + "/";
|
||||
row.addEventListener("click", () => {
|
||||
folderPickerCurrent = d.path;
|
||||
folderPickerRefresh();
|
||||
});
|
||||
folderPickerListEl.appendChild(row);
|
||||
}
|
||||
} catch (err) {
|
||||
folderPickerListEl.innerHTML = "";
|
||||
const msg = document.createElement("div");
|
||||
msg.className = "folder-picker-empty";
|
||||
msg.textContent = err.message;
|
||||
folderPickerListEl.appendChild(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function closeFolderPicker(picked) {
|
||||
folderPickerOverlayEl.hidden = true;
|
||||
const cb = folderPickerOnPick;
|
||||
folderPickerOnPick = null;
|
||||
if (picked && cb) cb(folderPickerCurrent);
|
||||
}
|
||||
|
||||
folderPickerCancelBtnEl.addEventListener("click", () => closeFolderPicker(false));
|
||||
folderPickerOkBtnEl.addEventListener("click", () => closeFolderPicker(true));
|
||||
folderPickerUpBtnEl.addEventListener("click", () => {
|
||||
const trimmed = folderPickerCurrent.replace(/\/+$/, "");
|
||||
const idx = trimmed.lastIndexOf("/");
|
||||
folderPickerCurrent = idx <= 0 ? "/" : trimmed.slice(0, idx) || "/";
|
||||
folderPickerRefresh();
|
||||
});
|
||||
folderPickerOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === folderPickerOverlayEl) closeFolderPicker(false);
|
||||
});
|
||||
extractBrowseBtnEl.addEventListener("click", () => {
|
||||
openFolderPicker(extractDestInputEl.value.trim() || cwd, p => {
|
||||
if (p) extractDestInputEl.value = p;
|
||||
});
|
||||
});
|
||||
|
||||
function openImagePreview(item) {
|
||||
if (busy) return;
|
||||
setModalBackgroundLocked(true);
|
||||
|
||||
+27
-5
@@ -8,6 +8,10 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Native sceAppInstUtil MetaInfo ABI is 6 pointers (0x30). The old 0x38 layout
|
||||
carried two extra Mono-managed fields (slot, is_playgo_enabled) that do not
|
||||
exist in the firmware's native struct; passing the oversized struct shifts
|
||||
every subsequent argument and makes InstallByPackage fail or misbehave. */
|
||||
typedef struct pkg_metadata {
|
||||
const char *uri;
|
||||
const char *ex_uri;
|
||||
@@ -15,13 +19,23 @@ typedef struct pkg_metadata {
|
||||
const char *content_id;
|
||||
const char *content_name;
|
||||
const char *icon_url;
|
||||
uint32_t slot;
|
||||
uint32_t is_playgo_enabled;
|
||||
} pkg_metadata_t;
|
||||
|
||||
_Static_assert(sizeof(pkg_metadata_t) == 0x38,
|
||||
_Static_assert(sizeof(pkg_metadata_t) == 0x30,
|
||||
"sceAppInstUtil metadata ABI mismatch");
|
||||
|
||||
/* The stock process lacks the privilege sceAppInstUtil needs. kstuff/etaHEN
|
||||
expose kernel_set_ucred_authid through libkernel_sys; raising the authid to
|
||||
the debug value before install is what lets the call succeed on a real
|
||||
console. Declared here (PS5 build only) and resolved by -lkernel_sys. */
|
||||
int kernel_set_ucred_authid(uint64_t authid);
|
||||
|
||||
#ifndef DEBUG_AUTHID
|
||||
#define DEBUG_AUTHID 0x4800000000000006ULL
|
||||
#endif
|
||||
|
||||
#define PKG_INSTALL_PRIV_FAILED 0x80000001
|
||||
|
||||
typedef struct pkg_info {
|
||||
char content_id[48];
|
||||
int type;
|
||||
@@ -78,8 +92,6 @@ pkg_installer_install(const char *path) {
|
||||
.content_id = "",
|
||||
.content_name = "",
|
||||
.icon_url = "",
|
||||
.slot = 0,
|
||||
.is_playgo_enabled = 0
|
||||
};
|
||||
pkg_info_t pkg_info = {0};
|
||||
playgo_info_t playgo_info = {0};
|
||||
@@ -98,6 +110,16 @@ pkg_installer_install(const char *path) {
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return result;
|
||||
}
|
||||
/* Raise the process authid to the debug value so sceAppInstUtil is allowed
|
||||
to install. Failure here means the kernel privilege was not granted
|
||||
(no kstuff/etaHEN present or not patched) -- report it distinctly rather
|
||||
than handing a privileged call to an unprivileged process. */
|
||||
if(kernel_set_ucred_authid(DEBUG_AUTHID)) {
|
||||
printf("pkg_installer: kernel_set_ucred_authid failed (0x%016llx)\n",
|
||||
(unsigned long long)DEBUG_AUTHID);
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return PKG_INSTALL_PRIV_FAILED;
|
||||
}
|
||||
result = sceAppInstUtilInstallByPackage(&metadata, &pkg_info, &playgo_info);
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return result;
|
||||
|
||||
Reference in new issue
Block a user