@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.17",
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.5.4"
42
+ "zod": "4.6.4"
43
43
  }
44
44
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./ini";
2
+ export * from "./javaProperties";
2
3
  export * from "./json";
3
4
  export * from "./properties";
4
5
  export * from "./sourceCfg";
@@ -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
+ };
@@ -66,6 +66,7 @@ export interface BridgeCodec {
66
66
 
67
67
  export interface BridgeCodecs {
68
68
  ini: BridgeCodec;
69
+ javaProperties: BridgeCodec;
69
70
  json: BridgeCodec;
70
71
  properties: BridgeCodec;
71
72
  sourceCfg: BridgeCodec;
@@ -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<void>;
134
+ add?(context: BridgeContext, input: string): Promise<BridgeAddOutcome | undefined>;
135
135
  actions?: Record<string, BridgeRowAction<Row>>;
136
136
  }
137
137
 
@@ -0,0 +1,5 @@
1
+ import type { LocalizedText } from "../core";
2
+
3
+ export interface BridgeAddOutcome {
4
+ notice: LocalizedText;
5
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./bridgeAccess";
2
+ export * from "./bridgeCollection";
2
3
  export * from "./bridgeConfirm";
3
4
  export * from "./bridgeControl";
4
5
  export * from "./bridgeDetail";
@@ -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];