PS4key import

This commit is contained in:
whowechina
2026-04-13 12:32:30 +08:00
parent f0924808d9
commit fcfea26e6a
6 changed files with 250 additions and 223 deletions
+18 -11
View File
@@ -67,7 +67,16 @@ static void disp_hid()
{
const char *joy_map[] = {"Switch", "Steam", "Arcade", "PS4"};
printf("[HID]\n");
printf(" Keymap: %s\n", joy_map[diva_cfg->hid.joy_map % 4]);
int joy = diva_cfg->hid.joy_map % 4;
const char *key_info = "\0";
if (joy == 3) {
const ps4key_t *key = (const ps4key_t *)savedata_get_global();
key_info = ps4key_key_valid(key) ? " (Key loaded)" : " (No key, disconnected every 8 minutes)";
}
printf(" Keymap: %s%s\n", joy_map[joy], key_info);
}
void handle_display(int argc, char *argv[])
@@ -431,7 +440,7 @@ static void handle_factory_reset()
static void handle_ps4key(int argc, char *argv[])
{
static ps4key_data_t ps4key_data;
static ps4key_t key;
const char *usage = "Usage: ps4key <serialized_key|clear>\n";
if (argc != 1) {
@@ -440,23 +449,21 @@ static void handle_ps4key(int argc, char *argv[])
}
if (strncasecmp(argv[0], "clear", strlen(argv[0])) == 0) {
ps4key_clear_data(&ps4key_data);
memset(&key, 0, sizeof(key));
savedata_clear_global();
printf("PS4 key data cleared.\n");
return;
}
const char *error = NULL;
if (!ps4key_parse_text(argv[0], &ps4key_data, &error)) {
printf("PS4 key import failed: %s\n", error ? error : "Unknown error.");
const char *error = "Unknown";
if (!ps4key_parse_text(argv[0], &key, &error)) {
printf("PS4 key import failed: %s\n", error);
return;
}
savedata_write_global(&ps4key_data, sizeof(ps4key_data));
printf("PS4 key stored: serial=%s pem=%u bytes sig=%u bytes.\n",
ps4key_data.serial,
(unsigned)ps4key_data.pem_len,
(unsigned)ps4key_data.sig_len);
savedata_write_global(&key, sizeof(key));
printf("PS4 key stored: Serial: %s, PEM: %d bytes, sig: %d bytes\n",
ps4key_get_serial(&key), key.pem_len - 1, key.sig_len);
}
void commands_init()
+115 -95
View File
@@ -1,16 +1,13 @@
#include "ps4key.h"
/*
* PS4 Key import and de-serialization
* WHowe <github.com/whowechina>
*/
#include <ctype.h>
#include <stdio.h>
#include <string.h>
typedef struct __attribute__((packed)) {
uint8_t magic[4];
uint8_t version;
uint16_t serial_len;
uint16_t pem_len;
uint16_t sig_len;
uint32_t crc32;
} ps4key_wire_header_t;
#include "ps4key.h"
static uint32_t crc32_calc(const uint8_t *data, size_t size)
{
@@ -109,133 +106,156 @@ static bool base64_decode(const char *input, uint8_t *output,
static bool serial_is_valid(const uint8_t *serial, size_t size)
{
if (size != PS4KEY_SERIAL_LENGTH) {
if (size != (PS4KEY_SERIAL_LENGTH + 1)) {
return false;
}
for (size_t i = 0; i < size; i++) {
for (size_t i = 0; i < PS4KEY_SERIAL_LENGTH; i++) {
if (!isdigit(serial[i])) {
return false;
}
}
if (serial[PS4KEY_SERIAL_LENGTH] != '\0') {
return false;
}
return true;
}
static bool pem_is_valid(const char *pem)
{
return strstr(pem, "-----BEGIN RSA PRIVATE KEY-----") != NULL &&
strstr(pem, "-----END RSA PRIVATE KEY-----") != NULL;
return strstr(pem, "-----BEGIN RSA PRIVATE KEY-----") &&
strstr(pem, "-----END RSA PRIVATE KEY-----");
}
void ps4key_clear_data(ps4key_data_t *data)
static void set_error(const char **error, const char *message)
{
if (data == NULL) {
return;
if (error) {
*error = message;
}
memset(data, 0, sizeof(*data));
}
bool ps4key_parse_text(const char *text, ps4key_data_t *data, const char **error)
static bool ps4key_validate(const ps4key_t *key, const char **error)
{
static uint8_t decoded[sizeof(ps4key_wire_header_t) +
PS4KEY_SERIAL_LENGTH +
PS4KEY_PEM_MAX_LENGTH +
PS4KEY_SIG_MAX_LENGTH];
if ((text == NULL) || (data == NULL)) {
if (error) {
*error = "Missing input.";
}
if (key == NULL) {
set_error(error, "Missing input.");
return false;
}
if (strncmp(text, PS4KEY_TEXT_PREFIX, strlen(PS4KEY_TEXT_PREFIX)) != 0) {
if (error) {
*error = "Missing PS4K1 prefix.";
}
if ((key->magic != PS4KEY_STORAGE_MAGIC) ||
(key->version != PS4KEY_STORAGE_VERSION)) {
set_error(error, "Unexpected serialization header.");
return false;
}
size_t decoded_len = 0;
if (!base64_decode(text + strlen(PS4KEY_TEXT_PREFIX), decoded,
sizeof(decoded), &decoded_len)) {
if (error) {
*error = "Base64 decode failed.";
}
if ((key->serial_len != (PS4KEY_SERIAL_LENGTH + 1)) ||
(key->pem_len <= 1) || (key->pem_len > (PS4KEY_PEM_MAX_LENGTH + 1)) ||
(key->sig_len == 0) || (key->sig_len > PS4KEY_SIG_MAX_LENGTH)) {
set_error(error, "Serialized part lengths are invalid.");
return false;
}
if (decoded_len < sizeof(ps4key_wire_header_t)) {
if (error) {
*error = "Serialized data is too short.";
}
size_t payload_len = key->serial_len + key->pem_len + key->sig_len;
if (payload_len > sizeof(key->payload)) {
set_error(error, "Serialized length mismatch.");
return false;
}
const ps4key_wire_header_t *header = (const ps4key_wire_header_t *)decoded;
if ((memcmp(header->magic, "PS4K", 4) != 0) ||
(header->version != PS4KEY_STORAGE_VERSION)) {
if (error) {
*error = "Unexpected serialization header.";
}
if (crc32_calc(key->payload, payload_len) != key->crc32) {
set_error(error, "CRC32 mismatch.");
return false;
}
size_t payload_len = (size_t)header->serial_len + header->pem_len + header->sig_len;
if (decoded_len != sizeof(*header) + payload_len) {
if (error) {
*error = "Serialized length mismatch.";
}
if (!serial_is_valid(key->payload, key->serial_len)) {
set_error(error, "Invalid serial.");
return false;
}
if ((header->serial_len != PS4KEY_SERIAL_LENGTH) ||
(header->pem_len == 0) || (header->pem_len > PS4KEY_PEM_MAX_LENGTH) ||
(header->sig_len == 0) || (header->sig_len > PS4KEY_SIG_MAX_LENGTH)) {
if (error) {
*error = "Serialized part lengths are invalid.";
}
const uint8_t *pem_ptr = key->payload + key->serial_len;
if (pem_ptr[key->pem_len - 1] != '\0') {
set_error(error, "PEM data is not null terminated.");
return false;
}
const uint8_t *payload = decoded + sizeof(*header);
if (crc32_calc(payload, payload_len) != header->crc32) {
if (error) {
*error = "CRC32 mismatch.";
}
return false;
}
if (!serial_is_valid(payload, header->serial_len)) {
if (error) {
*error = "Serial must be exactly 16 decimal digits.";
}
return false;
}
const char *pem = (const char *)(payload + header->serial_len);
const uint8_t *sig = payload + header->serial_len + header->pem_len;
ps4key_clear_data(data);
data->magic = PS4KEY_STORAGE_MAGIC;
data->version = PS4KEY_STORAGE_VERSION;
data->serial_len = header->serial_len;
data->pem_len = header->pem_len;
data->sig_len = header->sig_len;
data->crc32 = header->crc32;
memcpy(data->serial, payload, header->serial_len);
memcpy(data->pem, pem, header->pem_len);
memcpy(data->sig, sig, header->sig_len);
data->serial[header->serial_len] = '\0';
data->pem[header->pem_len] = '\0';
if (!pem_is_valid(data->pem)) {
if (error) {
*error = "PEM content does not look like an RSA private key.";
}
ps4key_clear_data(data);
if (!pem_is_valid((const char *)pem_ptr)) {
set_error(error, "PEM content does not look like an RSA private key.");
return false;
}
return true;
}
}
bool ps4key_parse_text(const char *text, ps4key_t *key, const char **error)
{
static uint8_t decoded[sizeof(ps4key_t)];
if ((text == NULL) || (key == NULL)) {
set_error(error, "Missing input.");
return false;
}
if (strncmp(text, PS4KEY_TEXT_PREFIX, strlen(PS4KEY_TEXT_PREFIX)) != 0) {
set_error(error, "Missing PS4K1 prefix.");
return false;
}
size_t decoded_len = 0;
if (!base64_decode(text + strlen(PS4KEY_TEXT_PREFIX), decoded, sizeof(decoded), &decoded_len)) {
set_error(error, "Base64 decode failed.");
return false;
}
const size_t header_len = offsetof(ps4key_t, payload);
if (decoded_len < header_len) {
set_error(error, "Serialized data is too short.");
return false;
}
const ps4key_t *src = (const ps4key_t *)decoded;
size_t payload_len = src->serial_len + src->pem_len + src->sig_len;
if (decoded_len != header_len + payload_len) {
set_error(error, "Serialized length mismatch.");
return false;
}
memset(key, 0, sizeof(*key));
memcpy(key, decoded, decoded_len);
return ps4key_validate(key, error);
}
bool ps4key_key_valid(const ps4key_t *key)
{
return ps4key_validate(key, NULL);
}
const char *ps4key_get_serial(const ps4key_t *key)
{
if (key == NULL) {
return NULL;
}
return (const char *)key->payload;
}
const char *ps4key_get_pem(const ps4key_t *key)
{
if (key == NULL) {
return NULL;
}
return (const char *)(key->payload + key->serial_len);
}
const uint8_t *ps4key_get_sig(const ps4key_t *key)
{
if (key == NULL) {
return NULL;
}
return key->payload + key->serial_len + key->pem_len;
}
size_t ps4key_get_sig_len(const ps4key_t *key)
{
if (key == NULL) {
return 0;
}
return key->sig_len;
}
+14 -8
View File
@@ -1,5 +1,6 @@
/*
* PS4 key serialization support
* PS4 Key import and de-serialization
* WHowe <github.com/whowechina>
*/
#ifndef PS4KEY_H
@@ -15,20 +16,25 @@
#define PS4KEY_SERIAL_LENGTH 16
#define PS4KEY_PEM_MAX_LENGTH 2048
#define PS4KEY_SIG_MAX_LENGTH 512
#define PS4KEY_PAYLOAD_MAX_LENGTH (PS4KEY_SERIAL_LENGTH + 1 + PS4KEY_PEM_MAX_LENGTH + 1 + PS4KEY_SIG_MAX_LENGTH)
typedef struct __attribute__((packed)) {
uint32_t magic;
uint16_t version;
uint8_t version;
uint8_t reserved;
uint16_t serial_len;
uint16_t pem_len;
uint16_t sig_len;
uint32_t crc32;
char serial[PS4KEY_SERIAL_LENGTH + 1];
char pem[PS4KEY_PEM_MAX_LENGTH + 1];
uint8_t sig[PS4KEY_SIG_MAX_LENGTH];
} ps4key_data_t;
uint8_t payload[PS4KEY_PAYLOAD_MAX_LENGTH];
} ps4key_t;
void ps4key_clear_data(ps4key_data_t *data);
bool ps4key_parse_text(const char *text, ps4key_data_t *data, const char **error);
bool ps4key_parse_text(const char *text, ps4key_t *key, const char **error);
bool ps4key_key_valid(const ps4key_t *key);
const char *ps4key_get_serial(const ps4key_t *key);
const char *ps4key_get_pem(const ps4key_t *key);
const uint8_t *ps4key_get_sig(const ps4key_t *key);
size_t ps4key_get_sig_len(const ps4key_t *key);
#endif
+4 -11
View File
@@ -32,7 +32,7 @@ static uint32_t my_magic = 0xcafecafe;
#define SAVE_TIMEOUT_US 5000000
#define SAVE_SECTOR_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)
#define GLOBAL_SECTOR_NUM 2
#define GLOBAL_SECTOR_NUM 1
#define GLOBAL_SECTOR_SIZE (GLOBAL_SECTOR_NUM * FLASH_SECTOR_SIZE)
#define GLOBAL_SECTOR_OFFSET (SAVE_SECTOR_OFFSET - GLOBAL_SECTOR_SIZE)
@@ -164,16 +164,9 @@ void savedata_request(bool immediately)
}
}
void savedata_read_global(size_t offset, void *data, size_t size)
void *savedata_get_global()
{
if ((data == NULL) || (size == 0) || (offset >= GLOBAL_SECTOR_SIZE)) {
return;
}
if (size > GLOBAL_SECTOR_SIZE - offset) {
size = GLOBAL_SECTOR_SIZE - offset;
}
memcpy(data, (void *)(XIP_BASE + GLOBAL_SECTOR_OFFSET + offset), size);
return (void *)(XIP_BASE + GLOBAL_SECTOR_OFFSET);
}
static void do_write_global(void *param)
@@ -211,7 +204,7 @@ void savedata_write_global(const void *data, size_t size)
param[0] = (uintptr_t)global_write_buffer;
param[1] = padded;
printf("Program Global %8x ", GLOBAL_SECTOR_OFFSET);
printf("Program Global %08x ", GLOBAL_SECTOR_OFFSET);
if (flash_safe_execute(do_write_global, param, 1000) != PICO_OK) {
printf("Failed!\n");
} else {
+1 -1
View File
@@ -20,7 +20,7 @@ void savedata_loop();
void *savedata_alloc(size_t size, void *def, void (*after_load)());
void savedata_request(bool immediately);
void savedata_read_global(size_t offset, void *data, size_t size);
void *savedata_get_global();
void savedata_write_global(const void *data, size_t size);
void savedata_clear_global();
size_t savedata_global_size();
+98 -97
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PS4 Key Serialization for Diva Pico</title>
<title>PS4 Key Packer for Diva Pico</title>
<style>
:root {
--bg: #f4efe6;
@@ -47,8 +47,8 @@
h1 {
margin: 0;
font-size: clamp(2rem, 4vw, 3.8rem);
line-height: 0.95;
font-size: clamp(1.5rem, 3vw, 2.4rem);
line-height: 1.1;
letter-spacing: -0.04em;
}
@@ -69,13 +69,19 @@
overflow: hidden;
}
.grid {
.panel-main {
margin-top: 12px;
min-width: 800px;
}
.panel-top {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: stretch;
}
.section {
padding: 24px;
padding: 24px 24px 2px;
border-right: 1px solid var(--line);
}
@@ -83,6 +89,29 @@
border-right: none;
}
.button-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 0;
padding-top: 0;
}
.button-section {
padding: 2px 24px 24px;
border-right: 1px solid var(--line);
display: flex;
align-items: center;
}
.button-section:last-child {
border-right: none;
}
.button-section.left,
.button-section.right {
justify-content: center;
}
.section h2 {
margin: 0 0 8px;
font-size: 1rem;
@@ -130,43 +159,28 @@
resize: vertical;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
.output-actions {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
button {
border: none;
border-radius: 999px;
padding: 14px 22px;
padding: 14px 88px;
font: inherit;
font-weight: 700;
cursor: pointer;
box-shadow: 0 8px 24px rgba(15, 118, 110, 0.10);
transition: transform 180ms ease, box-shadow 180ms ease, opacity 180ms ease;
}
button:hover {
transform: translateY(-1px);
opacity: 0.96;
}
.primary {
color: #f7fffe;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
box-shadow: 0 14px 30px rgba(15, 118, 110, 0.24);
}
.secondary {
color: var(--ink);
background: rgba(255, 255, 255, 0.78);
border: 1px solid var(--line);
.primay {
color: var(--accent-strong);
background: linear-gradient(135deg, #e6f7f5 0%, #d2f1ed 100%);
border: 1px solid #b6e2de;
box-shadow: 0 8px 24px rgba(15, 118, 110, 0.10);
padding: 14px 88px;
}
@@ -178,7 +192,7 @@
.status.output-status {
margin-top: 0;
margin-bottom: 0;
text-align: right;
text-align: left;
}
.status.warn {
@@ -215,60 +229,48 @@
body {
padding: 20px 14px 32px;
}
.grid {
grid-template-columns: 1fr;
}
.section {
border-right: none;
border-bottom: 1px solid var(--line);
}
.section:last-child {
border-bottom: none;
}
}
</style>
</head>
<body>
<main>
<section class="hero">
<h1>PS4 Key Serialization for Diva Pico</h1>
<p class="subtitle">Pack <strong>signature.bin</strong>, <strong>serial.txt</strong>, and <strong>key.pem</strong> into one copy-friendly plain text string for the new <strong>ps4key</strong> CLI command. The format is <strong>PS4K1 + Base64(binary payload)</strong> with a CRC32 over the raw payload bytes.</p>
<h1>PS4 Key Packer for Diva Pico</h1>
</section>
<section class="panel grid">
<div class="section">
<h2>Input</h2>
<p>Provide the binary signature, the 16-digit serial, and the RSA private key text. Validation is intentionally strict to catch common mistakes before flashing.</p>
<section class="panel panel-main">
<div class="panel-top">
<div class="section">
<h2>Input</h2>
<p>Provide the binary signature, the serial, and the RSA private key files.</p>
<label for="sigFile">signature.bin file</label>
<input id="sigFile" type="file" accept=".bin,application/octet-stream">
<label for="sigFile">signature.bin 文件</label>
<input id="sigFile" type="file" accept=".bin,application/octet-stream">
<label for="serialFile">serial.txt file</label>
<input id="serialFile" type="file" accept=".txt,text/plain">
<label for="serialFile">serial.txt 文件</label>
<input id="serialFile" type="file" accept=".txt,text/plain">
<label for="pemFile">key.pem file</label>
<input id="pemFile" type="file" accept=".pem,text/plain">
<label for="pemFile">key.pem 文件</label>
<input id="pemFile" type="file" accept=".pem,text/plain">
<div class="actions">
<button id="packButton" class="primary" type="button">Pack</button>
<div id="status" class="status" aria-live="polite"></div>
<div id="meta" class="meta"></div>
</div>
<div id="status" class="status" aria-live="polite"></div>
<div id="meta" class="meta"></div>
<div class="section">
<h2>Output</h2>
<textarea id="output" class="output" readonly wrap="soft" placeholder="Packed text will appear here."></textarea>
<div id="outputStatus" class="status output-status" aria-live="polite"></div>
</div>
</div>
<div class="section">
<h2>Output</h2>
<p>Single-line plain text, suitable for direct paste into the CLI as <strong>ps4key &lt;serialized_key&gt;</strong>.</p>
<textarea id="output" class="output" readonly wrap="soft" placeholder="Packed text will appear here."></textarea>
<div class="output-actions">
<button id="copyButton" class="secondary" type="button">Copy Output</button>
<div class="button-row">
<div class="button-section left">
<button id="packButton" class="primay" type="button">Pack the Files</button>
</div>
<div class="button-section right">
<button id="copyButton" class="primay" type="button">Copy to Clipboard</button>
</div>
<div id="outputStatus" class="status output-status" aria-live="polite"></div>
</div>
</section>
</main>
@@ -316,20 +318,20 @@
function assertInputs(serial, pem, sigBytes) {
if (!sigBytes || sigBytes.length === 0) {
throw new Error("请上传 signature.bin 二进制文件。");
throw new Error("Please upload the signature.bin file.");
}
if (sigBytes.length > SIG_MAX_LENGTH) {
throw new Error(`signature.bin 二进制文件过大,最大 ${SIG_MAX_LENGTH} 字节。`);
throw new Error(`signature.bin is too large, max ${SIG_MAX_LENGTH} bytes.`);
}
if (!/^\d{16}$/.test(serial)) {
throw new Error("serial.txt 必须是 16 位十进制字符串。");
throw new Error("serial.txt must be a 16-digit decimal string.");
}
if (!pem.includes("-----BEGIN RSA PRIVATE KEY-----") ||
!pem.includes("-----END RSA PRIVATE KEY-----")) {
throw new Error("key.pem 必须包含 BEGIN/END RSA PRIVATE KEY 标记。");
throw new Error("key.pem must contain BEGIN/END RSA PRIVATE KEY markers.");
}
if (pem.length === 0 || pem.length > PEM_MAX_LENGTH) {
throw new Error(`key.pem 长度必须在 1-${PEM_MAX_LENGTH} 字符之间。`);
throw new Error(`key.pem length must be 1-${PEM_MAX_LENGTH} characters.`);
}
}
@@ -357,20 +359,25 @@
function packPayload(serial, pem, sigBytes) {
const serialBytes = encoder.encode(serial);
const pemBytes = encoder.encode(pem);
const payloadLength = serialBytes.length + pemBytes.length + sigBytes.length;
const serialLen = serialBytes.length + 1;
const pemLen = pemBytes.length + 1;
const payloadLength = serialLen + pemLen + sigBytes.length;
const payload = new Uint8Array(payloadLength);
payload.set(serialBytes, 0);
payload.set(pemBytes, serialBytes.length);
payload.set(sigBytes, serialBytes.length + pemBytes.length);
payload[serialBytes.length] = 0;
payload.set(pemBytes, serialLen);
payload[serialLen + pemBytes.length] = 0;
payload.set(sigBytes, serialLen + pemLen);
const header = new Uint8Array(15);
const header = new Uint8Array(16);
header.set([0x50, 0x53, 0x34, 0x4b], 0);
header[4] = 1;
header[5] = 0;
const view = new DataView(header.buffer);
view.setUint16(5, serialBytes.length, true);
view.setUint16(7, pemBytes.length, true);
view.setUint16(9, sigBytes.length, true);
view.setUint32(11, crc32(payload), true);
view.setUint16(6, serialLen, true);
view.setUint16(8, pemLen, true);
view.setUint16(10, sigBytes.length, true);
view.setUint32(12, crc32(payload), true);
const packed = new Uint8Array(header.length + payload.length);
packed.set(header, 0);
@@ -393,17 +400,18 @@
async function doPack() {
try {
// 前置文件选择校验
// Pre-file selection validation
if (!sigFileInput.files || !sigFileInput.files[0]) {
setStatus("error", "请先选择 sig.bin 文件。");
setStatus("error", "Please select sig.bin file.");
return;
}
if (!serialFileInput.files || !serialFileInput.files[0]) {
setStatus("error", "请先选择 serial.txt 文件。");
setStatus("error", "Please select serial.txt file.");
return;
}
if (!pemFileInput.files || !pemFileInput.files[0]) {
setStatus("error", "请先选择 key.pem 文件。");
setStatus("error", "Please select key.pem file.");
return;
}
@@ -411,16 +419,17 @@
const serialText = await readFileAsText(serialFileInput);
const pemText = await readFileAsText(pemFileInput);
if (!sigBytes || sigBytes.length === 0) {
setStatus("error", "sig.bin 文件读取失败或为空。");
setStatus("error", "sig.bin file read failed or empty.");
return;
}
if (!serialText || serialText.trim().length === 0) {
setStatus("error", "serial.txt 文件读取失败或为空。");
setStatus("error", "serial.txt file read failed or empty.");
return;
}
if (!pemText || pemText.trim().length === 0) {
setStatus("error", "key.pem 文件读取失败或为空。");
setStatus("error", "key.pem file read failed or empty.");
return;
}
@@ -431,13 +440,7 @@
const packed = packPayload(serial, pem, sigBytes);
output.value = packed;
setMeta([
`serial: ${serial.length} bytes`,
`pem: ${pem.length} bytes`,
`sig: ${sigBytes.length} bytes`,
`output: ${packed.length} chars`
]);
setStatus("ok", "打包成功。");
setStatus("ok", `Packed successfully, total ${packed.length} chars.`);
} catch (error) {
output.value = "";
setMeta([]);
@@ -447,7 +450,6 @@
async function copyOutput() {
if (!output.value) {
setOutputStatus("warn", "Nothing to copy.");
return;
}
@@ -458,7 +460,6 @@
} catch (error) {
output.focus();
output.select();
setOutputStatus("warn", "Clipboard API is unavailable. The output has been selected.");
}
}