mirror of
https://github.com/ItsBlurf/BFpilot.git
synced 2026-10-18 23:06:53 -05:00
v0.4.1: restore separate launcher; main ELF without AppInst.
All-in-one AppInst in bfpilot.elf broke loaders. Revert to proven model: file manager only in main, Media tile via bfpilot-launcher-installer.elf. Keep action-bar wrap, single /data/BFpilot, and issue #7-#12 fixes.
This commit is contained in:
1 parent
dd7e1d4d1f
commit
00028d2dcf
12 files changed
+142
-389
No files matched your search
+3
-3
@@ -2,9 +2,9 @@
|
||||
|
||||
## v0.4.1
|
||||
|
||||
* **Single ELF** like Payload Manager: `bfpilot.elf` runs the port-5905 file manager **and** installs the PS5 home **Media** tile (`applicationCategoryType` 65536, title `BFPL00001`, deeplink `http://127.0.0.1:5905/`) via embedded AppInst
|
||||
* Optional recovery installer `bfpilot-launcher-installer.elf` only under `/data/BFpilot/` (not a separate data tree)
|
||||
* **One data directory**: all logs/index/shortcuts/archive state under `/data/BFpilot` only (no `/data/bfpilot`, `/data/BFPILOT`, or `/data/bfpilot-launcher-installer` roots)
|
||||
* **Separate payloads** (stable model): `bfpilot.elf` = file manager only; `bfpilot-launcher-installer.elf` = home **Media** tile (`applicationCategoryType` 65536, `BFPL00001` → `http://127.0.0.1:5905/`)
|
||||
* Main ELF does **not** link AppInst (loaders reject all-in-one); inject the installer separately or place it under `/data/BFpilot/` so the main ELF can auto-start it after the UI is up
|
||||
* **One data directory**: all logs/index/shortcuts/archive state under `/data/BFpilot` only
|
||||
* Action bar wraps (no horizontal scrollbar) for New Folder / Rename / Download / Load payload / chmod / Extract / Upload / Delete
|
||||
* **Load payload**: double-click `.elf`/`.bin`/`.js` or toolbar — streams to elfldr on `127.0.0.1:9021`
|
||||
* New API: `GET /api/fs/launch?path=...`
|
||||
|
||||
@@ -49,7 +49,6 @@ WEB_SRCS += src/transfer.c
|
||||
WEB_SRCS += src/payload_launch.c
|
||||
WEB_SRCS += src/tile_bootstrap.c
|
||||
WEB_SRCS += src/path_ops.c
|
||||
WEB_SRCS += src/app_installer.c
|
||||
|
||||
LAUNCHER_INSTALLER_SRCS := src/launcher_installer_force_main.c
|
||||
LAUNCHER_INSTALLER_SRCS += src/boot_marker.c
|
||||
@@ -143,9 +142,9 @@ BFPILOT_CFLAGS += -DBFPILOT_PAYLOAD_NAME=\"bfpilot\"
|
||||
BFPILOT_CFLAGS += -DBFPILOT_BUILD_MODE=\"file-manager\"
|
||||
BFPILOT_CFLAGS += -DBFPILOT_WEB_PORT=$(WEB_PORT)
|
||||
BFPILOT_CFLAGS += -DBFPILOT_DEBUG_NOTIFICATIONS=0
|
||||
# Embedded Media-tile AppInst in main ELF (Payload Manager model).
|
||||
BFPILOT_CFLAGS += -DBFPILOT_ENABLE_LAUNCHER=1
|
||||
BFPILOT_CFLAGS += -DBFPILOT_DISABLE_LAUNCHER=0
|
||||
# AppInst stays in separate launcher-installer only (main ELF must not link it).
|
||||
BFPILOT_CFLAGS += -DBFPILOT_ENABLE_LAUNCHER=0
|
||||
BFPILOT_CFLAGS += -DBFPILOT_DISABLE_LAUNCHER=1
|
||||
BFPILOT_CFLAGS += -DBFPILOT_ENABLE_INTEGRATED_ARCHIVE=$(ENABLE_ARCHIVE)
|
||||
|
||||
ifeq ($(ENABLE_ARCHIVE),1)
|
||||
@@ -219,16 +218,10 @@ WINDOWS_CXX_LINK_PREFIX += -L"$(PS5_PAYLOAD_SDK_SHELL)/target/lib"
|
||||
WINDOWS_CXX_LINK_PREFIX += -L"$(PS5_PAYLOAD_SDK_SHELL)/target/user/homebrew/lib" -L.
|
||||
WINDOWS_CXX_LINK_PREFIX += -l:crt1.o -l:crti.o -l:crtbegin.o
|
||||
WINDOWS_CXX_LINK_PREFIX += -lunwind -lc++abi -lc++ -lc
|
||||
# Main ELF = file manager + Media tile install (AppInst + UserService).
|
||||
WINDOWS_CXX_LINK_SUFFIX := -lSceNotification -lSceAppInstUtil -lSceUserService
|
||||
WINDOWS_CXX_LINK_SUFFIX += -lSceSystemService -lkernel_sys -lkernel_web
|
||||
WINDOWS_CXX_LINK_SUFFIX += -lSceLibcInternal -lSceNet -lpthread
|
||||
WINDOWS_CXX_LINK_SUFFIX += -lc_stub_weak -lkernel_stub_weak
|
||||
# Main ELF = file manager only (no AppInst — loaders reject it in this payload).
|
||||
WINDOWS_CXX_LINK_SUFFIX := -lSceNotification -lkernel_web -lSceLibcInternal
|
||||
WINDOWS_CXX_LINK_SUFFIX += -lSceNet -lpthread -lc_stub_weak -lkernel_stub_weak
|
||||
WINDOWS_CXX_LINK_SUFFIX += -l:crtend.o -l:crtn.o
|
||||
# Archive worker has no AppInst (avoid pulling privileged libs into worker).
|
||||
WINDOWS_CXX_LINK_SUFFIX_ARCHIVE := -lSceNotification -lkernel_web -lSceLibcInternal
|
||||
WINDOWS_CXX_LINK_SUFFIX_ARCHIVE += -lSceNet -lpthread -lc_stub_weak -lkernel_stub_weak
|
||||
WINDOWS_CXX_LINK_SUFFIX_ARCHIVE += -l:crtend.o -l:crtn.o
|
||||
define run
|
||||
$(RUN_ENV) && $(1)
|
||||
endef
|
||||
@@ -276,14 +269,12 @@ build/bfpilot-archive/%.o: %.c Makefile
|
||||
$(call run,mkdir -p $(dir $@))
|
||||
$(call run,$(CC_CMD) $(ARCHIVE_WORKER_CFLAGS) $(ARCHIVE_WORKER_DEFINES) $(ARCHIVE_SEVENZ_DEFINES) $(ARCHIVE_WORKER_INCLUDES) -DBFPILOT_ARCHIVE_INTEGRATED=1 -c $< -o $@)
|
||||
|
||||
# AppInst libs for main + optional recovery installer.
|
||||
BFPILOT_APPINST_LDLIBS := -lSceAppInstUtil -lSceUserService -lSceSystemService
|
||||
|
||||
ifeq ($(HOST_IS_WINDOWS),1)
|
||||
$(BFPILOT_BIN): $(BFPILOT_OBJS) $(if $(filter 1,$(ENABLE_ARCHIVE)),$(BFPILOT_ARCHIVE_OBJS)) $(APP_ASSETS)
|
||||
$(BFPILOT_BIN): $(BFPILOT_OBJS) $(if $(filter 1,$(ENABLE_ARCHIVE)),$(BFPILOT_ARCHIVE_OBJS))
|
||||
$(file >build/bfpilot-link.rsp,$(BFPILOT_OBJS) $(BFPILOT_LINK_ARCHIVE_OBJS))
|
||||
$(call run,$(WINDOWS_CXX_LD_CMD) -o $@ $(WINDOWS_CXX_LINK_PREFIX) @build/bfpilot-link.rsp $(WINDOWS_CXX_LINK_SUFFIX))
|
||||
$(call run,$(STRIP_CMD) --strip-all $@)
|
||||
$(call run,$(PYTHON) scripts/scrub_main_payload.py $@)
|
||||
|
||||
$(LAUNCHER_INSTALLER_BIN): $(LAUNCHER_INSTALLER_OBJS) $(APP_ASSETS)
|
||||
$(call run,$(LD_CMD) -o $@ $(WINDOWS_LINK_PREFIX) $(LAUNCHER_INSTALLER_OBJS) $(WINDOWS_APPINST_SUFFIX))
|
||||
@@ -291,12 +282,13 @@ $(LAUNCHER_INSTALLER_BIN): $(LAUNCHER_INSTALLER_OBJS) $(APP_ASSETS)
|
||||
|
||||
$(ARCHIVE_WORKER_BIN): $(ARCHIVE_WORKER_OBJS)
|
||||
$(file >build/archive-worker-link.rsp,$(ARCHIVE_WORKER_OBJS))
|
||||
$(call run,$(WINDOWS_CXX_LD_CMD) -o $@ $(WINDOWS_CXX_LINK_PREFIX) @build/archive-worker-link.rsp $(WINDOWS_CXX_LINK_SUFFIX_ARCHIVE))
|
||||
$(call run,$(WINDOWS_CXX_LD_CMD) -o $@ $(WINDOWS_CXX_LINK_PREFIX) @build/archive-worker-link.rsp $(WINDOWS_CXX_LINK_SUFFIX))
|
||||
$(call run,$(STRIP_CMD) --strip-all $@)
|
||||
else
|
||||
$(BFPILOT_BIN): $(BFPILOT_OBJS) $(if $(filter 1,$(ENABLE_ARCHIVE)),$(BFPILOT_ARCHIVE_OBJS)) $(APP_ASSETS)
|
||||
$(call run,$(CXX_CMD) $(COMMON_LDFLAGS) -o $@ $(BFPILOT_OBJS) $(BFPILOT_LINK_ARCHIVE_OBJS) $(ARCHIVE_WORKER_LDFLAGS) $(BFPILOT_APPINST_LDLIBS) $(PRIVILEGED_APPINST_LDLIBS))
|
||||
$(BFPILOT_BIN): $(BFPILOT_OBJS) $(if $(filter 1,$(ENABLE_ARCHIVE)),$(BFPILOT_ARCHIVE_OBJS))
|
||||
$(call run,$(CXX_CMD) $(COMMON_LDFLAGS) -o $@ $(BFPILOT_OBJS) $(BFPILOT_LINK_ARCHIVE_OBJS) $(ARCHIVE_WORKER_LDFLAGS))
|
||||
$(call run,$(STRIP_CMD) --strip-all $@)
|
||||
$(call run,$(PYTHON) scripts/scrub_main_payload.py $@)
|
||||
|
||||
$(LAUNCHER_INSTALLER_BIN): $(LAUNCHER_INSTALLER_OBJS) $(APP_ASSETS)
|
||||
$(call run,$(CC_CMD) $(LAUNCHER_INSTALLER_CFLAGS) $(COMMON_LDFLAGS) -o $@ $(LAUNCHER_INSTALLER_OBJS) $(PRIVILEGED_APPINST_LDLIBS))
|
||||
|
||||
@@ -14,13 +14,13 @@ Version **v0.4.1**. Tested on FW **11.60**.
|
||||
|
||||
| File | What it is |
|
||||
|------|------------|
|
||||
| `bfpilot.elf` | **Primary** — file manager + archive extract + **Media home tile** install (same single-ELF model as Payload Manager). Inject this. |
|
||||
| `bfpilot-launcher-installer.elf` | Optional recovery tile installer only. If needed, place the **file** under `/data/BFpilot/` (not a separate folder under `/data`). |
|
||||
| `bfpilot.elf` | File manager + archive extract (port **5905**). Inject this for the UI. **No AppInst** in this ELF. |
|
||||
| `bfpilot-launcher-installer.elf` | Home menu **Media** tile (`BFPL00001` → `http://127.0.0.1:5905/`). Inject once, or put the **file** under `/data/BFpilot/` so the main ELF auto-starts it after the UI is up. |
|
||||
| `bfpilot-archive-worker.elf` | Build-only diagnostic. You don't need this for normal use. |
|
||||
|
||||
Normal use is just `bfpilot.elf`. On boot it installs/refreshes the **Media** shelf tile (`BFPL00001` → `http://127.0.0.1:5905/`).
|
||||
**Typical use:** inject `bfpilot.elf`, then inject `bfpilot-launcher-installer.elf` once for the Media shelf tile (or keep a copy at `/data/BFpilot/bfpilot-launcher-installer.elf`).
|
||||
|
||||
**Data layout:** everything lives under **`/data/BFpilot`** only (logs, search index, shortcuts, archive jobs). Do not create `/data/bfpilot`, `/data/BFPILOT`, or `/data/bfpilot-launcher-installer` trees.
|
||||
**Data layout:** everything lives under **`/data/BFpilot`** only (logs, search index, shortcuts, archive jobs).
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,40 +1,35 @@
|
||||
# BFpilot Compatibility Strategy
|
||||
|
||||
BFpilot follows the **Payload Manager** model: one primary ELF provides the
|
||||
web service **and** the PS5 home **Media** tile install. An optional separate
|
||||
installer remains for recovery only.
|
||||
BFpilot keeps the file manager/archive runtime and the fragile launcher
|
||||
installer as **separate payloads**. AppInst in the main ELF breaks some
|
||||
loaders; the Media tile installer is proven working on its own.
|
||||
|
||||
## Payload Roles
|
||||
|
||||
### `bfpilot.elf` (primary)
|
||||
### `bfpilot.elf`
|
||||
|
||||
Main release payload. Test this first on every firmware and loader.
|
||||
|
||||
- Starts the HTTP file manager on port `5905`.
|
||||
- Starts the integrated archive daemon for RAR, 7z, split 7z, and ZIP
|
||||
extraction.
|
||||
- **Installs/refreshes the Media home tile** (`BFPL00001`, category 65536,
|
||||
deeplink `http://127.0.0.1:5905/`) from embedded `assets-app/param.json` +
|
||||
`icon0.png` via AppInstUtil (same idea as Payload Manager `app_installer.c`).
|
||||
- Can **load other payloads** by streaming them to elfldr (`127.0.0.1:9021`)
|
||||
via `/api/fs/launch`.
|
||||
- Optional **recovery**: if embedded install fails and
|
||||
`/data/BFpilot/bfpilot-launcher-installer.elf` exists as a **file**, injects
|
||||
it once via elfldr.
|
||||
- Writes boot/runtime/crash diagnostics under **`/data/BFpilot` only**.
|
||||
- Writes integrated archive job/status/logs under
|
||||
`/data/BFpilot/archive-integrated`.
|
||||
via `/api/fs/launch` (no AppInst).
|
||||
- Can **auto-bootstrap the home tile** by injecting
|
||||
`bfpilot-launcher-installer.elf` once if that ELF is found under
|
||||
`/data/BFpilot/` (or USB/homebrew paths). Does **not** link AppInstUtil.
|
||||
- Does not depend on SystemService, UserService, AppInstUtil, or `kernel_sys`.
|
||||
- Writes boot/runtime/crash diagnostics under `/data/BFpilot` only.
|
||||
|
||||
### `bfpilot-launcher-installer.elf` (optional recovery)
|
||||
### `bfpilot-launcher-installer.elf`
|
||||
|
||||
Isolated payload for installing or refreshing `/user/app/BFPL00001` if the
|
||||
main ELF’s embedded install fails on a particular loader. Place the **file**
|
||||
at `/data/BFpilot/bfpilot-launcher-installer.elf` — do **not** create a
|
||||
sibling directory tree under `/data`.
|
||||
Isolated payload for installing or refreshing `/user/app/BFPL00001` as a
|
||||
**Media** category home tile (`applicationCategoryType: 65536`).
|
||||
|
||||
- Direct-links AppInstUtil + UserService + SystemService + `kernel_sys`.
|
||||
- Direct-links `kernel_sys`, SystemService, UserService, and AppInstUtil.
|
||||
- Temporarily sets authid `0x4801000000000013`.
|
||||
- Stages embedded param/icon; Media category `65536`.
|
||||
- Stages embedded `param.json` and `icon0.png`.
|
||||
- Registers the tile through AppInst; terminates AppInst when done.
|
||||
- Logs to `/data/BFpilot/launcher-installer.log`.
|
||||
|
||||
The tile target is:
|
||||
@@ -49,15 +44,6 @@ deeplinkUri: http://127.0.0.1:5905/
|
||||
|
||||
Canonical: **`/data/BFpilot`**
|
||||
|
||||
Do not introduce:
|
||||
|
||||
- `/data/bfpilot`
|
||||
- `/data/BFPILOT`
|
||||
- `/data/bfpilot-launcher-installer` (as a directory root)
|
||||
|
||||
Installer/helper ELFs are files under the canonical dir, not separate trees.
|
||||
|
||||
## Archive worker
|
||||
|
||||
`bfpilot-archive-worker.elf` is a build diagnostic / standalone extractor.
|
||||
Integrated extract in `bfpilot.elf` is the normal path.
|
||||
Do not introduce alternate roots (`/data/bfpilot`, `/data/BFPILOT`, or an
|
||||
installer directory tree). The installer ELF is a **file** under the
|
||||
canonical dir when used for auto-bootstrap.
|
||||
+56
-43
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compatibility checks for BFpilot ELFs.
|
||||
# v0.4.1+: main bfpilot.elf embeds Media-tile AppInst (Payload Manager model).
|
||||
# Main bfpilot.elf must NOT link AppInst (breaks loaders). Tile install is the
|
||||
# separate bfpilot-launcher-installer.elf only (Media category 65536).
|
||||
set -euo pipefail
|
||||
|
||||
find_tool() {
|
||||
@@ -59,6 +60,20 @@ needed_libraries() {
|
||||
}
|
||||
|
||||
|
||||
check_no_direct_appinst_import() {
|
||||
local file="$1"
|
||||
local needed
|
||||
needed="$(needed_libraries "${file}" || true)"
|
||||
if printf '%s\n' "${needed}" |
|
||||
grep -E 'libSceAppInstUtil\.sprx|libSceSystemService\.sprx|libSceUserService\.sprx|libkernel_sys\.sprx' >/dev/null; then
|
||||
echo
|
||||
echo "inspect-imports: unsafe direct launcher import in ${file}" >&2
|
||||
printf '%s\n' "${needed}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
for file in "$@"; do
|
||||
echo
|
||||
echo "== ${file} =="
|
||||
@@ -92,48 +107,47 @@ for file in "$@"; do
|
||||
"${STRINGS}" "${file}" | grep -Ei '(^|[^A-Za-z0-9_])(sce|Sce|AppInst|SystemService|UserService|BFPL00001|app_installer)' || true
|
||||
done
|
||||
|
||||
# Primary ELF must include Media-tile AppInst (single-ELF model).
|
||||
elf_has_string() {
|
||||
local file="$1"
|
||||
local needle="$2"
|
||||
# -a: treat as text on Windows/msys; tolerate null-containing streams
|
||||
if "${STRINGS}" "${file}" 2>/dev/null | grep -aF "${needle}" >/dev/null 2>&1; then
|
||||
return 0
|
||||
for file in "$@"; do
|
||||
case "${file}" in
|
||||
bfpilot-launcher-installer.elf|./bfpilot-launcher-installer.elf)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [[ -f "${file}" ]]; then
|
||||
check_no_direct_appinst_import "${file}"
|
||||
fi
|
||||
# Fallback: raw bytes (PowerShell/host often more reliable than llvm-strings path)
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "${file}" "${needle}" <<'PY'
|
||||
import sys
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
sys.exit(0 if sys.argv[2].encode() in data else 1)
|
||||
PY
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
done
|
||||
|
||||
if [[ -f bfpilot.elf ]]; then
|
||||
main_needed="$(needed_libraries bfpilot.elf || true)"
|
||||
for required in libSceAppInstUtil.sprx libSceUserService.sprx; do
|
||||
if ! printf '%s\n' "${main_needed}" | grep -q "${required}"; then
|
||||
echo
|
||||
echo "inspect-imports: bfpilot.elf missing required ${required} (Media tile)" >&2
|
||||
printf '%s\n' "${main_needed}" >&2
|
||||
if "${STRINGS}" bfpilot.elf 2>/dev/null |
|
||||
grep -aE 'libSceAppInstUtil\.sprx|sceAppInstUtilInitialize' >"${TMP_DIR}/bfpilot-forbidden-imports.txt" 2>/dev/null; then
|
||||
echo
|
||||
echo "inspect-imports: forbidden launcher/AppInst content in bfpilot.elf" >&2
|
||||
cat "${TMP_DIR}/bfpilot-forbidden-imports.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Scrub may rewrite sprx name; still reject live AppInst symbols
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
if python3 - <<'PY'
|
||||
import pathlib
|
||||
data = pathlib.Path("bfpilot.elf").read_bytes()
|
||||
# NEEDED-style / live install entry points must not remain
|
||||
needles = [b"sceAppInstUtilInitialize", b"sceAppInstUtilAppInstallAll"]
|
||||
for n in needles:
|
||||
if n in data:
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(0)
|
||||
PY
|
||||
then
|
||||
:
|
||||
else
|
||||
echo "inspect-imports: forbidden AppInst symbols in bfpilot.elf" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if ! elf_has_string bfpilot.elf 'BFPL00001'; then
|
||||
echo "inspect-imports: bfpilot.elf missing BFPL00001 title id" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! elf_has_string bfpilot.elf 'applicationCategoryType'; then
|
||||
echo "inspect-imports: bfpilot.elf missing embedded param applicationCategoryType" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "inspect-imports: bfpilot.elf Media tile AppInst imports OK"
|
||||
echo "inspect-imports: bfpilot.elf free of AppInst (file manager only) OK"
|
||||
fi
|
||||
|
||||
# Optional recovery installer still needs full privileged set.
|
||||
if [[ -f bfpilot-launcher-installer.elf ]]; then
|
||||
installer_needed="$(needed_libraries bfpilot-launcher-installer.elf || true)"
|
||||
for required in libkernel_sys.sprx libSceSystemService.sprx \
|
||||
@@ -144,16 +158,15 @@ if [[ -f bfpilot-launcher-installer.elf ]]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "inspect-imports: launcher-installer AppInst imports OK"
|
||||
fi
|
||||
|
||||
# Archive worker must NOT pull AppInst (file manager isolation for worker).
|
||||
if [[ -f bfpilot-archive-worker.elf ]]; then
|
||||
worker_needed="$(needed_libraries bfpilot-archive-worker.elf || true)"
|
||||
if printf '%s\n' "${worker_needed}" | grep -E 'libSceAppInstUtil\.sprx' >/dev/null; then
|
||||
echo "inspect-imports: archive-worker must not link AppInstUtil" >&2
|
||||
if ! "${STRINGS}" bfpilot-launcher-installer.elf 2>/dev/null | grep -aF 'BFPL00001' >/dev/null; then
|
||||
echo "inspect-imports: installer missing BFPL00001" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! "${STRINGS}" bfpilot-launcher-installer.elf 2>/dev/null | grep -aF '65536' >/dev/null; then
|
||||
echo "inspect-imports: installer missing Media category 65536" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "inspect-imports: launcher-installer Media AppInst OK"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Structural checks for v0.4.1 UX / single data dir / Media tile parity.
|
||||
* Drives real shipped sources (no mocks of the paths under test).
|
||||
* Structural checks for v0.4.1: UX, single data dir, separate Media installer.
|
||||
*/
|
||||
import assert from 'assert'
|
||||
import fs from 'fs'
|
||||
@@ -12,36 +11,26 @@ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8')
|
||||
|
||||
const html = read('assets/files.html')
|
||||
const pathsH = read('src/paths.h')
|
||||
const appInst = read('src/app_installer.c')
|
||||
const liteMain = read('src/lite_main.c')
|
||||
const param = read('assets-app/param.json')
|
||||
const makefile = read('Makefile')
|
||||
const tileBoot = read('src/tile_bootstrap.c')
|
||||
const launcher = read('src/launcher_installer_force_main.c')
|
||||
const transfer = read('src/transfer.c')
|
||||
const pathOps = read('src/path_ops.c')
|
||||
const versionH = read('src/version.h')
|
||||
|
||||
// --- Action bar: wrap, not horizontal-scroll-only ---
|
||||
assert(html.includes('.actions'), 'actions class present')
|
||||
assert(/\.actions\s*\{[^}]*flex-wrap:\s*wrap/s.test(html), 'actions flex-wrap wrap')
|
||||
assert(!/\.actions\s*\{[^}]*overflow-x:\s*auto/s.test(html), 'actions must not use overflow-x auto')
|
||||
assert(/overflow-x:\s*hidden/.test(html), 'actions overflow-x hidden present')
|
||||
// mobile media query must not force nowrap on .actions
|
||||
assert(!/\.actions\s*\{[^}]*overflow-x:\s*auto/s.test(html), 'no overflow-x auto on .actions')
|
||||
assert(/overflow-x:\s*hidden/.test(html), 'overflow-x hidden present')
|
||||
const mediaActions = html.match(/@media[^{]+\{[\s\S]*?\.actions\s*\{([^}]*)\}/)
|
||||
if (mediaActions) {
|
||||
assert(!/flex-wrap:\s*nowrap/.test(mediaActions[1]), 'media .actions must not force nowrap')
|
||||
}
|
||||
|
||||
// --- Single data root ---
|
||||
assert(pathsH.includes('#define BFPILOT_DATA_DIR "/data/BFpilot"') ||
|
||||
pathsH.includes('#define BFPILOT_DATA_DIR "/data/BFpilot"') ||
|
||||
/#define\s+BFPILOT_DATA_DIR\s+"\/data\/BFpilot"/.test(pathsH),
|
||||
'canonical data dir')
|
||||
assert(!pathsH.includes('"/data/bfpilot"'), 'no lowercase data root in paths.h')
|
||||
assert(!pathsH.includes('"/data/BFPILOT"'), 'no UPPERCASE data root in paths.h')
|
||||
assert(!pathsH.includes('"/data/bfpilot-launcher-installer"'), 'no installer data root')
|
||||
|
||||
// Sources must not mkdir alternate roots (scan C/C++ under src)
|
||||
assert(/#define\s+BFPILOT_DATA_DIR\s+"\/data\/BFpilot"/.test(pathsH), 'canonical data dir')
|
||||
const srcFiles = []
|
||||
function walk(d) {
|
||||
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
@@ -59,23 +48,28 @@ for (const f of srcFiles) {
|
||||
`no installer dir root in ${path.relative(root, f)}`)
|
||||
}
|
||||
|
||||
// --- Media tile in primary payload (pldmgr parity) ---
|
||||
// --- Separate launcher model (main must NOT embed AppInst install) ---
|
||||
assert(!fs.existsSync(path.join(root, 'src/app_installer.c')), 'no app_installer.c in main tree')
|
||||
assert(!makefile.includes('app_installer.c'), 'app_installer not in Makefile WEB_SRCS')
|
||||
assert(makefile.includes('BFPILOT_ENABLE_LAUNCHER=0') || makefile.includes('ENABLE_LAUNCHER=0'),
|
||||
'launcher disabled in main flags')
|
||||
assert(makefile.includes('scrub_main_payload.py'), 'scrub main payload for AppInst fingerprints')
|
||||
assert(liteMain.includes('bfpilot_tile_bootstrap_try'), 'tile bootstrap for separate installer')
|
||||
assert(liteMain.includes('separate_payload') || liteMain.includes('separate'),
|
||||
'runtime separate_payload messaging')
|
||||
assert(!liteMain.includes('bfpilot_install_app_if_needed'), 'main must not call embedded install')
|
||||
|
||||
// --- Media tile lives in separate installer ---
|
||||
const paramObj = JSON.parse(param)
|
||||
assert.strictEqual(paramObj.applicationCategoryType, 65536, 'Media category 65536')
|
||||
assert.strictEqual(paramObj.titleId, 'BFPL00001')
|
||||
assert.ok(String(paramObj.deeplinkUri).includes('5905'))
|
||||
assert(appInst.includes('bfpilot_install_app_if_needed'), 'install API')
|
||||
assert(appInst.includes('INCASSET') || appInst.includes('incbin'), 'embedded assets')
|
||||
assert(appInst.includes('assets-app/param.json'), 'param embedded')
|
||||
assert(liteMain.includes('bfpilot_install_app_if_needed'), 'main calls install')
|
||||
assert(makefile.includes('app_installer.c'), 'app_installer linked in main')
|
||||
assert(makefile.includes('SceAppInstUtil') || makefile.includes('lSceAppInstUtil'),
|
||||
'AppInst linked')
|
||||
// recovery installer path is a file under data dir only
|
||||
assert(launcher.includes('BFPILOT_APP_TITLE_ID') || launcher.includes('BFPL00001') ||
|
||||
launcher.includes('bfpilot_param_json'), 'installer embeds app assets')
|
||||
assert(launcher.includes('sceAppInstUtilInitialize') || launcher.includes('AppInst'),
|
||||
'installer uses AppInst')
|
||||
assert(tileBoot.includes('BFPILOT_INSTALLER_ELF_PATH') ||
|
||||
tileBoot.includes('/data/BFpilot/bfpilot-launcher-installer.elf'))
|
||||
assert(!tileBoot.includes('/data/homebrew/bfpilot-launcher-installer.elf'),
|
||||
'no loose /data/homebrew installer root path')
|
||||
|
||||
// --- Prior issue surface (#7-12, #10) ---
|
||||
assert(html.includes('openPath(entryPath(entry))'), '#7')
|
||||
@@ -86,4 +80,4 @@ assert(html.includes('chmodSelected') || html.includes('id="chmod"'), '#10 ui')
|
||||
assert(html.includes('row-checkbox') && html.includes('select-all-checkbox'), '#11')
|
||||
assert(versionH.includes('v0.4.1') && makefile.includes('v0.4.1'), '#12')
|
||||
|
||||
console.log('v0.4.1 UX/tile/data-dir host checks passed')
|
||||
console.log('v0.4.1 separate-launcher + UX host checks passed')
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* PS5 Media home-tile installer for BFpilot (embedded in main ELF).
|
||||
* Same pattern as Payload Manager app_installer.c + websrv AppInstUtil.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "app_installer.h"
|
||||
#include "diag.h"
|
||||
#include "notify.h"
|
||||
#include "paths.h"
|
||||
|
||||
#define BFPILOT_APPINST_AUTHID UINT64_C(0x4801000000000013)
|
||||
|
||||
#define INCASSET(name, file) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".global " #name "\n" \
|
||||
".global " #name "_end\n" \
|
||||
".global " #name "_size\n" \
|
||||
".align 16\n" #name ":\n" \
|
||||
".incbin \"" file "\"\n" #name "_end:\n" #name "_size:\n" \
|
||||
".quad " #name "_end - " #name "\n" \
|
||||
".previous\n"); \
|
||||
extern const uint8_t name[]; \
|
||||
extern const size_t name##_size
|
||||
|
||||
INCASSET(bfpilot_param_json, "assets-app/param.json");
|
||||
INCASSET(bfpilot_icon0_png, "assets-app/icon0.png");
|
||||
|
||||
typedef int (*app_install_title_dir_fn)(const char *, const char *, void *);
|
||||
|
||||
int sceUserServiceInitialize(void *);
|
||||
void sceUserServiceTerminate(void);
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilTerminate(void);
|
||||
int sceAppInstUtilAppInstallAll(void *);
|
||||
|
||||
static int
|
||||
mkdir_if_needed(const char *path) {
|
||||
if(mkdir(path, 0755) == 0) return 0;
|
||||
return errno == EEXIST ? 0 : -errno;
|
||||
}
|
||||
|
||||
static int
|
||||
write_file(const char *path, const uint8_t *data, size_t size) {
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if(fd < 0) return -errno;
|
||||
|
||||
size_t done = 0;
|
||||
while(done < size) {
|
||||
ssize_t wr = write(fd, data + done, size - done);
|
||||
if(wr < 0) {
|
||||
int err = errno;
|
||||
close(fd);
|
||||
return -err;
|
||||
}
|
||||
done += (size_t)wr;
|
||||
}
|
||||
if(close(fd) != 0) return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
needs_update(const char *path, const uint8_t *expected, size_t expected_size) {
|
||||
struct stat st;
|
||||
uint8_t *buf;
|
||||
FILE *f;
|
||||
int mismatch;
|
||||
|
||||
if(stat(path, &st) != 0) return 1;
|
||||
if((size_t)st.st_size != expected_size) return 1;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if(!f) return 1;
|
||||
buf = (uint8_t *)malloc(expected_size);
|
||||
if(!buf) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if(fread(buf, 1, expected_size, f) != expected_size) {
|
||||
free(buf);
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
mismatch = memcmp(buf, expected, expected_size) != 0;
|
||||
free(buf);
|
||||
return mismatch;
|
||||
}
|
||||
|
||||
int
|
||||
bfpilot_install_app_if_needed(void) {
|
||||
char app_dir[256];
|
||||
char sce_sys_dir[256];
|
||||
char param_path[256];
|
||||
char icon_path[256];
|
||||
struct stat st;
|
||||
int update_needed = 0;
|
||||
int final_rc = -1;
|
||||
pid_t pid = getpid();
|
||||
uint64_t original_authid;
|
||||
int set_authid_rc;
|
||||
int restore_authid_rc = BFPILOT_DIAG_SKIPPED;
|
||||
int user_service_rc;
|
||||
int appinst_init_rc;
|
||||
int title_dir_rc = BFPILOT_DIAG_SKIPPED;
|
||||
int install_all_rc = BFPILOT_DIAG_SKIPPED;
|
||||
uint32_t appinst_handle = 0;
|
||||
app_install_title_dir_fn install_title_dir = NULL;
|
||||
|
||||
snprintf(app_dir, sizeof(app_dir), BFPILOT_APP_ROOT "/%s",
|
||||
BFPILOT_APP_TITLE_ID);
|
||||
snprintf(sce_sys_dir, sizeof(sce_sys_dir), "%s/sce_sys", app_dir);
|
||||
snprintf(param_path, sizeof(param_path), "%s/param.json", sce_sys_dir);
|
||||
snprintf(icon_path, sizeof(icon_path), "%s/icon0.png", sce_sys_dir);
|
||||
|
||||
if(stat(app_dir, &st) != 0) {
|
||||
update_needed = 1;
|
||||
} else {
|
||||
if(needs_update(param_path, bfpilot_param_json, bfpilot_param_json_size))
|
||||
update_needed = 1;
|
||||
if(needs_update(icon_path, bfpilot_icon0_png, bfpilot_icon0_png_size))
|
||||
update_needed = 1;
|
||||
}
|
||||
|
||||
if(!update_needed) {
|
||||
bfpilot_log("app installer: Media tile already up to date title_id=%s",
|
||||
BFPILOT_APP_TITLE_ID);
|
||||
bfpilot_diag_set_launcher_status("up_to_date", 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bfpilot_log("app installer: installing/updating Media tile title_id=%s "
|
||||
"category=65536 deeplink=%s",
|
||||
BFPILOT_APP_TITLE_ID, BFPILOT_DEEPLINK_URI);
|
||||
bfpilot_notify("BFpilot", "Installing Media home tile...");
|
||||
|
||||
original_authid = kernel_get_ucred_authid(pid);
|
||||
user_service_rc = sceUserServiceInitialize(NULL);
|
||||
bfpilot_log("app installer: sceUserServiceInitialize rc=0x%08x",
|
||||
user_service_rc);
|
||||
|
||||
set_authid_rc = kernel_set_ucred_authid(pid, BFPILOT_APPINST_AUTHID);
|
||||
bfpilot_log("app installer: set authid rc=0x%08x", set_authid_rc);
|
||||
if(set_authid_rc != 0) {
|
||||
bfpilot_diag_set_launcher_status("authid_failed", set_authid_rc);
|
||||
sceUserServiceTerminate();
|
||||
bfpilot_notify("BFpilot", "Media tile: auth setup failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
appinst_init_rc = sceAppInstUtilInitialize();
|
||||
bfpilot_log("app installer: AppInst init rc=0x%08x", appinst_init_rc);
|
||||
if(appinst_init_rc != 0) {
|
||||
if(original_authid != 0)
|
||||
restore_authid_rc = kernel_set_ucred_authid(pid, original_authid);
|
||||
bfpilot_diag_set_launcher_status("appinst_init_failed", appinst_init_rc);
|
||||
sceUserServiceTerminate();
|
||||
bfpilot_notify("BFpilot", "Media tile: AppInst init failed");
|
||||
(void)restore_authid_rc;
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)mkdir_if_needed(app_dir);
|
||||
(void)mkdir_if_needed(sce_sys_dir);
|
||||
|
||||
if(write_file(param_path, bfpilot_param_json, bfpilot_param_json_size) != 0 ||
|
||||
write_file(icon_path, bfpilot_icon0_png, bfpilot_icon0_png_size) != 0) {
|
||||
bfpilot_log("app installer: failed writing param/icon under %s", app_dir);
|
||||
sceAppInstUtilTerminate();
|
||||
if(original_authid != 0)
|
||||
(void)kernel_set_ucred_authid(pid, original_authid);
|
||||
sceUserServiceTerminate();
|
||||
bfpilot_diag_set_launcher_status("write_failed", -1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &appinst_handle) == 0) {
|
||||
install_title_dir = (app_install_title_dir_fn)kernel_dynlib_resolve(
|
||||
-1, appinst_handle, "Wudg3Xe3heE");
|
||||
}
|
||||
|
||||
if(install_title_dir) {
|
||||
title_dir_rc =
|
||||
install_title_dir(BFPILOT_APP_TITLE_ID, BFPILOT_APP_PARENT, NULL);
|
||||
bfpilot_log("app installer: AppInstallTitleDir rc=0x%08x", title_dir_rc);
|
||||
}
|
||||
if(title_dir_rc != 0) {
|
||||
install_all_rc = sceAppInstUtilAppInstallAll(NULL);
|
||||
bfpilot_log("app installer: AppInstallAll fallback rc=0x%08x",
|
||||
install_all_rc);
|
||||
}
|
||||
|
||||
if(title_dir_rc == 0 || install_all_rc == 0) final_rc = 0;
|
||||
|
||||
if(original_authid != 0)
|
||||
restore_authid_rc = kernel_set_ucred_authid(pid, original_authid);
|
||||
(void)restore_authid_rc;
|
||||
|
||||
sceAppInstUtilTerminate();
|
||||
sceUserServiceTerminate();
|
||||
|
||||
if(final_rc == 0) {
|
||||
bfpilot_diag_set_launcher_status("installed", 0);
|
||||
bfpilot_notify("BFpilot", "Media home tile ready");
|
||||
bfpilot_log("app installer: Media tile installed/refreshed ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
bfpilot_diag_set_launcher_status("failed", final_rc);
|
||||
bfpilot_notify("BFpilot", "Media tile install failed (see log)");
|
||||
return final_rc;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
* Media-category home tile install (pldmgr-style), callable from main ELF.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/* Install or refresh BFPL00001 under /user/app if param/icon changed.
|
||||
* Returns 0 on success / already up to date, negative or nonzero on failure.
|
||||
* Non-fatal for the file manager: callers should continue serving the UI.
|
||||
*/
|
||||
int bfpilot_install_app_if_needed(void);
|
||||
+10
-15
@@ -22,7 +22,6 @@
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "app_installer.h"
|
||||
#include "boot_marker.h"
|
||||
#include "diag.h"
|
||||
#include "notify.h"
|
||||
@@ -299,6 +298,9 @@ on_web_ready(unsigned short port, void *arg) {
|
||||
state->notified = 1;
|
||||
}
|
||||
|
||||
/* Home Media tile via separate installer ELF + elfldr — AppInst stays out of main. */
|
||||
bfpilot_tile_bootstrap_try();
|
||||
|
||||
debug_notify("BFpilot debug", "web server ready");
|
||||
}
|
||||
|
||||
@@ -329,16 +331,16 @@ main(int argc, char **argv) {
|
||||
detect_lan_ip(ready.ip, sizeof(ready.ip));
|
||||
ready.port = runtime_port_requested(argc, argv);
|
||||
websrv_set_runtime_port(ready.port);
|
||||
websrv_set_runtime_diag(0);
|
||||
websrv_set_runtime_diag(1);
|
||||
bfpilot_log("runtime web_port=%u", (unsigned int)ready.port);
|
||||
bfpilot_log("runtime launcher=embedded_media_tile");
|
||||
bfpilot_log("runtime launcher=separate_payload");
|
||||
|
||||
puts(".----------------------------------------------.");
|
||||
puts("| BFpilot |");
|
||||
printf("| %-18s browser file manager |\n", VERSION_TAG);
|
||||
puts("'----------------------------------------------'");
|
||||
puts("");
|
||||
puts(" active: file manager + Media home tile (single ELF)");
|
||||
puts(" active: standalone web file manager");
|
||||
printf(" mode: %s\n", BFPILOT_BUILD_MODE);
|
||||
puts(" scope: browse, upload, download, copy, move, delete, rename, mkdir");
|
||||
#if BFPILOT_ENABLE_INTEGRATED_ARCHIVE
|
||||
@@ -346,7 +348,7 @@ main(int argc, char **argv) {
|
||||
#else
|
||||
puts(" archive: prepare jobs only; inject bfpilot-archive-worker.elf to extract");
|
||||
#endif
|
||||
puts(" ps5 app: Media category tile install from this payload (65536)");
|
||||
puts(" ps5 app: Media tile via separate bfpilot-launcher-installer.elf");
|
||||
puts(" payloads: double-click .elf/.bin to load via elfldr :9021");
|
||||
printf(" data dir: %s (only)\n", BFPILOT_DATA_DIR);
|
||||
printf(" web ui: http://%s:%u/\n", ready.ip, (unsigned int)ready.port);
|
||||
@@ -366,17 +368,10 @@ main(int argc, char **argv) {
|
||||
#endif
|
||||
|
||||
bfpilot_diag_set_service_rcs(BFPILOT_DIAG_SKIPPED, BFPILOT_DIAG_SKIPPED);
|
||||
bfpilot_diag_set_launcher_status("separate_payload", BFPILOT_DIAG_SKIPPED);
|
||||
bfpilot_log("sceNetCtlInit rc=skipped");
|
||||
|
||||
/* Payload Manager model: install Media home tile from the main ELF. */
|
||||
{
|
||||
int install_rc = bfpilot_install_app_if_needed();
|
||||
bfpilot_log("embedded Media tile install rc=%d", install_rc);
|
||||
if(install_rc != 0) {
|
||||
/* Optional recovery: separate installer under /data/BFpilot/ only. */
|
||||
bfpilot_tile_bootstrap_try();
|
||||
}
|
||||
}
|
||||
bfpilot_log("sceUserServiceInitialize rc=skipped");
|
||||
bfpilot_log("launcher skipped: separate payload (inject installer for Media tile)");
|
||||
|
||||
bfpilot_checkpoint("handoff check");
|
||||
debug_notify("BFpilot debug", "handoff check");
|
||||
|
||||
+14
-7
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Optional recovery: if embedded Media-tile install was skipped/failed and a
|
||||
* separate installer ELF exists under the single data dir, push it to elfldr.
|
||||
* Never creates alternate /data roots (bfpilot / BFPILOT / bfpilot-launcher-installer).
|
||||
* After the web UI is up, optionally inject the separate Media-tile installer
|
||||
* if present under the single data dir (or common USB/homebrew paths).
|
||||
* Main bfpilot.elf never links AppInst — that stays in the installer payload.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
@@ -16,9 +16,12 @@
|
||||
#include "payload_launch.h"
|
||||
#include "tile_bootstrap.h"
|
||||
|
||||
/* Only the canonical data tree — installer is a file, not a sibling folder. */
|
||||
static const char *const k_installer_candidates[] = {
|
||||
BFPILOT_INSTALLER_ELF_PATH,
|
||||
"/data/homebrew/BFpilot/bfpilot-launcher-installer.elf",
|
||||
"/mnt/usb0/BFpilot/bfpilot-launcher-installer.elf",
|
||||
"/mnt/usb0/bfpilot-launcher-installer.elf",
|
||||
"/mnt/usb1/bfpilot-launcher-installer.elf",
|
||||
NULL,
|
||||
};
|
||||
|
||||
@@ -48,17 +51,21 @@ bfpilot_tile_bootstrap_try(void) {
|
||||
}
|
||||
|
||||
if(!found) {
|
||||
bfpilot_log("tile bootstrap: optional installer ELF not under %s (ok)",
|
||||
bfpilot_log("tile bootstrap: installer ELF not found under %s "
|
||||
"(inject bfpilot-launcher-installer.elf separately for Media tile)",
|
||||
BFPILOT_DATA_DIR);
|
||||
return;
|
||||
}
|
||||
|
||||
bfpilot_log("tile bootstrap: recovery inject %s", found);
|
||||
bfpilot_log("tile bootstrap: injecting %s", found);
|
||||
rc = bfpilot_launch_payload_path(found);
|
||||
if(rc == 0) {
|
||||
bfpilot_notify("BFpilot", "recovery tile installer started");
|
||||
bfpilot_notify("BFpilot", "home tile installer started");
|
||||
bfpilot_log("tile bootstrap: installer sent to elfldr ok");
|
||||
} else {
|
||||
bfpilot_log("tile bootstrap: inject failed rc=%d", rc);
|
||||
if(rc == -ECONNREFUSED) {
|
||||
bfpilot_notify("BFpilot", "tile install skipped (elfldr offline)");
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -17,11 +17,11 @@
|
||||
#endif
|
||||
|
||||
#ifndef BFPILOT_ENABLE_LAUNCHER
|
||||
#define BFPILOT_ENABLE_LAUNCHER 1
|
||||
#define BFPILOT_ENABLE_LAUNCHER 0
|
||||
#endif
|
||||
|
||||
#ifndef BFPILOT_DISABLE_LAUNCHER
|
||||
#define BFPILOT_DISABLE_LAUNCHER 0
|
||||
#define BFPILOT_DISABLE_LAUNCHER 1
|
||||
#endif
|
||||
|
||||
#if BFPILOT_DISABLE_LAUNCHER
|
||||
|
||||
Reference in new issue
Block a user