From 9dccca9d9a6026dd909ee97d0373d9b23bce3ee5 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 4 Jun 2024 15:03:30 +0100 Subject: [PATCH] Add API to get mod cache and data directories --- .../net/fabricmc/loader/api/FabricLoader.java | 8 ++ .../fabricmc/loader/api/ModDirectories.java | 66 +++++++++ .../loader/impl/FabricLoaderImpl.java | 18 ++- .../loader/impl/game/GameProviderHelper.java | 6 +- .../impl/metadata/MetadataVerifier.java | 6 + .../loader/impl/util/GlobalDirectories.java | 133 ++++++++++++++++++ .../loader/impl/util/ModDirectoriesImpl.java | 68 +++++++++ .../fabricmc/test/ModDirectoriesTests.java | 58 ++++++++ 8 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/fabricmc/loader/api/ModDirectories.java create mode 100644 src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java create mode 100644 src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java create mode 100644 src/test/java/net/fabricmc/test/ModDirectoriesTests.java diff --git a/src/main/java/net/fabricmc/loader/api/FabricLoader.java b/src/main/java/net/fabricmc/loader/api/FabricLoader.java index e83b54033..106226dd0 100644 --- a/src/main/java/net/fabricmc/loader/api/FabricLoader.java +++ b/src/main/java/net/fabricmc/loader/api/FabricLoader.java @@ -236,4 +236,12 @@ static FabricLoader getInstance() { * @return the launch arguments for the game */ String[] getLaunchArguments(boolean sanitize); + + /** + * Get a {@link ModDirectories} instance, proving access to various cache or data directories. + * + * @param modId the ID of the mod. + * @return A {@link ModDirectories} instance. + */ + ModDirectories getDirectories(String modId); } diff --git a/src/main/java/net/fabricmc/loader/api/ModDirectories.java b/src/main/java/net/fabricmc/loader/api/ModDirectories.java new file mode 100644 index 000000000..881caf2fc --- /dev/null +++ b/src/main/java/net/fabricmc/loader/api/ModDirectories.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.api; + +import java.nio.file.Path; + +/** + * Provides access to cache or data directories for a mod. + * + *

These directories are unique to the mod id and should not be shared between mods. These directories are not versioned so + * special care must be taken to ensure backwards compatibility with older versions of the mod running on the same machine. + * + *

Global directories are shared between all instances of Fabric Loader for the current user of the machine, no matter the game or launcher. + * + *

The cache directories should be used for temporary files that can be regenerated if lost. + * + *

Any sensitive data should be encrypted, and the private key stored in the {@link #getGlobalDataDir()}. + * + *

A sandbox implementation will grant full access to all of these directories, and may not isolate them from other instances. + * Code should not be stored and executed from these directories to prevent a sandbox escape. + */ +public interface ModDirectories { + /** + * Get the local cache directory for the mod. + * + *

Note: This directory should not be distributed, for example in a mod pack. + * + * @return A {@link Path} to the cache directory. + */ + Path getCacheDir(); + + /** + * Get the global cache directory for the mod. + * + * @return A {@link Path} to the global cache directory. + */ + Path getGlobalCacheDir(); + + /** + * Get the local data directory for the mod. + * + * @return A {@link Path} to the data directory. + */ + Path getDataDir(); + + /** + * Get the global data directory for the mod. + * + * @return A {@link Path} to the global data directory. + */ + Path getGlobalDataDir(); +} diff --git a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java index 0977d60c2..bd5e447fe 100644 --- a/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java +++ b/src/main/java/net/fabricmc/loader/impl/FabricLoaderImpl.java @@ -43,6 +43,7 @@ import net.fabricmc.loader.api.LanguageAdapter; import net.fabricmc.loader.api.MappingResolver; import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.ModDirectories; import net.fabricmc.loader.api.ObjectShare; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import net.fabricmc.loader.impl.discovery.ArgumentModCandidateFinder; @@ -60,10 +61,13 @@ import net.fabricmc.loader.impl.metadata.DependencyOverrides; import net.fabricmc.loader.impl.metadata.EntrypointMetadata; import net.fabricmc.loader.impl.metadata.LoaderModMetadata; +import net.fabricmc.loader.impl.metadata.MetadataVerifier; import net.fabricmc.loader.impl.metadata.VersionOverrides; import net.fabricmc.loader.impl.util.DefaultLanguageAdapter; import net.fabricmc.loader.impl.util.ExceptionUtil; +import net.fabricmc.loader.impl.util.GlobalDirectories; import net.fabricmc.loader.impl.util.LoaderUtil; +import net.fabricmc.loader.impl.util.ModDirectoriesImpl; import net.fabricmc.loader.impl.util.SystemProperties; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; @@ -77,7 +81,6 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader { public static final String VERSION = "0.15.11"; public static final String MOD_ID = "fabricloader"; - public static final String CACHE_DIR_NAME = ".fabric"; // relative to game dir private static final String PROCESSED_MODS_DIR_NAME = "processedMods"; // relative to cache dir public static final String REMAPPED_JARS_DIR_NAME = "remappedJars"; // relative to cache dir private static final String TMP_DIR_NAME = "tmp"; // relative to cache dir @@ -100,6 +103,7 @@ public final class FabricLoaderImpl extends net.fabricmc.loader.FabricLoader { private GameProvider provider; private Path gameDir; private Path configDir; + private GlobalDirectories globalDirectories; private FabricLoaderImpl() { } @@ -134,6 +138,7 @@ public void setGameProvider(GameProvider provider) { private void setGameDir(Path gameDir) { this.gameDir = gameDir; this.configDir = gameDir.resolve("config"); + this.globalDirectories = GlobalDirectories.create(getEnvironmentType(), gameDir); } @Override @@ -230,7 +235,7 @@ private void setup() throws ModResolutionException { dumpModList(modCandidates); - Path cacheDir = gameDir.resolve(CACHE_DIR_NAME); + Path cacheDir = getDirectories(MOD_ID).getCacheDir(); Path outputdir = cacheDir.resolve(PROCESSED_MODS_DIR_NAME); // runtime mod remapping @@ -595,6 +600,15 @@ public String[] getLaunchArguments(boolean sanitize) { return getGameProvider().getLaunchArguments(sanitize); } + @Override + public ModDirectories getDirectories(String modId) { + if (!MetadataVerifier.isValidModId(modId)) { + throw new IllegalArgumentException("Invalid mod ID: " + modId); + } + + return new ModDirectoriesImpl(modId, getGameDir(), globalDirectories); + } + @Override protected Path getModsDirectory0() { String directory = System.getProperty(SystemProperties.MODS_FOLDER); diff --git a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java index 0e724ef1c..46a8bcd45 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java +++ b/src/main/java/net/fabricmc/loader/impl/game/GameProviderHelper.java @@ -189,7 +189,7 @@ public static Map deobfuscate(Map inputFileMap, Stri return inputFileMap; } - Path deobfJarDir = getDeobfJarDir(gameDir, gameId, gameVersion); + Path deobfJarDir = getDeobfJarDir(gameId, gameVersion); List inputFiles = new ArrayList<>(inputFileMap.size()); List outputFiles = new ArrayList<>(inputFileMap.size()); List tmpFiles = new ArrayList<>(inputFileMap.size()); @@ -247,8 +247,8 @@ public static Map deobfuscate(Map inputFileMap, Stri return ret; } - private static Path getDeobfJarDir(Path gameDir, String gameId, String gameVersion) { - Path ret = gameDir.resolve(FabricLoaderImpl.CACHE_DIR_NAME).resolve(FabricLoaderImpl.REMAPPED_JARS_DIR_NAME); + private static Path getDeobfJarDir(String gameId, String gameVersion) { + Path ret = FabricLoaderImpl.INSTANCE.getDirectories(FabricLoaderImpl.MOD_ID).getCacheDir().resolve(FabricLoaderImpl.REMAPPED_JARS_DIR_NAME); StringBuilder versionDirName = new StringBuilder(); if (!gameId.isEmpty()) { diff --git a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java index c0157c109..1799e61a2 100644 --- a/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java +++ b/src/main/java/net/fabricmc/loader/impl/metadata/MetadataVerifier.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -145,4 +146,9 @@ private static void checkModId(String id, String name) throws ParseMetadataExcep throw new ParseMetadataException(sw.toString()); } + + public static boolean isValidModId(String id) { + Objects.requireNonNull(id, "id"); + return MOD_ID_PATTERN.matcher(id).matches(); + } } diff --git a/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java b/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java new file mode 100644 index 000000000..b94c054a6 --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/util/GlobalDirectories.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.util; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; + +import net.fabricmc.api.EnvType; + +public abstract class GlobalDirectories { + abstract Path getGlobalCacheRoot(); + + abstract Path getGlobalDataRoot(); + + public static GlobalDirectories create(EnvType envType, Path gameDir) { + switch (envType) { + case CLIENT: + return Client.create(); + case SERVER: + return new Server(gameDir); + default: + throw new IllegalStateException(); + } + } + + abstract static class Client extends GlobalDirectories { + private final Path cache; + private final Path data; + + protected Client(Path cache, Path data) { + this.cache = cache; + this.data = data; + } + + @Override + public Path getGlobalCacheRoot() { + return cache; + } + + @Override + public Path getGlobalDataRoot() { + return data; + } + + static Client create() { + final String os = System.getProperty("os.name").toLowerCase(Locale.ROOT); + + if (os.contains("win")) { + return new Windows(); + } else if (os.contains("mac")) { + return new MacOS(); + } + + // Linux or unknown. + return new Linux(); + } + + static final class Windows extends Client { + private Windows() { + super(getCacheDir(), getDataDir()); + } + + private static Path getCacheDir() { + return Paths.get(System.getenv("LocalAppData"), "net.fabricmc.loader"); + } + + private static Path getDataDir() { + return Paths.get(System.getenv("AppData"), "net.fabricmc.loader"); + } + } + + static final class MacOS extends Client { + private MacOS() { + super(getCacheDir(), getDataDir()); + } + + private static Path getCacheDir() { + return Paths.get(System.getProperty("user.home"), "Library", "Caches", "net.fabricmc.loader"); + } + + private static Path getDataDir() { + return Paths.get(System.getProperty("user.home"), "Library", "Application Support", "net.fabricmc.loader"); + } + } + + static final class Linux extends Client { + private Linux() { + super(getCacheDir(), getDataDir()); + } + + private static Path getCacheDir() { + return Paths.get(System.getProperty("user.home"), ".caches", "net.fabricmc.loader"); + } + + private static Path getDataDir() { + return Paths.get(System.getProperty("user.home"), ".config", "net.fabricmc.loader"); + } + } + } + + static final class Server extends GlobalDirectories { + private final Path gameDir; + + private Server(Path gameDir) { + this.gameDir = gameDir; + } + + @Override + public Path getGlobalCacheRoot() { + return gameDir.resolve("fabric-global"); + } + + @Override + public Path getGlobalDataRoot() { + return gameDir.resolve("cache-global"); + } + } +} diff --git a/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java b/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java new file mode 100644 index 000000000..4ffcb4c6c --- /dev/null +++ b/src/main/java/net/fabricmc/loader/impl/util/ModDirectoriesImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loader.impl.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import net.fabricmc.loader.api.ModDirectories; + +public final class ModDirectoriesImpl implements ModDirectories { + private final String modId; + private final Path gameDir; + private final GlobalDirectories globalDirectories; + + public ModDirectoriesImpl(String modId, Path gameDir, GlobalDirectories globalDirectories) { + this.modId = Objects.requireNonNull(modId); + this.gameDir = gameDir; + this.globalDirectories = Objects.requireNonNull(globalDirectories); + } + + @Override + public Path getCacheDir() { + return ensureDir(gameDir.resolve("cache").resolve(modId)); + } + + @Override + public Path getGlobalCacheDir() { + return ensureDir(globalDirectories.getGlobalCacheRoot().resolve(modId)); + } + + @Override + public Path getDataDir() { + return ensureDir(gameDir.resolve("fabric").resolve(modId)); + } + + @Override + public Path getGlobalDataDir() { + return ensureDir(globalDirectories.getGlobalDataRoot().resolve(modId)); + } + + private Path ensureDir(Path path) { + if (!Files.exists(path)) { + try { + Files.createDirectories(path); + } catch (IOException e) { + throw new RuntimeException("Failed to create directory: " + path, e); + } + } + + return path; + } +} diff --git a/src/test/java/net/fabricmc/test/ModDirectoriesTests.java b/src/test/java/net/fabricmc/test/ModDirectoriesTests.java new file mode 100644 index 000000000..2fe87bcc9 --- /dev/null +++ b/src/test/java/net/fabricmc/test/ModDirectoriesTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.ModDirectories; +import net.fabricmc.loader.impl.util.GlobalDirectories; +import net.fabricmc.loader.impl.util.ModDirectoriesImpl; + +// Just a basic test to ensure that we can write to these directories +public class ModDirectoriesTests { + private final byte[] DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + @Test + void testModDirectoriesClient() throws IOException { + Path gameDir = Files.createTempDirectory("loader-test"); + GlobalDirectories globalDirectories = GlobalDirectories.create(EnvType.CLIENT, gameDir); + ModDirectories directories = new ModDirectoriesImpl("test", gameDir, globalDirectories); + + Files.write(directories.getCacheDir().resolve("test.txt"), DATA); + Files.write(directories.getGlobalCacheDir().resolve("test.txt"), DATA); + Files.write(directories.getDataDir().resolve("test.txt"), DATA); + Files.write(directories.getGlobalDataDir().resolve("test.txt"), DATA); + } + + @Test + void testModDirectoriesServer() throws IOException { + Path gameDir = Files.createTempDirectory("loader-test"); + GlobalDirectories globalDirectories = GlobalDirectories.create(EnvType.SERVER, gameDir); + ModDirectories directories = new ModDirectoriesImpl("test", gameDir, globalDirectories); + + Files.write(directories.getCacheDir().resolve("test.txt"), DATA); + Files.write(directories.getGlobalCacheDir().resolve("test.txt"), DATA); + Files.write(directories.getDataDir().resolve("test.txt"), DATA); + Files.write(directories.getGlobalDataDir().resolve("test.txt"), DATA); + } +}