@serverkgg/bridge 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/codecs/index.ts +1 -0
- package/src/codecs/javaProperties.test.ts +67 -0
- package/src/codecs/javaProperties.ts +99 -0
- package/src/context/bridgeContext.ts +1 -0
- package/src/modules/bridge.ts +2 -0
- package/src/modules/bridgeModule.ts +2 -2
- package/src/panel/bridgeCollection.ts +5 -0
- package/src/panel/index.ts +1 -0
- package/src/runtime/bridgeCodec.ts +12 -0
- package/src/runtime/bridgeRegistry.ts +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverkgg/bridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
|
|
5
5
|
"homepage": "https://serverk.gg",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
"test": "bun test --parallel"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"zod": "4.
|
|
42
|
+
"zod": "4.6.4"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/src/codecs/index.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { escapeJavaProperty, mergeJavaProperties, parseJavaProperties, unescapeJavaProperty } from "./javaProperties";
|
|
3
|
+
|
|
4
|
+
const ARABIC = "سيرفرك";
|
|
5
|
+
|
|
6
|
+
describe("parseJavaProperties", () => {
|
|
7
|
+
test("decodes the unicode escapes the java server writes", () => {
|
|
8
|
+
expect(parseJavaProperties("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\n")).toEqual({
|
|
9
|
+
motd: ARABIC,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("decodes backslash escapes in values", () => {
|
|
14
|
+
expect(parseJavaProperties("resource-pack=https\\://example.com/pack.zip\nmotd=a\\tb\\\\c\n")).toEqual({
|
|
15
|
+
"resource-pack": "https://example.com/pack.zip",
|
|
16
|
+
motd: "a\tb\\c",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("repairs a value the server stored as latin-1 bytes of utf-8", () => {
|
|
21
|
+
expect(parseJavaProperties("motd=ddss \\u00D9\\u008A\\u00D8\\u00AA\n")).toEqual({
|
|
22
|
+
motd: "ddss يت",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("keeps plain latin-1 text that is not utf-8", () => {
|
|
27
|
+
expect(unescapeJavaProperty("caf\\u00E9")).toBe("café");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("keeps comments and plain values", () => {
|
|
31
|
+
expect(parseJavaProperties("#Minecraft server properties\nmax-players=20\n")).toEqual({
|
|
32
|
+
"max-players": "20",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("mergeJavaProperties", () => {
|
|
38
|
+
test("writes non ascii text as unicode escapes", () => {
|
|
39
|
+
expect(
|
|
40
|
+
mergeJavaProperties("motd=A Minecraft Server\nmax-players=20\n", {
|
|
41
|
+
motd: ARABIC,
|
|
42
|
+
}),
|
|
43
|
+
).toBe("motd=\\u0633\\u064A\\u0631\\u0641\\u0631\\u0643\nmax-players=20\n");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("escapes the characters java treats as separators", () => {
|
|
47
|
+
expect(escapeJavaProperty("https://a.b#c=d!e")).toBe("https\\://a.b\\#c\\=d\\!e");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("escapes a leading space and control characters", () => {
|
|
51
|
+
expect(escapeJavaProperty(" a\tb\\c")).toBe("\\ a\\tb\\\\c");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("round trips astral characters", () => {
|
|
55
|
+
const value = "hi 🎮";
|
|
56
|
+
|
|
57
|
+
expect(unescapeJavaProperty(escapeJavaProperty(value))).toBe(value);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("appends a key the file does not have yet", () => {
|
|
61
|
+
expect(
|
|
62
|
+
mergeJavaProperties("", {
|
|
63
|
+
motd: "hello",
|
|
64
|
+
}),
|
|
65
|
+
).toBe("motd=hello\n");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mergeProperties, parseProperties } from "./properties";
|
|
2
|
+
|
|
3
|
+
const UNICODE_ESCAPE = /\\u([0-9A-Fa-f]{4})/g;
|
|
4
|
+
|
|
5
|
+
const SIMPLE_ESCAPE = /\\(.)/g;
|
|
6
|
+
|
|
7
|
+
const SIMPLE_ESCAPES: Record<string, string> = {
|
|
8
|
+
t: "\t",
|
|
9
|
+
n: "\n",
|
|
10
|
+
r: "\r",
|
|
11
|
+
f: "\f",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ESCAPED_CHARACTERS: Record<string, string> = {
|
|
15
|
+
"\\": "\\\\",
|
|
16
|
+
"\t": "\\t",
|
|
17
|
+
"\n": "\\n",
|
|
18
|
+
"\r": "\\r",
|
|
19
|
+
"\f": "\\f",
|
|
20
|
+
"=": "\\=",
|
|
21
|
+
":": "\\:",
|
|
22
|
+
"#": "\\#",
|
|
23
|
+
"!": "\\!",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const PRINTABLE_MIN = 0x20;
|
|
27
|
+
|
|
28
|
+
const PRINTABLE_MAX = 0x7e;
|
|
29
|
+
|
|
30
|
+
const LATIN1_MAX = 0xff;
|
|
31
|
+
|
|
32
|
+
const HEX_DIGITS = 4;
|
|
33
|
+
|
|
34
|
+
const latin1Decoder = new TextDecoder("utf-8", {
|
|
35
|
+
fatal: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const repairLatin1 = (text: string) => {
|
|
39
|
+
const codes = Array.from(text, (character) => character.charCodeAt(0));
|
|
40
|
+
|
|
41
|
+
if (codes.every((code) => code <= PRINTABLE_MAX) || codes.some((code) => code > LATIN1_MAX)) {
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return latin1Decoder.decode(Uint8Array.from(codes));
|
|
47
|
+
} catch {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const unescapeJavaProperty = (text: string) => {
|
|
53
|
+
const unescaped = text
|
|
54
|
+
.replace(UNICODE_ESCAPE, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
55
|
+
.replace(SIMPLE_ESCAPE, (_, character: string) => SIMPLE_ESCAPES[character] ?? character);
|
|
56
|
+
|
|
57
|
+
return repairLatin1(unescaped);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const escapeJavaProperty = (text: string) => {
|
|
61
|
+
let escaped = "";
|
|
62
|
+
|
|
63
|
+
for (const character of text) {
|
|
64
|
+
const code = character.codePointAt(0) ?? 0;
|
|
65
|
+
const known = ESCAPED_CHARACTERS[character];
|
|
66
|
+
|
|
67
|
+
if (known !== undefined) {
|
|
68
|
+
escaped += known;
|
|
69
|
+
} else if (code < PRINTABLE_MIN || code > PRINTABLE_MAX) {
|
|
70
|
+
for (let index = 0; index < character.length; index += 1) {
|
|
71
|
+
escaped += `\\u${character.charCodeAt(index).toString(16).toUpperCase().padStart(HEX_DIGITS, "0")}`;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
escaped += character;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return escaped.startsWith(" ") ? `\\${escaped}` : escaped;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const parseJavaProperties = (contents: string) => {
|
|
82
|
+
const values: Record<string, string> = {};
|
|
83
|
+
|
|
84
|
+
for (const [key, value] of Object.entries(parseProperties(contents))) {
|
|
85
|
+
values[unescapeJavaProperty(key)] = unescapeJavaProperty(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return values;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const mergeJavaProperties = (contents: string, values: Record<string, string>) => {
|
|
92
|
+
const escaped: Record<string, string> = {};
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(values)) {
|
|
95
|
+
escaped[escapeJavaProperty(key)] = escapeJavaProperty(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return mergeProperties(contents, escaped);
|
|
99
|
+
};
|
package/src/modules/bridge.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from "../core";
|
|
12
12
|
import { BridgeUserError } from "../core";
|
|
13
13
|
import type {
|
|
14
|
+
BridgeAddOutcome,
|
|
14
15
|
BridgeDetail,
|
|
15
16
|
BridgeDetailBadge,
|
|
16
17
|
BridgeDetailLink,
|
|
@@ -72,6 +73,7 @@ import type {
|
|
|
72
73
|
|
|
73
74
|
export namespace Bridge {
|
|
74
75
|
export type Actions = BridgeActionsModule;
|
|
76
|
+
export type AddOutcome = BridgeAddOutcome;
|
|
75
77
|
export type Announce = BridgeAnnounceModule;
|
|
76
78
|
export type Backup = BridgeBackupModule;
|
|
77
79
|
export type Catalog = BridgeCatalogModule;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BridgeContext, BridgeReply } from "../context";
|
|
2
2
|
import type { BridgeInstallOutcome, BridgeKind, BridgeOption, BridgeRow, BridgeStream, BridgeValues } from "../core";
|
|
3
|
-
import type { BridgeDetail, BridgeDetailOutcome } from "../panel";
|
|
3
|
+
import type { BridgeAddOutcome, BridgeDetail, BridgeDetailOutcome } from "../panel";
|
|
4
4
|
import type {
|
|
5
5
|
BridgeCatalogEntry,
|
|
6
6
|
BridgeCatalogPage,
|
|
@@ -131,7 +131,7 @@ export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
|
|
|
131
131
|
requiresRunning?: boolean;
|
|
132
132
|
refreshSeconds?: number;
|
|
133
133
|
list(context: BridgeContext): Promise<Row[]>;
|
|
134
|
-
add?(context: BridgeContext, input: string): Promise<
|
|
134
|
+
add?(context: BridgeContext, input: string): Promise<BridgeAddOutcome | undefined>;
|
|
135
135
|
actions?: Record<string, BridgeRowAction<Row>>;
|
|
136
136
|
}
|
|
137
137
|
|
package/src/panel/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mergeIni,
|
|
3
|
+
mergeJavaProperties,
|
|
3
4
|
mergeJson,
|
|
4
5
|
mergeProperties,
|
|
5
6
|
mergeSourceCfg,
|
|
6
7
|
mergeUeIni,
|
|
7
8
|
mergeXmlProperties,
|
|
8
9
|
parseIni,
|
|
10
|
+
parseJavaProperties,
|
|
9
11
|
parseJson,
|
|
10
12
|
parseProperties,
|
|
11
13
|
parseSourceCfg,
|
|
@@ -61,6 +63,15 @@ export const createBridgeCodecs = (files: BridgeFiles): BridgeCodecs => {
|
|
|
61
63
|
},
|
|
62
64
|
};
|
|
63
65
|
|
|
66
|
+
const javaProperties: BridgeCodec = {
|
|
67
|
+
async read(path) {
|
|
68
|
+
return widen(parseJavaProperties(await readOr(files, path)));
|
|
69
|
+
},
|
|
70
|
+
async merge(path, values) {
|
|
71
|
+
await files.write(path, mergeJavaProperties(await readOr(files, path), toStrings(values)));
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
64
75
|
const json: BridgeCodec = {
|
|
65
76
|
async read(path) {
|
|
66
77
|
return parseJson(await readOr(files, path));
|
|
@@ -112,6 +123,7 @@ export const createBridgeCodecs = (files: BridgeFiles): BridgeCodecs => {
|
|
|
112
123
|
|
|
113
124
|
return {
|
|
114
125
|
ini,
|
|
126
|
+
javaProperties,
|
|
115
127
|
json,
|
|
116
128
|
properties,
|
|
117
129
|
sourceCfg,
|
|
@@ -324,9 +324,7 @@ export const createRegistry = (
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
if (action === BridgeAction.Add && module.add) {
|
|
327
|
-
await module.add(context, asString(args, "input"));
|
|
328
|
-
|
|
329
|
-
return null;
|
|
327
|
+
return (await module.add(context, asString(args, "input"))) ?? null;
|
|
330
328
|
}
|
|
331
329
|
|
|
332
330
|
const rowAction = module.actions?.[action];
|