Skip to content

Commit 8119855

Browse files
committed
Require explicit GUIDs for profiles and presets
1 parent 7c3afaf commit 8119855

8 files changed

Lines changed: 90 additions & 19 deletions

File tree

HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package org.jackhuang.hmcl.game;
1919

20+
import com.github.f4b6a3.uuid.alt.GUID;
2021
import com.google.gson.JsonParseException;
2122
import kala.compress.archivers.zip.ZipArchiveReader;
2223
import org.jackhuang.hmcl.Metadata;
@@ -191,7 +192,7 @@ public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path
191192

192193
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
193194
.thenAcceptAsync(Schedulers.javafx(), location -> {
194-
Profile newProfile = new Profile(name, PortablePath.fromPath(location));
195+
Profile newProfile = new Profile(GUID.v4(), name, PortablePath.fromPath(location));
195196
Profiles.getProfiles().add(newProfile);
196197
Profiles.setSelectedProfile(newProfile);
197198
});

HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public static GameSettings.Preset getDefaultGameSettingsPresetOrCreate() {
216216
return setting;
217217
}
218218

219-
setting = new GameSettings.Preset();
219+
setting = new GameSettings.Preset(GUID.v4());
220220
setting.nameProperty().setValue(i18n("message.default"));
221221
getGameSettings().add(setting);
222222
setDefaultGameSettingsPreset(setting.idProperty().getValue());

HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
package org.jackhuang.hmcl.setting;
1919

2020
import com.github.f4b6a3.uuid.alt.GUID;
21+
import com.google.gson.JsonDeserializationContext;
22+
import com.google.gson.JsonElement;
23+
import com.google.gson.JsonParseException;
2124
import com.google.gson.annotations.JsonAdapter;
2225
import com.google.gson.annotations.SerializedName;
2326
import javafx.collections.FXCollections;
@@ -40,6 +43,7 @@
4043
import org.jetbrains.annotations.UnknownNullability;
4144

4245
import java.io.IOException;
46+
import java.lang.reflect.Type;
4347
import java.nio.file.InvalidPathException;
4448
import java.nio.file.Path;
4549
import java.util.Collection;
@@ -114,26 +118,35 @@ protected Instance createInstance() {
114118
@JsonAdapter(Preset.Adapter.class)
115119
@JsonSerializable
116120
public static final class Preset extends GameSettings {
117-
/// Creates a preset with generated identity.
118-
public Preset() {
119-
this(GUID.v4());
121+
/// Creates a preset with the given identity.
122+
public Preset(GUID id) {
123+
this(id, true);
120124
}
121125

122126
/// Creates a preset with the given identity.
123-
public Preset(GUID id) {
127+
private Preset(GUID id, boolean checkNonNil) {
124128
register();
125-
this.id.setValue(id);
129+
this.id.setValue(checkNonNil ? requireNonNilId(id) : Objects.requireNonNull(id));
126130
}
127131

128132
/// The stable preset ID.
129133
@SerializedName("id")
130-
private final SettingProperty<GUID> id = newSettingProperty("id");
134+
private final SettingProperty<GUID> id = newSettingProperty("id", GUID.NIL);
131135

132136
/// Returns the preset ID property.
133137
public SettingProperty<GUID> idProperty() {
134138
return id;
135139
}
136140

141+
/// Returns a non-nil preset ID or throws when the value is not usable.
142+
private static GUID requireNonNilId(GUID id) {
143+
Objects.requireNonNull(id);
144+
if (GUID.NIL.equals(id)) {
145+
throw new IllegalArgumentException("Preset ID cannot be nil");
146+
}
147+
return id;
148+
}
149+
137150
/// The display name of this preset.
138151
@SerializedName("name")
139152
private final SettingProperty<String> name = newSettingProperty("name", "");
@@ -153,10 +166,22 @@ public SettingProperty<DefaultIsolationType> defaultIsolationTypeProperty() {
153166
}
154167

155168
/// JSON adapter for presets.
156-
public static final class Adapter extends ObservableSetting.Adapter<Preset> {
169+
public static final class Adapter extends ObservableSetting.Adapter<@Nullable Preset> {
157170
@Override
158171
protected Preset createInstance() {
159-
return new Preset();
172+
return new Preset(GUID.NIL, false);
173+
}
174+
175+
@Override
176+
public @Nullable Preset deserialize(
177+
JsonElement json,
178+
Type typeOfT,
179+
JsonDeserializationContext context) throws JsonParseException {
180+
@Nullable Preset result = super.deserialize(json, typeOfT, context);
181+
if (result != null && GUID.NIL.equals(result.idProperty().getValue())) {
182+
throw new JsonParseException("Preset ID cannot be nil");
183+
}
184+
return result;
160185
}
161186
}
162187
}

HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public final class Profile implements Observable {
6161
private final HMCLGameRepository repository;
6262

6363
/// The stable profile ID.
64-
private final ObjectProperty<GUID> id = new SimpleObjectProperty<>(this, "id", GUID.v4());
64+
private final ObjectProperty<GUID> id = new SimpleObjectProperty<>(this, "id", GUID.NIL);
6565

6666
/// Returns the stable profile ID property.
6767
public ObjectProperty<GUID> idProperty() {
@@ -75,7 +75,7 @@ public GUID getId() {
7575

7676
/// Sets the stable profile ID.
7777
public void setId(GUID id) {
78-
this.id.set(Objects.requireNonNull(id));
78+
this.id.set(requireNonNilId(id));
7979
}
8080

8181
private final StringProperty selectedVersion = new SimpleStringProperty();
@@ -124,18 +124,19 @@ public void setName(String name) {
124124
this.name.set(name);
125125
}
126126

127-
public Profile(String name, Path initialGameDir) {
128-
this(name, PortablePath.fromPath(initialGameDir));
127+
/// Creates a profile.
128+
public Profile(GUID id, String name, Path initialGameDir) {
129+
this(id, name, PortablePath.fromPath(initialGameDir));
129130
}
130131

131132
/// Creates a profile.
132-
public Profile(String name, PortablePath path) {
133-
this(GUID.v4(), name, path, null);
133+
public Profile(GUID id, String name, PortablePath path) {
134+
this(id, name, path, null);
134135
}
135136

136137
/// Creates a profile with an explicit stable ID.
137138
Profile(GUID id, String name, PortablePath path, @Nullable String selectedVersion) {
138-
this.id.set(Objects.requireNonNull(id));
139+
this.id.set(requireNonNilId(id));
139140
this.name = new SimpleStringProperty(this, "name", name);
140141
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
141142
repository = new HMCLGameRepository(this, path.toPath());
@@ -148,6 +149,15 @@ public Profile(String name, PortablePath path) {
148149
addPropertyChangedListener(onInvalidating(this::invalidate));
149150
}
150151

152+
/// Returns a non-nil profile ID or throws when the value is not usable.
153+
private static GUID requireNonNilId(GUID id) {
154+
Objects.requireNonNull(id);
155+
if (GUID.NIL.equals(id)) {
156+
throw new IllegalArgumentException("Profile ID cannot be nil");
157+
}
158+
return id;
159+
}
160+
151161
private void checkSelectedVersion() {
152162
runInFX(() -> {
153163
if (!repository.isLoaded()) return;
@@ -229,6 +239,8 @@ public JsonElement serialize(@Nullable Profile src, Type typeOfSrc, JsonSerializ
229239
GUID id = context.deserialize(obj.get("id"), GUID.class);
230240
if (id == null) {
231241
throw new JsonParseException("Profile ID cannot be null");
242+
} else if (GUID.NIL.equals(id)) {
243+
throw new JsonParseException("Profile ID cannot be nil");
232244
}
233245
PortablePath path = context.deserialize(obj.get("path"), PortablePath.class);
234246
if (path == null) {

HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ private void createPreset() {
835835
return;
836836
}
837837

838-
GameSettings.Preset setting = new GameSettings.Preset();
838+
GameSettings.Preset setting = new GameSettings.Preset(GUID.v4());
839839
setting.nameProperty().setValue(name.trim());
840840
ConfigHolder.getGameSettings().add(setting);
841841
selectPreset(setting);

HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package org.jackhuang.hmcl.ui.profile;
1919

20+
import com.github.f4b6a3.uuid.alt.GUID;
2021
import com.jfoenix.controls.JFXButton;
2122
import com.jfoenix.controls.JFXTextField;
2223
import com.jfoenix.validation.RequiredFieldValidator;
@@ -188,7 +189,7 @@ private void onSave() {
188189
if (StringUtils.isBlank(getLocation())) {
189190
gameDir.fire();
190191
}
191-
Profile newProfile = new Profile(txtProfileName.getText(), PortablePath.of(getLocation()));
192+
Profile newProfile = new Profile(GUID.v4(), txtProfileName.getText(), PortablePath.of(getLocation()));
192193
Profiles.getProfiles().add(newProfile);
193194
}
194195

HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.jackhuang.hmcl.setting;
1919

2020
import com.github.f4b6a3.uuid.alt.GUID;
21+
import com.google.gson.JsonParseException;
2122
import com.google.gson.JsonObject;
2223
import com.google.gson.JsonParser;
2324
import org.jackhuang.hmcl.util.PortablePath;
@@ -85,6 +86,21 @@ public void storesProfilePath() {
8586
assertFalse(deserialized.getPath().isAbsolute());
8687
}
8788

89+
/// Tests that profiles must be initialized with a non-nil ID.
90+
@Test
91+
public void rejectsNilProfileId() {
92+
assertThrows(IllegalArgumentException.class,
93+
() -> new Profile(GUID.NIL, "Dev", PortablePath.of("versions/Dev")));
94+
95+
assertThrows(JsonParseException.class, () -> JsonUtils.GSON.fromJson("""
96+
{
97+
"id": "00000000-0000-0000-0000-000000000000",
98+
"name": "Dev",
99+
"path": "versions/Dev"
100+
}
101+
""", Profile.class));
102+
}
103+
88104
/// Tests that game directory files do not preserve the workspace-level selected directory.
89105
@Test
90106
public void doesNotStoreSelectedGameDirectoryInGameDirectories() {

HMCL/src/test/java/org/jackhuang/hmcl/setting/GameSettingsPresetsTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.jackhuang.hmcl.setting;
1919

2020
import com.github.f4b6a3.uuid.alt.GUID;
21+
import com.google.gson.JsonParseException;
2122
import com.google.gson.JsonObject;
2223
import com.google.gson.JsonParser;
2324
import org.jackhuang.hmcl.util.gson.JsonFileFormat;
@@ -42,6 +43,21 @@ public void storesDefaultGameSettingsPresetInConfig() {
4243
assertEquals(id.toString(), serialized.get(Config.DEFAULT_GAME_SETTINGS_PRESET_MEMBER_NAME).getAsString());
4344
}
4445

46+
/// Tests that presets must be initialized with a non-nil ID.
47+
@Test
48+
public void rejectsNilPresetId() {
49+
assertThrows(IllegalArgumentException.class, () -> new GameSettings.Preset(GUID.NIL));
50+
51+
assertThrows(JsonParseException.class, () -> JsonUtils.GSON.fromJson("""
52+
{
53+
"id": "00000000-0000-0000-0000-000000000000"
54+
}
55+
""", GameSettings.Preset.class));
56+
57+
assertThrows(JsonParseException.class,
58+
() -> JsonUtils.GSON.fromJson("{}", GameSettings.Preset.class));
59+
}
60+
4561
/// Tests that preset files do not preserve the workspace-level default preset selection.
4662
@Test
4763
public void doesNotStoreDefaultGameSettingsPresetInPresets() {

0 commit comments

Comments
 (0)