├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── release-publish.yml │ └── test.yml ├── .gitignore ├── .run └── modrinth-modpack (slug).run.xml ├── DEVELOPMENT.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── dev ├── .gitignore ├── curseforge.http ├── github.http ├── modrinth.http ├── neoforge version combos.drawio ├── neoforge.http ├── paper.http ├── purpur.http └── wiremock │ ├── README.md │ ├── data │ ├── __files │ │ └── .gitignore │ └── mappings │ │ └── .gitignore │ └── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── me │ │ └── itzg │ │ └── helpers │ │ ├── CharsetDetector.java │ │ ├── McImageHelper.java │ │ ├── assertcmd │ │ ├── AssertCommand.java │ │ ├── EvalExistence.java │ │ ├── FileExists.java │ │ ├── FileNotExists.java │ │ ├── JsonPathEquals.java │ │ └── PropertyEquals.java │ │ ├── cache │ │ ├── ApiCaching.java │ │ ├── ApiCachingDisabled.java │ │ ├── ApiCachingImpl.java │ │ ├── CacheArgs.java │ │ └── CacheIndex.java │ │ ├── curseforge │ │ ├── CategoryInfo.java │ │ ├── CurseForgeApiClient.java │ │ ├── CurseForgeFilesCommand.java │ │ ├── CurseForgeFilesManifest.java │ │ ├── CurseForgeInstaller.java │ │ ├── CurseForgeManifest.java │ │ ├── ExcludeIncludeIds.java │ │ ├── ExcludeIncludesContent.java │ │ ├── FileHashInvalidException.java │ │ ├── FileHashVerifier.java │ │ ├── InstallCurseForgeCommand.java │ │ ├── LevelFrom.java │ │ ├── MissingModsException.java │ │ ├── ModFileIds.java │ │ ├── ModFileRefResolver.java │ │ ├── ModPackResults.java │ │ ├── ModpacksPageUrlParser.java │ │ ├── OutputPaths.java │ │ ├── OutputSubdirResolver.java │ │ ├── OverridesApplier.java │ │ ├── OverridesFromZipApplier.java │ │ ├── PathWithInfo.java │ │ ├── ResolvedModFile.java │ │ ├── SchemasCommand.java │ │ ├── model │ │ │ ├── Category.java │ │ │ ├── CurseForgeFile.java │ │ │ ├── CurseForgeMod.java │ │ │ ├── CurseForgeResponse.java │ │ │ ├── FileDependency.java │ │ │ ├── FileHash.java │ │ │ ├── FileIndex.java │ │ │ ├── FileModule.java │ │ │ ├── FileRelationType.java │ │ │ ├── FileReleaseType.java │ │ │ ├── FileStatus.java │ │ │ ├── GetCategoriesResponse.java │ │ │ ├── GetModFileResponse.java │ │ │ ├── GetModFilesResponse.java │ │ │ ├── GetModResponse.java │ │ │ ├── HashAlgo.java │ │ │ ├── ManifestFileRef.java │ │ │ ├── ManifestMinecraftInfo.java │ │ │ ├── ManifestType.java │ │ │ ├── MinecraftModpackManifest.java │ │ │ ├── ModAsset.java │ │ │ ├── ModAuthor.java │ │ │ ├── ModLinks.java │ │ │ ├── ModLoader.java │ │ │ ├── ModLoaderType.java │ │ │ ├── ModStatus.java │ │ │ ├── ModsSearchResponse.java │ │ │ ├── SortableGameVersion.java │ │ │ └── package-info.java │ │ └── package-info.java │ │ ├── env │ │ ├── EnvironmentVariablesProvider.java │ │ ├── InterpolationException.java │ │ ├── Interpolator.java │ │ ├── MissingVariablesException.java │ │ ├── SimplePlaceholders.java │ │ └── StandardEnvironmentVariablesProvider.java │ │ ├── errors │ │ ├── EmitsExitCode.java │ │ ├── ExceptionHandler.java │ │ ├── ExitCodeMapper.java │ │ ├── ExitCodeProvider.java │ │ ├── GenericException.java │ │ ├── InvalidParameterException.java │ │ ├── RateLimitException.java │ │ └── Validators.java │ │ ├── fabric │ │ ├── FabricLauncherInstaller.java │ │ ├── FabricManifest.java │ │ ├── FabricMetaClient.java │ │ ├── InstallFabricLoaderCommand.java │ │ ├── InstallerEntry.java │ │ ├── LoaderResponseEntry.java │ │ ├── LocalFile.java │ │ ├── Origin.java │ │ ├── RemoteFile.java │ │ ├── VersionEntry.java │ │ └── Versions.java │ │ ├── files │ │ ├── AntPathMatcher.java │ │ ├── BaseManifest.java │ │ ├── ChecksumAlgo.java │ │ ├── Checksums.java │ │ ├── FileTreeSnapshot.java │ │ ├── IoStreams.java │ │ ├── ManifestException.java │ │ ├── Manifests.java │ │ ├── ReactiveFileUtils.java │ │ ├── ResultsFileWriter.java │ │ ├── TabularOutput.java │ │ ├── TomlPathCommand.java │ │ └── YamlPathCommand.java │ │ ├── find │ │ ├── FindCommand.java │ │ ├── FindFilesVisitor.java │ │ ├── FindType.java │ │ ├── FindTypeConverter.java │ │ ├── MatchHandler.java │ │ ├── PathMatcherConverter.java │ │ └── TrackShallowest.java │ │ ├── forge │ │ ├── ForgeInstaller.java │ │ ├── ForgeInstallerResolver.java │ │ ├── ForgeManifest.java │ │ ├── InstallForgeCommand.java │ │ ├── InstallNeoForgeCommand.java │ │ ├── InstallerResolver.java │ │ ├── LegacyManifest.java │ │ ├── NeoForgeInstallerResolver.java │ │ ├── PromoEntry.java │ │ ├── ProvidedInstallerResolver.java │ │ ├── ResolvedVersions.java │ │ ├── VersionPair.java │ │ └── model │ │ │ └── PromotionsSlim.java │ │ ├── get │ │ ├── EntityWriter.java │ │ ├── ExtendedRequestRetryStrategy.java │ │ ├── GetCommand.java │ │ ├── JsonPathOutputHandler.java │ │ ├── PrintWriterHandler.java │ │ ├── RequestFailedException.java │ │ └── UnexpectedContentTypeException.java │ │ ├── github │ │ ├── DownloadLatestAssetCommand.java │ │ ├── GithubClient.java │ │ ├── GithubCommands.java │ │ └── model │ │ │ ├── Asset.java │ │ │ └── Release.java │ │ ├── http │ │ ├── ContentTypeValidator.java │ │ ├── DeriveFilenameHandler.java │ │ ├── FailedRequestException.java │ │ ├── Fetch.java │ │ ├── FetchBuilderBase.java │ │ ├── FileDownloadStatus.java │ │ ├── FileDownloadStatusHandler.java │ │ ├── FileDownloadedHandler.java │ │ ├── FilenameExtractor.java │ │ ├── FormFetchBuilder.java │ │ ├── HttpClientException.java │ │ ├── LatchingUrisInterceptor.java │ │ ├── LenientUriConverter.java │ │ ├── LoggingResponseHandler.java │ │ ├── NotModifiedHandler.java │ │ ├── ObjectFetchBuilder.java │ │ ├── ObjectListFetchBuilder.java │ │ ├── OutputResponseHandler.java │ │ ├── OutputToDirectoryFetchBuilder.java │ │ ├── OutputToDirectoryHandler.java │ │ ├── PathOrUri.java │ │ ├── PathOrUriConverter.java │ │ ├── RequestAssembler.java │ │ ├── RequestResponseAssembler.java │ │ ├── ResponseParsingException.java │ │ ├── SharedFetch.java │ │ ├── SharedFetchArgs.java │ │ ├── SpecificFileFetchBuilder.java │ │ ├── StringFetchBuilder.java │ │ ├── UriBuilder.java │ │ ├── Uris.java │ │ └── package-info.java │ │ ├── json │ │ └── ObjectMappers.java │ │ ├── logger │ │ └── CustomHighlight.java │ │ ├── lombok.config │ │ ├── modrinth │ │ ├── ExcludeIncludesContent.java │ │ ├── FetchedPack.java │ │ ├── FileInclusionCalculator.java │ │ ├── FilePackFetcher.java │ │ ├── InstallModrinthModpackCommand.java │ │ ├── Installation.java │ │ ├── LegacyModrinthManifest.java │ │ ├── Loader.java │ │ ├── ModpackLoader.java │ │ ├── ModrinthApiClient.java │ │ ├── ModrinthApiPackFetcher.java │ │ ├── ModrinthCommand.java │ │ ├── ModrinthHttpPackFetcher.java │ │ ├── ModrinthManifest.java │ │ ├── ModrinthModpackManifest.java │ │ ├── ModrinthPackFetcher.java │ │ ├── ModrinthPackInstaller.java │ │ ├── NoApplicableVersionsException.java │ │ ├── NoFilesAvailableException.java │ │ ├── ProjectRef.java │ │ ├── ResolvedProject.java │ │ └── model │ │ │ ├── Constants.java │ │ │ ├── DependencyId.java │ │ │ ├── DependencyType.java │ │ │ ├── Env.java │ │ │ ├── EnvType.java │ │ │ ├── ModpackIndex.java │ │ │ ├── Project.java │ │ │ ├── ProjectType.java │ │ │ ├── ServerSide.java │ │ │ ├── Version.java │ │ │ ├── VersionDependency.java │ │ │ ├── VersionFile.java │ │ │ └── VersionType.java │ │ ├── mvn │ │ ├── MavenDownloadCommand.java │ │ ├── MavenMetadata.java │ │ └── MavenRepoApi.java │ │ ├── paper │ │ ├── InstallPaperCommand.java │ │ ├── PaperDownloadsClient.java │ │ ├── PaperManifest.java │ │ └── model │ │ │ ├── BuildInfo.java │ │ │ ├── ProjectInfo.java │ │ │ ├── ReleaseChannel.java │ │ │ ├── VersionBuilds.java │ │ │ ├── VersionInfo.java │ │ │ └── VersionMeta.java │ │ ├── patch │ │ ├── FileFormat.java │ │ ├── Json5FileFormat.java │ │ ├── JsonFileFormat.java │ │ ├── ObjectMapperFileFormat.java │ │ ├── PatchCommand.java │ │ ├── PatchSetProcessor.java │ │ ├── TomlFileFormat.java │ │ ├── ValueTypeConverter.java │ │ ├── YamlFileFormat.java │ │ └── model │ │ │ ├── PatchAddOperation.java │ │ │ ├── PatchDefinition.java │ │ │ ├── PatchOperation.java │ │ │ ├── PatchPutOperation.java │ │ │ ├── PatchSet.java │ │ │ └── PatchSetOperation.java │ │ ├── properties │ │ ├── PropertyDefinition.java │ │ └── SetPropertiesCommand.java │ │ ├── purpur │ │ ├── InstallPurpurCommand.java │ │ ├── PurpurDownloadsClient.java │ │ ├── PurpurManifest.java │ │ └── model │ │ │ ├── ProjectInfo.java │ │ │ ├── VersionInfo.java │ │ │ └── VersionMeta.java │ │ ├── quilt │ │ ├── InstallQuiltCommand.java │ │ ├── QuiltInstaller.java │ │ └── QuiltManifest.java │ │ ├── singles │ │ ├── Asciify.java │ │ ├── HashCommand.java │ │ ├── MoreCollections.java │ │ ├── NetworkInterfacesCommand.java │ │ └── TestLoggingCommand.java │ │ ├── sync │ │ ├── CopyingFileProcessor.java │ │ ├── FileProcessor.java │ │ ├── InterpolateCommand.java │ │ ├── InterpolatingFileProcessor.java │ │ ├── MulitCopyCommand.java │ │ ├── MultiCopyManifest.java │ │ ├── ReplaceEnvOptions.java │ │ ├── Sync.java │ │ ├── SyncAndInterpolate.java │ │ └── SynchronizingFileVisitor.java │ │ ├── users │ │ ├── ExistingFileBehavior.java │ │ ├── ManageUsersCommand.java │ │ ├── MojangUserApi.java │ │ ├── PlayerdbUserApi.java │ │ ├── Type.java │ │ ├── UserApi.java │ │ ├── UserApiProvider.java │ │ ├── UuidQuirks.java │ │ ├── ext │ │ │ ├── MojangProfile.java │ │ │ ├── PlayerdbPlayer.java │ │ │ └── PlayerdbResponse.java │ │ └── model │ │ │ ├── JavaOp.java │ │ │ └── JavaUser.java │ │ ├── vanillatweaks │ │ ├── LegacyManifest.java │ │ ├── VanillaTweaksCommand.java │ │ ├── VanillaTweaksManifest.java │ │ └── model │ │ │ ├── PackDefinition.java │ │ │ ├── Type.java │ │ │ └── ZipLinkResponse.java │ │ └── versions │ │ ├── CompareVersionsCommand.java │ │ ├── Comparison.java │ │ ├── JavaReleaseCommand.java │ │ ├── MinecraftVersionsApi.java │ │ ├── ResolveMinecraftVersionCommand.java │ │ └── VersionManifestV2.java └── resources │ └── logback.xml └── test ├── java └── me │ └── itzg │ └── helpers │ ├── CharsetDetectorTest.java │ ├── LatchingExecutionExceptionHandler.java │ ├── MoreAssertions.java │ ├── TestLoggingAppender.java │ ├── assertcmd │ ├── FileExistsTest.java │ ├── FileNotExistsTest.java │ ├── JsonPathEqualsTest.java │ └── PropertyEqualsTest.java │ ├── curseforge │ ├── CurseForgeApiClientTest.java │ ├── CurseForgeFilesCommandTest.java │ ├── CurseForgeInstallerTest.java │ ├── FileHashVerifierTest.java │ ├── ModFileRefResolverTest.java │ └── ModpacksPageUrlParserTest.java │ ├── env │ ├── InterpolatorTest.java │ └── MappedEnvVarProvider.java │ ├── fabric │ ├── FabricLauncherInstallerTest.java │ ├── http-client.env.json │ └── research.http │ ├── files │ ├── AntPathMatcherTest.java │ ├── ManifestsTest.java │ ├── TabularOutputTest.java │ ├── TomlPathCommandTest.java │ └── YamlPathCommandTest.java │ ├── find │ └── FindCommandTest.java │ ├── forge │ ├── NeoForgeInstallerResolverTest.java │ └── ProvidedInstallerResolverTest.java │ ├── get │ ├── ExistsTest.java │ ├── GetCommandTest.java │ ├── JsonPathTests.java │ ├── MockServerSupport.java │ ├── OutputToDirTest.java │ └── OutputToFileTest.java │ ├── github │ └── DownloadLatestAssetCommandTest.java │ ├── http │ ├── LenientUriConverterTest.java │ ├── ObjectFetchBuilderTest.java │ ├── ObjectListFetchBuilderTest.java │ ├── OutputToDirectoryFetchBuilderTest.java │ └── SpecificFileFetchBuilderTest.java │ ├── lombok.config │ ├── modrinth │ ├── InstallModrinthModpackCommandTest.java │ ├── ModrinthApiClientTest.java │ ├── ModrinthApiPackFetcherTest.java │ ├── ModrinthCommandTest.java │ ├── ModrinthHttpPackFetcherTest.java │ ├── ModrinthPackInstallerTest.java │ ├── ModrinthTestHelpers.java │ └── ProjectRefTest.java │ ├── mvn │ └── MavenRepoApiTest.java │ ├── paper │ └── PaperDownloadsClientTest.java │ ├── patch │ ├── PatchSetProcessorTest.java │ └── ValueTypeConverterTest.java │ ├── properties │ └── SetPropertiesCommandTest.java │ ├── singles │ └── HashCommandTest.java │ ├── sync │ ├── InterpolatingFileProcessorTest.java │ ├── MulitCopyCommandTest.java │ └── SyncAndInterpolateTest.java │ ├── users │ └── ManageUsersCommandTest.java │ ├── vanillatweaks │ └── VanillaTweaksCommandTest.java │ └── versions │ └── CompareVersionsCommandTest.java └── resources ├── ModrinthCommandTest └── __files │ ├── NOTES.md │ ├── bulk-geyser.json │ ├── bulk-lithium.json │ ├── modrinth-project-version-cloth-config.json │ ├── modrinth-project-version-fabric-api.json │ ├── modrinth-projects-fabric-api-cloth-config.json │ └── project-geyser-only-betas.json ├── __files ├── fabric-empty-launcher.jar ├── folia │ ├── projects-folia.json │ └── versions-1-20-6-builds.json ├── forge │ ├── neoforged-forge-maven-metadata.xml │ └── neoforged-neoforge-maven-metadata.xml ├── github │ └── release-with-sources-jar.json ├── modrinth │ ├── project-BITzwT7B-version-resp.json │ ├── project-dynmap-bad-server-side.json │ ├── project-version-chunky-fabric-1.21.1.json │ └── project-version-only-beta.json └── paper │ ├── version-builds-response_mix_default_latest.json │ └── version-builds-response_only_experimental.json ├── cf-excludeInclude-ids.json ├── cf-excludeInclude-slugs.json ├── content ├── excluded │ └── notthis.txt ├── mainmenu.json ├── never.txt ├── plain.txt └── testing.txt ├── curseforge └── mappings │ ├── README.md │ ├── cdn.json │ ├── v1_categories-452fda0d-5d8a-48ad-a86c-b3d73cb7738d.json │ ├── v1_mods_238222.json │ ├── v1_mods_238222_files_4434385-8de5c26d-b14a-4689-815b-448fc69ecc35.json │ ├── v1_mods_238222_files_4593548-302af753-b376-4818-86ce-aa930437b4d1.json │ ├── v1_mods_238222_files_4615177-dee8eca2-74b7-4997-acca-5d4ecec61f0c.json │ ├── v1_mods_238222_files_4644453-b7ba62e2-168c-4aa0-aaab-92acfed0d285.json │ ├── v1_mods_31043.json │ ├── v1_mods_31054.json │ ├── v1_mods_31054_files_3677516-37c56a78-938f-44ad-ae0c-b155be83c210.json │ ├── v1_mods_667389.json │ ├── v1_mods_search-2dd68b95-c81b-49ad-829a-5d89c1c1b2c2.json │ ├── v1_mods_search-781cf780-7195-4075-8193-2c997834472c.json │ ├── v1_mods_search-9857bbc1-5406-4357-9d79-1030a97f7a72.json │ └── v1_mods_search-f693f170-735e-4001-8242-b4c24a1e4e21.json ├── fabric ├── mappings │ ├── v2_versions_game.json │ ├── v2_versions_installer.json │ ├── v2_versions_loader_1_19_2.json │ ├── v2_versions_loader_1_19_2_server_jar-GET.json │ ├── v2_versions_loader_1_19_2_server_jar-HEAD.json │ ├── v2_versions_loader_1_19_3.json │ ├── v2_versions_loader_1_19_3_server_jar-GET.json │ └── v2_versions_loader_1_19_3_server_jar-HEAD.json └── test-file.txt ├── forge ├── forge-1.20.2-48.1.0-installer-trimmed.jar ├── version-cleanroom.json ├── version-forge-1.12.2.json └── version-forge-1.20.2.json ├── logback-test.xml ├── mvn └── __files │ └── packwiz_maven-metadata.xml ├── paper-env.yml ├── paper-out.yml ├── paper.yml ├── patch-example.json ├── patch ├── expected-setInJson.json ├── expected-setInJson5.json5 ├── expected-setInToml.toml ├── expected-setInYaml.yaml ├── expected-setNativeTypes.yaml ├── expected-setWithEnv.yaml ├── testing-with-array.json ├── testing-with-comment.json ├── testing.json ├── testing.json5 ├── testing.toml └── testing.yaml ├── properties ├── property-definitions.json ├── server.properties ├── with-escapes.txt └── with-unicode.txt ├── server-setup-config.yaml ├── test3.txt ├── vanillatweaks └── mappings │ ├── assets_server_sharecodephp-af3b07fa-cb39-44f3-b8cb-871076311d74.json │ ├── assets_server_sharecodephp-cf1755e0-a0c4-4020-a97d-adc860882596.json │ ├── assets_server_sharecodephp-d520ebaa-7346-4228-af50-bee647057b5b.json │ ├── assets_server_zipcraftingtweaksphp-c9d8e902-29f4-4537-bd14-18232974d32e.json │ ├── assets_server_zipdatapacksphp-92d91d61-e30a-4313-86c7-c84d97fe656f.json │ ├── assets_server_zipresourcepacksphp-c8bbd8c7-7417-4bc5-8452-8c9ba68d766b.json │ ├── download_vanillatweaks_c826799zip-4e6d2f20-a904-44db-83da-dea1e3f32b9f.json │ ├── download_vanillatweaks_d952917_unzip_mezip-d6ff5215-fe9c-446c-b8dc-0f01a3b8226f.json │ └── download_vanillatweaks_r201938zip-5467e5f1-f733-4946-9ca3-00253d66b80a.json └── velocity.toml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/java 3 | { 4 | "name": "Java", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/java:17", 7 | 8 | "features": { 9 | "ghcr.io/devcontainers/features/java:1": { 10 | "version": "none", 11 | "installMaven": "false", 12 | "installGradle": "true" 13 | }, 14 | "ghcr.io/devcontainers/features/sshd:1": { 15 | "version": "latest" 16 | } 17 | } 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "java -version", 24 | 25 | // Configure tool-specific properties. 26 | // "customizations": {}, 27 | 28 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 29 | // "remoteUser": "root" 30 | } 31 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 3 | custom: 4 | - https://www.buymeacoffee.com/itzg 5 | - https://paypal.me/itzg 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: gradle 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | patches: 13 | patterns: 14 | - "*" 15 | update-types: 16 | - "patch" 17 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: Enhancements 7 | labels: 8 | - enhancement 9 | - title: Bug Fixes 10 | labels: 11 | - bug 12 | - title: Documentation 13 | labels: 14 | - documentation 15 | - title: Other Changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/release-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release and publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | - "[0-9]+.[0-9]+.[0-9]+-*" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | uses: itzg/github-workflows/.github/workflows/gradle-build.yml@main 13 | with: 14 | arguments: > 15 | test 16 | githubPublishApplication 17 | scoop-bucket-repo: itzg/scoop-bucket 18 | homebrew-tap-repo: itzg/homebrew-tap 19 | secrets: 20 | GITHUB_PUBLISH_TOKEN: ${{ secrets.PUSH_ACCESS_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "*.md" 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - "*.md" 14 | 15 | jobs: 16 | test: 17 | uses: itzg/github-workflows/.github/workflows/gradle-build.yml@main 18 | with: 19 | arguments: test 20 | include-test-report: true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.vscode/ 3 | /target/ 4 | /build/ 5 | /bin/ 6 | /.gradle/ 7 | 8 | /tmp/ 9 | 10 | /dependency-reduced-pom.xml 11 | /pom.xml.releaseBackup 12 | /release.properties 13 | -------------------------------------------------------------------------------- /.run/modrinth-modpack (slug).run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Ad hoc testing 2 | 3 | Beyond the unit tests, ad hoc "integration testing" can be done by running via Gradle passing the intended command-line via `--args`, such as: 4 | 5 | ```shell 6 | ./gradlew run --args="assert fileExists build.gradle" 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Geoff Bourne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | http-client.private.env.json -------------------------------------------------------------------------------- /dev/curseforge.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET https://api.curseforge.com/v1/categories?gameId=432&classesOnly=true 3 | x-api-key: {{cfApiKey}} 4 | 5 | <> 2023-04-01T131517.200.json 6 | <> 2023-03-30T083426.200.json 7 | 8 | ### 9 | # mod 10 | GET https://api.curseforge.com/v1/mods/search?gameId=432&slug=jei&classId=6 11 | x-api-key: {{cfApiKey}} 12 | 13 | ### modpack that claims neoforge support...but uses 1.18.2 of Forge, which is not availalbe in NeoForge 14 | GET https://api.curseforge.com/v1/mods/search?gameId=432&slug=projekt-arcturus&classId=4471 15 | x-api-key: {{cfApiKey}} 16 | 17 | ### 18 | GET https://api.curseforge.com/v1/mods/search?gameId=432&slug=lazy-ae2&classId=6 19 | x-api-key: {{cfApiKey}} 20 | 21 | ### 22 | GET https://api.curseforge.com/v1/mods/322347/files/3254160 23 | x-api-key: {{cfApiKey}} 24 | 25 | ### 26 | # mod 27 | GET https://api.curseforge.com/v1/mods/238222 28 | x-api-key: {{cfApiKey}} 29 | 30 | ### 31 | # bukkit-plugin 32 | GET https://api.curseforge.com/v1/mods/31043 33 | x-api-key: {{cfApiKey}} 34 | 35 | ### 36 | GET https://api.curseforge.com/v1/mods/694605/files/4098018/download-url 37 | x-api-key: {{cfApiKey}} 38 | 39 | ### 40 | GET https://api.curseforge.com/v1/mods/238222/files/4644453 41 | x-api-key: {{cfApiKey}} 42 | 43 | <> 2023-04-01T192357.200.json 44 | 45 | ### 46 | GET https://api.curseforge.com/v1/mods/622737/files/4560441 47 | x-api-key: {{cfApiKey}} 48 | 49 | <> 2023-03-30T081023.200.json 50 | <> 2023-03-30T075419.200.json 51 | <> 2023-03-30T074422.200.json 52 | 53 | ### 54 | GET https://api.curseforge.com/v1/mods/707734/files/4415193 55 | x-api-key: {{cfApiKey}} 56 | 57 | <> 2023-03-30T080528.403.html 58 | 59 | 60 | ### 61 | GET http://localhost:8080/v1/mods/238222/files/4615177 62 | x-api-key: {{cfApiKey}} 63 | -------------------------------------------------------------------------------- /dev/github.http: -------------------------------------------------------------------------------- 1 | ### 2 | @org = kettingpowered 3 | @repo = kettinglauncher 4 | GET https://api.github.com/repos/{{org}}/{{repo}}/releases/latest 5 | Accept: Accept: application/vnd.github+json -------------------------------------------------------------------------------- /dev/neoforge.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET https://maven.neoforged.net/api/maven/versions 3 | 4 | ### 5 | GET https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge -------------------------------------------------------------------------------- /dev/paper.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET https://api.papermc.io/v2/projects/paper -------------------------------------------------------------------------------- /dev/purpur.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET https://api.purpurmc.org/v2/purpur/1.20 3 | User-Agent: mc-image-helper 4 | 5 | <> f-3.txt 6 | <> f-2.txt 7 | 8 | ### 9 | GET https://api.purpurmc.org/v2/purpur/1.19.4/1985 10 | User-Agent: mc-image-helper 11 | 12 | <> 2023-06-13T204234.200.json 13 | 14 | ### 15 | GET https://api.purpurmc.org/v2/purpur/1.19.4/1985/download 16 | User-Agent: mc-image-helper 17 | 18 | <> purpur-1.19.4-1985.jar 19 | <> 2023-06-13T170922.200.json 20 | <> f-1.txt 21 | <> f.txt -------------------------------------------------------------------------------- /dev/wiremock/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Starting 3 | 4 | ```shell 5 | docker compose up -d 6 | ``` 7 | 8 | ## Recording 9 | 10 | Visit http://localhost:8080/__admin/recorder to set target URL to proxy and start recording. 11 | 12 | Configure the operation to use as the base URL. 13 | 14 | Click the "Stop" button on the recorder page and recorded stub mappings will be saved in the `mappings` directory. -------------------------------------------------------------------------------- /dev/wiremock/data/__files/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/dev/wiremock/data/__files/.gitignore -------------------------------------------------------------------------------- /dev/wiremock/data/mappings/.gitignore: -------------------------------------------------------------------------------- 1 | *.json -------------------------------------------------------------------------------- /dev/wiremock/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | wiremock: 5 | image: wiremock/wiremock:2.35.0 6 | volumes: 7 | - ./data:/home/wiremock 8 | ports: 9 | - "8080:8080" -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | rootProject.name = 'mc-image-helper' 6 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/CharsetDetector.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.CharBuffer; 9 | import java.nio.charset.CharacterCodingException; 10 | import java.nio.charset.Charset; 11 | import java.nio.charset.CharsetDecoder; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.Arrays; 14 | 15 | public class CharsetDetector { 16 | public static final Charset[] KNOWN_CHARSETS = { 17 | StandardCharsets.UTF_8, 18 | StandardCharsets.ISO_8859_1, 19 | StandardCharsets.US_ASCII, 20 | StandardCharsets.UTF_16 21 | }; 22 | 23 | @Data @RequiredArgsConstructor 24 | public static class Result { 25 | final Charset charset; 26 | final CharBuffer content; 27 | } 28 | 29 | public static Result detect(byte[] content) throws IOException { 30 | for (Charset c : KNOWN_CHARSETS) { 31 | final CharsetDecoder decoder = c.newDecoder(); 32 | try { 33 | return new Result(c, decoder.decode(ByteBuffer.wrap(content))); 34 | } catch (CharacterCodingException e) { 35 | // not this one 36 | } 37 | } 38 | throw new IOException("Unknown character encoding. Tried "+ Arrays.toString(KNOWN_CHARSETS)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/assertcmd/AssertCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.assertcmd; 2 | 3 | import picocli.CommandLine.Command; 4 | 5 | @Command(name = "assert", description = "Provides assertion operators for verifying container setup", 6 | subcommands = { 7 | FileExists.class, 8 | FileNotExists.class, 9 | JsonPathEquals.class, 10 | PropertyEquals.class, 11 | } 12 | ) 13 | public class AssertCommand { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/assertcmd/FileExists.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.assertcmd; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.Callable; 5 | import picocli.CommandLine.Command; 6 | import picocli.CommandLine.ExitCode; 7 | import picocli.CommandLine.Parameters; 8 | 9 | @Command(name = "fileExists") 10 | class FileExists implements Callable { 11 | 12 | @Parameters 13 | List paths; 14 | 15 | @Override 16 | public Integer call() throws Exception { 17 | boolean missing = false; 18 | 19 | if (paths != null) { 20 | for (String path : paths) { 21 | if (!EvalExistence.exists(path)) { 22 | System.err.printf("%s does not exist%n", path); 23 | missing = true; 24 | } 25 | } 26 | } 27 | 28 | return missing ? ExitCode.SOFTWARE : ExitCode.OK; 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/assertcmd/FileNotExists.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.assertcmd; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.Callable; 5 | import me.itzg.helpers.assertcmd.EvalExistence.MatchingPaths; 6 | import picocli.CommandLine.Command; 7 | import picocli.CommandLine.ExitCode; 8 | import picocli.CommandLine.Parameters; 9 | 10 | @Command(name = "fileNotExists") 11 | class FileNotExists implements Callable { 12 | 13 | @Parameters 14 | List paths; 15 | 16 | @Override 17 | public Integer call() throws Exception { 18 | boolean failed = false; 19 | 20 | if (paths != null) { 21 | for (String path : paths) { 22 | final MatchingPaths matchingPaths = EvalExistence.matchingPaths(path); 23 | if (!matchingPaths.paths.isEmpty()) { 24 | if (matchingPaths.globbing) { 25 | System.err.printf("The files %s exist looking at %s%n", matchingPaths.paths, path); 26 | } 27 | else { 28 | System.err.printf("%s exists%n", path); 29 | } 30 | failed = true; 31 | } 32 | } 33 | } 34 | 35 | return failed ? ExitCode.SOFTWARE : ExitCode.OK; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/assertcmd/JsonPathEquals.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.assertcmd; 2 | 3 | import com.jayway.jsonpath.DocumentContext; 4 | import com.jayway.jsonpath.JsonPath; 5 | import com.jayway.jsonpath.PathNotFoundException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.Objects; 9 | import java.util.concurrent.Callable; 10 | import picocli.CommandLine.Command; 11 | import picocli.CommandLine.ExitCode; 12 | import picocli.CommandLine.Option; 13 | 14 | @Command(name = "jsonPathEquals") 15 | public class JsonPathEquals implements Callable { 16 | @Option(names = "--file", required = true) 17 | Path file; 18 | 19 | @Option(names = "--path", required = true) 20 | String jsonPath; 21 | 22 | @Option(names = "--expect", required = true) 23 | String expectedValue; 24 | 25 | @Override 26 | public Integer call() throws Exception { 27 | if (!Files.exists(file)) { 28 | System.err.printf("The file %s does not exist%n", file); 29 | return ExitCode.SOFTWARE; 30 | } 31 | 32 | final DocumentContext doc = JsonPath.parse(file.toFile()); 33 | 34 | final String result; 35 | try { 36 | result = doc.read(jsonPath, String.class); 37 | } catch (PathNotFoundException e) { 38 | System.err.printf("The path %s in %s does not exist%n", 39 | jsonPath, file); 40 | return ExitCode.SOFTWARE; 41 | } 42 | if (!Objects.equals(result, expectedValue)) { 43 | System.err.printf("Expected '%s' at the path %s in %s, but was '%s'%n", 44 | expectedValue, jsonPath, file, result); 45 | return ExitCode.SOFTWARE; 46 | } 47 | 48 | return ExitCode.OK; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/assertcmd/PropertyEquals.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.assertcmd; 2 | 3 | import java.io.BufferedReader; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.Objects; 7 | import java.util.Properties; 8 | import java.util.concurrent.Callable; 9 | import picocli.CommandLine.Command; 10 | import picocli.CommandLine.ExitCode; 11 | import picocli.CommandLine.Option; 12 | 13 | @Command(name = "propertyEquals") 14 | public class PropertyEquals implements Callable { 15 | @Option(names = "--file", description = "Property file", required = true) 16 | Path file; 17 | 18 | @Option(names = "--property", required = true) 19 | String property; 20 | 21 | @Option(names = "--expect", required = true) 22 | String expectedValue; 23 | 24 | @Override 25 | public Integer call() throws Exception { 26 | if (!Files.exists(file)) { 27 | System.err.printf("The file %s does not exist%n", file); 28 | return ExitCode.SOFTWARE; 29 | } 30 | 31 | final Properties properties = new Properties(); 32 | try (BufferedReader reader = Files.newBufferedReader(file)) { 33 | properties.load(reader); 34 | } 35 | 36 | final String value = properties.getProperty(this.property); 37 | if (value == null) { 38 | System.err.printf("The property %s in the file %s does not exist%n", property, file); 39 | return ExitCode.SOFTWARE; 40 | } 41 | 42 | if (!Objects.equals(value, expectedValue)) { 43 | System.err.printf("Expected the property %s in the file %s to be '%s', but was '%s'%n", 44 | property, file, expectedValue, value); 45 | return ExitCode.SOFTWARE; 46 | } 47 | 48 | return ExitCode.OK; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/cache/ApiCaching.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.cache; 2 | 3 | import java.io.IOException; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface ApiCaching extends AutoCloseable { 7 | 8 | Mono cache(String operation, Class returnType, Mono resolver, Object... keys); 9 | 10 | void close() throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/cache/ApiCachingDisabled.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.cache; 2 | 3 | import java.io.IOException; 4 | import reactor.core.publisher.Mono; 5 | 6 | public class ApiCachingDisabled implements ApiCaching { 7 | 8 | @Override 9 | public Mono cache(String operation, Class returnType, Mono resolver, Object... keys) { 10 | return resolver; 11 | } 12 | 13 | @Override 14 | public void close() throws IOException { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/cache/CacheArgs.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.cache; 2 | 3 | import java.time.Duration; 4 | import java.util.Map; 5 | import lombok.Data; 6 | import picocli.CommandLine.Option; 7 | 8 | @Data 9 | public class CacheArgs { 10 | @Option(names = "--api-cache-ttl", paramLabel = "OPERATION=DURATION", 11 | description = "Set individual operation TTLs" 12 | ) 13 | Map cacheDurations; 14 | 15 | @Option(names = "--api-cache-default-ttl", defaultValue = "P2D", paramLabel = "DURATION", 16 | description = "Set default/fallback TTL in ISO-8601 duration format.\nDefault: ${DEFAULT-VALUE}" 17 | ) 18 | Duration defaultCacheDuration; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/cache/CacheIndex.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.cache; 2 | 3 | import java.time.Instant; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class CacheIndex { 10 | private Map> operations = new HashMap<>(); 11 | 12 | @Data 13 | public static class CacheEntry { 14 | private String filename; 15 | private Instant expiresAt; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/CategoryInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.util.Map; 4 | import lombok.AllArgsConstructor; 5 | import me.itzg.helpers.curseforge.model.Category; 6 | 7 | @AllArgsConstructor 8 | public class CategoryInfo { 9 | Map contentClassIds; 10 | Map slugIds; 11 | 12 | public int getClassIdForSlug(String categorySlug) { 13 | final Integer classId = slugIds.get(categorySlug); 14 | if (classId != null) { 15 | return classId; 16 | } 17 | else { 18 | throw new IllegalArgumentException("Unexpected category: " + categorySlug); 19 | } 20 | } 21 | 22 | public Category getCategory(int categoryId) { 23 | final Category category = contentClassIds.get(categoryId); 24 | if (category != null) { 25 | return category; 26 | } 27 | else { 28 | throw new IllegalArgumentException("Unknown category ID: " + categoryId); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/CurseForgeFilesManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.util.List; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.Getter; 7 | import lombok.experimental.SuperBuilder; 8 | import lombok.extern.jackson.Jacksonized; 9 | import me.itzg.helpers.files.BaseManifest; 10 | 11 | @Getter 12 | @SuperBuilder 13 | @Jacksonized 14 | public class CurseForgeFilesManifest extends BaseManifest { 15 | 16 | public static final String ID = "curseforge-files"; 17 | 18 | @Data 19 | @Builder 20 | @Jacksonized 21 | public static class FileEntry { 22 | final ModFileIds ids; 23 | final String filePath; 24 | } 25 | 26 | List entries; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/CurseForgeManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | @Getter 9 | @SuperBuilder 10 | @Jacksonized 11 | public class CurseForgeManifest extends BaseManifest { 12 | 13 | private String modpackName; 14 | private String modpackVersion; 15 | 16 | private String slug; 17 | private int modId; 18 | private int fileId; 19 | private String fileName; 20 | 21 | private String minecraftVersion; 22 | private String modLoaderId; 23 | private String levelName; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ExcludeIncludeIds.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.util.Set; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | @AllArgsConstructor 9 | @Getter 10 | @ToString 11 | public class ExcludeIncludeIds { 12 | Set excludeIds; 13 | Set forceIncludeIds; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ExcludeIncludesContent.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 4 | import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import lombok.Data; 8 | 9 | @JsonSchemaTitle("Mods Exclude/Include File Content") 10 | @Data 11 | public class ExcludeIncludesContent { 12 | 13 | @JsonPropertyDescription("Mods by slug|id to exclude for all modpacks") 14 | private Set globalExcludes; 15 | @JsonPropertyDescription("Mods by slug|id to force include for all modpacks") 16 | private Set globalForceIncludes; 17 | 18 | @JsonPropertyDescription("Specific exclude/includes by modpack slug") 19 | private Map modpacks; 20 | 21 | @Data 22 | public static class ExcludeIncludes { 23 | @JsonPropertyDescription("Mods by slug|id to exclude for this modpack") 24 | private Set excludes; 25 | @JsonPropertyDescription("Mods by slug|id to force include for this modpack") 26 | private Set forceIncludes; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/FileHashInvalidException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | public class FileHashInvalidException extends RuntimeException { 4 | 5 | public FileHashInvalidException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/LevelFrom.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | public enum LevelFrom { 4 | WORLD_FILE, 5 | OVERRIDES 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/MissingModsException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class MissingModsException extends RuntimeException { 8 | private final List needsDownload; 9 | 10 | public MissingModsException(List needsDownload) { 11 | 12 | this.needsDownload = needsDownload; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ModFileIds.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.extern.jackson.Jacksonized; 6 | 7 | @Data 8 | @Builder 9 | @Jacksonized 10 | public class ModFileIds { 11 | final int modId; 12 | final int fileId; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ModPackResults.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import lombok.Data; 4 | 5 | import java.nio.file.Path; 6 | import java.util.List; 7 | 8 | @Data 9 | public class ModPackResults { 10 | private String name; 11 | private String version; 12 | private List files; 13 | private String minecraftVersion; 14 | private String modLoaderId; 15 | private String levelName; 16 | private List needsDownload; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ModpacksPageUrlParser.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import me.itzg.helpers.errors.InvalidParameterException; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | class ModpacksPageUrlParser { 11 | private static final Pattern PAGE_URL_PATTERN = Pattern.compile( 12 | "https://(www|beta)\\.curseforge\\.com/minecraft/modpacks/(?[^/]+?)(/((files|download)(/(?\\d+)?)?)?)?"); 13 | 14 | @Data @Builder 15 | public static class Parsed { 16 | String slug; 17 | Integer fileId; 18 | } 19 | 20 | @NotNull 21 | public static Parsed parse(String pageUrl) { 22 | if (pageUrl == null) { 23 | return Parsed.builder().build(); 24 | } 25 | 26 | final Matcher m = PAGE_URL_PATTERN.matcher(pageUrl); 27 | if (m.matches()) { 28 | final String slug = m.group("slug"); 29 | final String fileIdStr = m.group("fileId"); 30 | if (fileIdStr != null) { 31 | return Parsed.builder() 32 | .slug(slug) 33 | .fileId(Integer.parseInt(fileIdStr)) 34 | .build(); 35 | } 36 | else { 37 | return Parsed.builder() 38 | .slug(slug) 39 | .build(); 40 | } 41 | } 42 | else { 43 | throw new InvalidParameterException("Unexpected CF page URL structure: " + pageUrl); 44 | } 45 | 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/OutputPaths.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.nio.file.Path; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import reactor.core.publisher.Mono; 7 | 8 | @AllArgsConstructor 9 | @Getter 10 | class OutputPaths { 11 | private final Mono modsDir; 12 | private final Mono pluginsDir; 13 | private final Mono worldsDir; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/OverridesApplier.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | import java.util.List; 6 | import lombok.AllArgsConstructor; 7 | 8 | interface OverridesApplier { 9 | 10 | Result apply() throws IOException; 11 | 12 | @AllArgsConstructor 13 | class Result { 14 | 15 | List paths; 16 | String levelName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/PathWithInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.Setter; 6 | import me.itzg.helpers.curseforge.model.CurseForgeFile; 7 | import me.itzg.helpers.curseforge.model.CurseForgeMod; 8 | 9 | import java.nio.file.Path; 10 | 11 | @RequiredArgsConstructor 12 | @Getter @Setter 13 | public class PathWithInfo { 14 | private final Path path; 15 | /** 16 | * If this is a world mod file, then this will be the level name that would reference it 17 | */ 18 | private String levelName; 19 | 20 | private boolean downloadNeeded; 21 | private CurseForgeMod modInfo; 22 | private CurseForgeFile curseForgeFile; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/ResolvedModFile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ResolvedModFile { 7 | final ModFileIds ids; 8 | 9 | String categoryClassSlug; 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/SchemasCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator; 6 | import java.util.concurrent.Callable; 7 | import me.itzg.helpers.json.ObjectMappers; 8 | import picocli.CommandLine.Command; 9 | import picocli.CommandLine.ExitCode; 10 | 11 | @Command(name = "schemas", description = "Output relevant JSON schemas") 12 | public class SchemasCommand implements Callable { 13 | 14 | @Override 15 | public Integer call() throws Exception { 16 | final ObjectMapper objectMapper = ObjectMappers.defaultMapper(); 17 | 18 | final JsonSchemaGenerator schemaGen = new JsonSchemaGenerator( 19 | objectMapper 20 | ); 21 | 22 | final JsonNode schema = schemaGen.generateJsonSchema(ExcludeIncludesContent.class); 23 | System.out.println(objectMapper 24 | .writerWithDefaultPrettyPrinter() 25 | .writeValueAsString(schema)); 26 | 27 | return ExitCode.OK; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/Category.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Category { 7 | private int gameId; 8 | private boolean isClass; 9 | private int classId; 10 | private String name; 11 | private String dateModified; 12 | private int parentCategoryId; 13 | private int id; 14 | private String iconUrl; 15 | private String slug; 16 | private String url; 17 | private int displayIndex; 18 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/CurseForgeFile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeName; 4 | import java.time.Instant; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonTypeName("File") 10 | public class CurseForgeFile { 11 | private int gameId; 12 | private boolean isAvailable; 13 | private String fileName; 14 | private List gameVersions; 15 | private String displayName; 16 | private List sortableGameVersions; 17 | private String downloadUrl; 18 | private Instant fileDate; 19 | private Boolean exposeAsAlternative; 20 | private int modId; 21 | private List modules; 22 | private List dependencies; 23 | private long fileFingerprint; 24 | private FileStatus fileStatus; 25 | private boolean isServerPack; 26 | private FileReleaseType releaseType; 27 | private List hashes; 28 | private Integer parentProjectFileId; 29 | private Integer alternateFileId; 30 | private int id; 31 | private long fileLength; 32 | private long downloadCount; 33 | private Integer serverPackFileId; 34 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/CurseForgeMod.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeName; 4 | import java.util.List; 5 | import lombok.Data; 6 | 7 | @Data 8 | @JsonTypeName("Mod") 9 | public class CurseForgeMod { 10 | private boolean isAvailable; 11 | private boolean allowModDistribution; 12 | private List screenshots; 13 | private int classId; 14 | private List latestFilesIndexes; 15 | private String dateCreated; 16 | private ModAsset logo; 17 | private ModLinks links; 18 | private String dateReleased; 19 | private int id; 20 | private List categories; 21 | private boolean isFeatured; 22 | private String slug; 23 | private int gameId; 24 | private String summary; 25 | private List latestFiles; 26 | private String dateModified; 27 | private int gamePopularityRank; 28 | private int thumbsUpCount; 29 | private String name; 30 | private int mainFileId; 31 | private int primaryCategoryId; 32 | private int downloadCount; 33 | private ModStatus status; 34 | private List authors; 35 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/CurseForgeResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class CurseForgeResponse { 7 | private boolean ok; 8 | private String error; 9 | private T data; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileDependency.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FileDependency { 7 | private FileRelationType relationType; 8 | private int modId; 9 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileHash.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FileHash { 7 | private String value; 8 | private HashAlgo algo; 9 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileIndex.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FileIndex { 7 | private String filename; 8 | private FileReleaseType releaseType; 9 | private String gameVersion; 10 | private Integer gameVersionTypeId; 11 | private ModLoaderType modLoader; 12 | private int fileId; 13 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileModule.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FileModule { 7 | 8 | private String name; 9 | private long fingerprint; 10 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileRelationType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum FileRelationType { 6 | EmbeddedLibrary, 7 | OptionalDependency, 8 | RequiredDependency, 9 | Tool, 10 | Incompatible, 11 | Include; 12 | 13 | @JsonValue 14 | public int toValue() { 15 | return ordinal()+1; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileReleaseType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum FileReleaseType { 6 | release, 7 | beta, 8 | alpha; 9 | 10 | @JsonValue 11 | public int toValue() { 12 | return this.ordinal()+1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/FileStatus.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | /** 6 | * Schema 7 | */ 8 | public enum FileStatus { 9 | Processing, 10 | ChangesRequired, 11 | UnderReview, 12 | Approved, 13 | Rejected, 14 | MalwareDetected, 15 | Deleted, 16 | Archived, 17 | Testing, 18 | Released, 19 | ReadyForReview, 20 | Deprecated, 21 | Baking, 22 | AwaitingPublishing, 23 | FailedPublishing; 24 | 25 | @JsonValue 26 | public int toValue() { 27 | return this.ordinal()+1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/GetCategoriesResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class GetCategoriesResponse { 8 | private List data; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/GetModFileResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class GetModFileResponse { 7 | CurseForgeFile data; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/GetModFilesResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class GetModFilesResponse { 8 | List data; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/GetModResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class GetModResponse { 7 | private CurseForgeMod data; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/HashAlgo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum HashAlgo { 6 | Sha1, 7 | Md5; 8 | 9 | @JsonValue 10 | public int toValue() { 11 | return ordinal() + 1; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ManifestFileRef.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ManifestFileRef { 7 | private int projectID; 8 | private boolean required; 9 | private int fileID; 10 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ManifestMinecraftInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class ManifestMinecraftInfo { 8 | private String version; 9 | private List modLoaders; 10 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ManifestType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | public enum ManifestType { 4 | minecraftModpack 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/MinecraftModpackManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class MinecraftModpackManifest{ 8 | private ManifestMinecraftInfo minecraft; 9 | private int manifestVersion; 10 | private String author; 11 | private String name; 12 | private ManifestType manifestType; 13 | private List files; 14 | private String overrides; 15 | private String version; 16 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModAsset.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ModAsset { 7 | private String description; 8 | private int id; 9 | private String title; 10 | private int modId; 11 | private String url; 12 | private String thumbnailUrl; 13 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModAuthor.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ModAuthor { 7 | private String name; 8 | private int id; 9 | private String url; 10 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModLinks.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ModLinks { 7 | private String sourceUrl; 8 | private String issuesUrl; 9 | private String websiteUrl; 10 | private String wikiUrl; 11 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModLoader.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ModLoader { 7 | private String id; 8 | private boolean primary; 9 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModLoaderType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum ModLoaderType { 6 | Any, // zero-indexed for this one 7 | Forge, 8 | Cauldron, 9 | LiteLoader, 10 | Fabric, 11 | Quilt, 12 | // undocumented as of 2023-07-23 but referenced in https://www.curseforge.com/minecraft/mc-mods/chimes/files/4671986 13 | NeoForge; 14 | 15 | @JsonValue 16 | public int toValue() { 17 | // zero-indexed for this one 18 | return ordinal(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModStatus.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | public enum ModStatus { 6 | New, 7 | ChangesRequired, 8 | UnderSoftReview, 9 | Approved, 10 | Rejected, 11 | ChangesMade, 12 | Inactive, 13 | Abandoned, 14 | Deleted, 15 | UnderReview; 16 | 17 | @JsonValue 18 | public int toValue() { 19 | return ordinal()+1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/ModsSearchResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class ModsSearchResponse { 8 | List data; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/SortableGameVersion.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class SortableGameVersion { 7 | private String gameVersionPadded; 8 | private String gameVersion; 9 | private String gameVersionReleaseDate; 10 | private String gameVersionName; 11 | private int gameVersionTypeId; 12 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/model/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps data model for CurseForge REST API 3 | */ 4 | package me.itzg.helpers.curseforge.model; -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/curseforge/package-info.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/env/EnvironmentVariablesProvider.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | public interface EnvironmentVariablesProvider { 4 | String get(String name); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/env/InterpolationException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | public class InterpolationException extends RuntimeException { 4 | 5 | public InterpolationException(String msg, Throwable e) { 6 | super(msg, e); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/env/MissingVariablesException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | import java.util.List; 4 | 5 | public class MissingVariablesException extends RuntimeException { 6 | private final List variables; 7 | 8 | public MissingVariablesException(List variables) { 9 | super("Missing one or more variables"); 10 | this.variables = variables; 11 | } 12 | 13 | public List getVariables() { 14 | return variables; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/env/StandardEnvironmentVariablesProvider.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | public class StandardEnvironmentVariablesProvider implements EnvironmentVariablesProvider { 4 | @Override 5 | public String get(String name) { 6 | return System.getenv(name); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/EmitsExitCode.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | 11 | /** 12 | * Used to annotate an {@link Exception} class with a status code to produce. 13 | */ 14 | public @interface EmitsExitCode { 15 | 16 | /** 17 | * @return typically a value of 200 or greater to indicate a custom, application exit code 18 | * and 300 or greater for subcommand specific exceptions. 19 | */ 20 | int value(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/ExitCodeMapper.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | import picocli.CommandLine.ExitCode; 4 | import picocli.CommandLine.IExitCodeExceptionMapper; 5 | 6 | public class ExitCodeMapper implements IExitCodeExceptionMapper { 7 | 8 | @Override 9 | public int getExitCode(Throwable exception) { 10 | if (exception instanceof ExitCodeProvider) { 11 | return ((ExitCodeProvider) exception).exitCode(); 12 | } 13 | 14 | final EmitsExitCode annotation = exception.getClass().getAnnotation(EmitsExitCode.class); 15 | if (annotation != null) { 16 | return annotation.value(); 17 | } 18 | 19 | return ExitCode.SOFTWARE; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/ExitCodeProvider.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | public interface ExitCodeProvider { 4 | int exitCode(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/GenericException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | public class GenericException extends RuntimeException { 4 | 5 | public GenericException(String message) { 6 | super(message); 7 | } 8 | 9 | public GenericException(String message, Throwable cause) { 10 | super(message+": "+cause.getMessage(), cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/InvalidParameterException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | import picocli.CommandLine.ExitCode; 4 | 5 | @EmitsExitCode(ExitCode.USAGE) 6 | public class InvalidParameterException extends RuntimeException { 7 | 8 | public InvalidParameterException(String message) { 9 | super(message); 10 | } 11 | 12 | public InvalidParameterException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | import java.time.Instant; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class RateLimitException extends RuntimeException { 8 | 9 | private final Instant delayUntil; 10 | 11 | public RateLimitException(Instant delayUntil, String message, Throwable cause) { 12 | super(message, cause); 13 | this.delayUntil = delayUntil; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/errors/Validators.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.errors; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | import picocli.CommandLine.Model.CommandSpec; 6 | import picocli.CommandLine.ParameterException; 7 | 8 | public class Validators { 9 | public static final Pattern VERSION_OR_LATEST = Pattern.compile("latest|\\d+(\\.\\d+)+(-.+)?", Pattern.CASE_INSENSITIVE); 10 | public static final String DESCRIPTION_MINECRAFT_VERSION = "May be 'latest' or specific version"; 11 | 12 | public static String validateMinecraftVersion(CommandSpec spec, String input) { 13 | final Matcher m = VERSION_OR_LATEST.matcher(input); 14 | if (m.matches()) { 15 | return input.toLowerCase(); 16 | } 17 | 18 | throw new ParameterException(spec.commandLine(), "Invalid value for minecraft version: " + input); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/FabricManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | @Getter 9 | @SuperBuilder 10 | @Jacksonized 11 | public class FabricManifest extends BaseManifest { 12 | 13 | public static final String MANIFEST_ID = "fabric"; 14 | /** 15 | * The path to the launcher. This should also be in {@link #getFiles()}, but provides a specific reference. 16 | */ 17 | String launcherPath; 18 | 19 | /** 20 | * Captures how and specifics about current fabric installation. 21 | */ 22 | Origin origin; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/InstallerEntry.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class InstallerEntry{ 7 | private String maven; 8 | private boolean stable; 9 | private String version; 10 | private String url; 11 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/LoaderResponseEntry.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class LoaderResponseEntry { 7 | 8 | private VersionEntry loader; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/LocalFile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | import lombok.extern.jackson.Jacksonized; 8 | 9 | @Getter 10 | @Builder 11 | @Jacksonized 12 | @EqualsAndHashCode(callSuper = false) 13 | @ToString 14 | public class LocalFile extends Origin { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/Origin.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonSubTypes.Type; 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; 7 | 8 | @JsonTypeInfo(use = Id.NAME) 9 | @JsonSubTypes({ 10 | @Type(value = Versions.class, name = "versions"), 11 | @Type(value = RemoteFile.class, name = "remote") 12 | }) 13 | public abstract class Origin { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/RemoteFile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | import lombok.extern.jackson.Jacksonized; 8 | 9 | @Getter 10 | @Builder 11 | @Jacksonized 12 | @EqualsAndHashCode(callSuper = false) 13 | @ToString 14 | public class RemoteFile extends Origin { 15 | 16 | String uri; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/VersionEntry.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class VersionEntry { 7 | String version; 8 | boolean stable; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/fabric/Versions.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.fabric; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | import lombok.extern.jackson.Jacksonized; 8 | 9 | @Getter 10 | @Builder 11 | @Jacksonized 12 | @EqualsAndHashCode(callSuper = false) 13 | @ToString 14 | public class Versions extends Origin { 15 | 16 | private String game; 17 | private String loader; 18 | @EqualsAndHashCode.Exclude 19 | private String installer; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/AntPathMatcher.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import java.util.Collection; 4 | import java.util.regex.Pattern; 5 | import java.util.stream.Collectors; 6 | 7 | public class AntPathMatcher { 8 | 9 | private final Pattern regex; 10 | 11 | public AntPathMatcher(Collection patterns) { 12 | this.regex = patterns != null && !patterns.isEmpty() ? 13 | convertToRegex(patterns) 14 | : null; 15 | } 16 | 17 | private static Pattern convertToRegex(Collection patterns) { 18 | return Pattern.compile( 19 | patterns.stream() 20 | .map(s -> 21 | // swap these out temporarily to avoid stepping on each other 22 | s.replace("**", "_DSTAR_") 23 | .replace("*", "_STAR_") 24 | // escape special characters 25 | .replaceAll("[.(\\[]", "\\\\$0") 26 | .replace("?", ".") 27 | // ...and then turn into regex equivalent 28 | .replace("_DSTAR_", ".*?") 29 | .replace("_STAR_", "[^/]*?") 30 | ) 31 | // ...and join the whole thing into alternates to end up with one regex to match 32 | // all the requested patterns 33 | .collect(Collectors.joining("|")) 34 | ); 35 | } 36 | 37 | public boolean matches(String input) { 38 | return regex != null 39 | && regex.matcher(input).matches(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/BaseManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; 5 | import lombok.Builder.Default; 6 | import lombok.Data; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | import java.nio.file.Path; 10 | import java.time.Instant; 11 | import java.util.Collection; 12 | 13 | /** 14 | * Sub-classes should be declared with: 15 | *
16 |  * {@code
17 |  * @Getter
18 |  * @SuperBuilder
19 |  * @Jacksonized
20 |  * }
21 |  * 
22 | */ 23 | @Data 24 | @SuperBuilder 25 | @JsonTypeInfo(use = Id.CLASS, property = "@type") 26 | public abstract class BaseManifest { 27 | @Default 28 | Instant timestamp = Instant.now(); 29 | 30 | /** 31 | * NOTE: use {@link Manifests#relativizeAll(Path, Collection)} to remap regular paths into relative paths 32 | */ 33 | Collection files; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/ChecksumAlgo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum ChecksumAlgo { 7 | MD5("md5", "MD5"), 8 | SHA1("sha1", "SHA-1"), 9 | SHA256("sha256", "SHA-256"),; 10 | 11 | private final String prefix; 12 | 13 | private final String jdkAlgo; 14 | 15 | ChecksumAlgo(String prefix, String jdkAlgo) { 16 | this.prefix = prefix; 17 | this.jdkAlgo = jdkAlgo; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/Checksums.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | import org.apache.commons.codec.binary.Hex; 10 | import org.jetbrains.annotations.Blocking; 11 | 12 | public class Checksums { 13 | 14 | private Checksums() { 15 | } 16 | 17 | @Blocking 18 | public static boolean valid(Path file, ChecksumAlgo algo, String expectedCheckum) throws IOException { 19 | final MessageDigest md; 20 | try { 21 | md = MessageDigest.getInstance(algo.getJdkAlgo()); 22 | } catch (NoSuchAlgorithmException e) { 23 | throw new IllegalStateException(e); 24 | } 25 | 26 | try (InputStream inputStream = Files.newInputStream(file)) { 27 | final byte[] buffer = new byte[1024]; 28 | int len; 29 | while ((len = inputStream.read(buffer)) >= 0) { 30 | md.update(buffer, 0, len); 31 | } 32 | } 33 | final byte[] digest = md.digest(); 34 | 35 | return expectedCheckum.toLowerCase().equals(Hex.encodeHexString(digest)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/FileTreeSnapshot.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public class FileTreeSnapshot { 11 | 12 | private final Path dir; 13 | private final Set files; 14 | 15 | protected FileTreeSnapshot(Path dir, Set files) { 16 | this.dir = dir; 17 | this.files = files; 18 | } 19 | 20 | public static FileTreeSnapshot takeSnapshot(Path dir) throws IOException { 21 | try (Stream stream = Files.walk(dir)) { 22 | return new FileTreeSnapshot( 23 | dir, 24 | stream.filter(Files::isRegularFile) 25 | .map(path -> dir.relativize(path).toString()) 26 | .collect(Collectors.toSet()) 27 | ); 28 | } 29 | } 30 | 31 | public Set findNewFiles() throws IOException { 32 | try (Stream stream = Files.walk(dir)) { 33 | return stream 34 | .map(path -> dir.relativize(path).toString()) 35 | .filter(relPath -> !files.contains(relPath)) 36 | .collect(Collectors.toSet()); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/IoStreams.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Path; 6 | import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 7 | import org.apache.commons.compress.archivers.zip.ZipFile; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | /** 11 | * Since Java 8 doesn't have InputStream.transferTo 12 | */ 13 | public class IoStreams { 14 | 15 | @FunctionalInterface 16 | public interface EntryReader { 17 | T read(InputStream in) throws IOException; 18 | } 19 | 20 | /** 21 | * @return the result of entryReader or null if entry not present 22 | */ 23 | @Nullable 24 | public static T readFileFromZip(Path zipFile, String entryName, EntryReader entryReader) throws IOException { 25 | try (ZipFile zip = ZipFile.builder().setPath(zipFile).get()) { 26 | final ZipArchiveEntry entry = zip.getEntry(entryName); 27 | if (entry != null) { 28 | try (InputStream entryStream = zip.getInputStream(entry)) { 29 | return entryReader.read(entryStream); 30 | } 31 | } 32 | else { 33 | return null; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/ManifestException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | public class ManifestException extends RuntimeException { 4 | 5 | public ManifestException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/files/YamlPathCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; 4 | import com.jayway.jsonpath.Configuration; 5 | import com.jayway.jsonpath.DocumentContext; 6 | import com.jayway.jsonpath.JsonPath; 7 | import com.jayway.jsonpath.spi.json.JacksonJsonProvider; 8 | import java.io.File; 9 | import java.util.concurrent.Callable; 10 | import picocli.CommandLine.Command; 11 | import picocli.CommandLine.ExitCode; 12 | import picocli.CommandLine.Option; 13 | import picocli.CommandLine.Parameters; 14 | 15 | @Command(name = "yaml-path", description = "Extracts a path from a YAML file using json-path syntax") 16 | public class YamlPathCommand implements Callable { 17 | 18 | @Option(names = "--file", description = "A YAML file to query") 19 | File yamlFile; 20 | 21 | @Parameters(arity = "1", description = "A YAML/JSON path in to query. Leading root anchor, $, will be added if not present") 22 | String yamlPath; 23 | 24 | @Override 25 | public Integer call() throws Exception { 26 | YAMLMapper yamlMapper = new YAMLMapper(); 27 | 28 | DocumentContext context = JsonPath.using(Configuration.builder() 29 | .jsonProvider(new JacksonJsonProvider(yamlMapper)) 30 | .build()).parse(yamlFile); 31 | 32 | Object result = context.read(yamlPath.startsWith("$") ? yamlPath : "$" + yamlPath); 33 | 34 | System.out.println(result); 35 | 36 | return ExitCode.OK; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/find/FindType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.find; 2 | 3 | public enum FindType { 4 | file, 5 | directory 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/find/FindTypeConverter.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.find; 2 | 3 | import picocli.CommandLine.ITypeConverter; 4 | 5 | /** 6 | * Allows command-line arg to be spelled out enum name or any prefer thereof such as "f" for file 7 | */ 8 | public class FindTypeConverter implements ITypeConverter { 9 | 10 | @Override 11 | public FindType convert(String value) { 12 | final String normalized = value.toLowerCase(); 13 | 14 | for (final FindType findType : FindType.values()) { 15 | if (findType.name().startsWith(normalized)) { 16 | return findType; 17 | } 18 | } 19 | 20 | throw new IllegalArgumentException("Unknown FindType: " + value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/find/MatchHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.find; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.FileVisitResult; 5 | import java.nio.file.Path; 6 | 7 | public interface MatchHandler { 8 | 9 | FileVisitResult handle(Path startingPath, Path matched) throws IOException; 10 | 11 | /** 12 | * @param directory the directory being concluded 13 | * @param matchCount the number of matches within this directory and its subdirectories 14 | * @param depth depth of the directory where zero is one of the starting points 15 | */ 16 | void postDirectory(Path directory, int matchCount, int depth) throws IOException; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/find/PathMatcherConverter.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.find; 2 | 3 | import java.nio.file.FileSystems; 4 | import java.nio.file.PathMatcher; 5 | import picocli.CommandLine.ITypeConverter; 6 | 7 | public class PathMatcherConverter implements ITypeConverter { 8 | 9 | @Override 10 | public PathMatcher convert(String value) { 11 | return FileSystems.getDefault().getPathMatcher( 12 | "glob:" + 13 | // escape any Windows backslashes 14 | value.replace("\\", "\\\\") 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/find/TrackShallowest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.find; 2 | 3 | import java.nio.file.FileVisitResult; 4 | import java.nio.file.Path; 5 | import lombok.Getter; 6 | 7 | class TrackShallowest implements MatchHandler { 8 | 9 | private int shallowestNameCount; 10 | @Getter 11 | private Path shallowest; 12 | @Getter 13 | private Path shallowestStartingPoint; 14 | 15 | @Override 16 | public FileVisitResult handle(Path startingPath, Path matched) { 17 | final int nameCount = startingPath.relativize(matched).getNameCount(); 18 | if (shallowest == null || nameCount < shallowestNameCount) { 19 | shallowest = matched; 20 | shallowestStartingPoint = startingPath; 21 | shallowestNameCount = nameCount; 22 | } 23 | return FileVisitResult.SKIP_SIBLINGS; 24 | } 25 | 26 | @Override 27 | public void postDirectory(Path directory, int matchCount, int depth) { 28 | // n/a 29 | } 30 | 31 | public boolean matched() { 32 | return shallowest != null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/ForgeManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | @Getter 9 | @SuperBuilder 10 | @Jacksonized 11 | public class ForgeManifest extends BaseManifest { 12 | String minecraftVersion; 13 | String forgeVersion; 14 | /** 15 | * absolute path to jar file or run script 16 | */ 17 | String serverEntry; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/InstallerResolver.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import java.nio.file.Path; 4 | 5 | public interface InstallerResolver { 6 | 7 | VersionPair resolve(); 8 | 9 | Path download(String minecraftVersion, String forgeVersion, Path outputDir); 10 | 11 | void cleanup(Path forgeInstallerJar); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/LegacyManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import java.time.Instant; 4 | import java.util.Set; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.extern.jackson.Jacksonized; 8 | 9 | @Data 10 | @Builder 11 | @Jacksonized 12 | public class LegacyManifest { 13 | 14 | public static final String FILENAME = ".forge.manifest"; 15 | 16 | Instant timestamp; 17 | 18 | String minecraftVersion; 19 | String forgeVersion; 20 | /** 21 | * jar file or run script relative to output location 22 | */ 23 | String serverEntry; 24 | Set files; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/PromoEntry.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | class PromoEntry { 9 | 10 | String mcVersion; 11 | String promo; 12 | String forgeVersion; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/ResolvedVersions.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ResolvedVersions { 7 | final String minecraft; 8 | final String forge; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/VersionPair.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | @RequiredArgsConstructor 9 | @ToString @EqualsAndHashCode 10 | public class VersionPair { 11 | 12 | final String minecraft; 13 | final String forge; 14 | @Setter 15 | String variantOverride; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/forge/model/PromotionsSlim.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.forge.model; 2 | 3 | import java.util.Map; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class PromotionsSlim { 8 | Map promos; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/get/EntityWriter.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.get; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.Writer; 6 | import java.nio.charset.StandardCharsets; 7 | import org.apache.hc.core5.http.HttpEntity; 8 | 9 | class EntityWriter { 10 | 11 | static void write(HttpEntity entity, Writer writer) throws IOException { 12 | final byte[] buffer = new byte[1024]; 13 | try (InputStream content = entity.getContent()) { 14 | int length; 15 | while ((length = content.read(buffer)) >= 0) { 16 | writer.write(new String(buffer, 0, length, StandardCharsets.UTF_8)); 17 | } 18 | } 19 | writer.flush(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/get/ExtendedRequestRetryStrategy.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.get; 2 | 3 | import java.io.InterruptedIOException; 4 | import java.net.ConnectException; 5 | import java.net.NoRouteToHostException; 6 | import java.net.UnknownHostException; 7 | import java.util.Arrays; 8 | import javax.net.ssl.SSLException; 9 | import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy; 10 | import org.apache.hc.core5.http.ConnectionClosedException; 11 | import org.apache.hc.core5.http.HttpStatus; 12 | import org.apache.hc.core5.util.TimeValue; 13 | 14 | public class ExtendedRequestRetryStrategy extends DefaultHttpRequestRetryStrategy { 15 | 16 | public ExtendedRequestRetryStrategy(int maxRetries, 17 | int retryDelay) { 18 | super(maxRetries, TimeValue.ofSeconds(retryDelay), 19 | // most of this comes from 20 | // org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy.DefaultHttpRequestRetryStrategy(int, org.apache.hc.core5.util.TimeValue) 21 | Arrays.asList( 22 | InterruptedIOException.class, 23 | UnknownHostException.class, 24 | ConnectException.class, 25 | ConnectionClosedException.class, 26 | NoRouteToHostException.class, 27 | SSLException.class), 28 | Arrays.asList( 29 | HttpStatus.SC_TOO_MANY_REQUESTS, 30 | HttpStatus.SC_SERVICE_UNAVAILABLE, 31 | // some APIs, such as CurseForge, are intermittently responding 403 32 | HttpStatus.SC_FORBIDDEN) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/get/PrintWriterHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.get; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.nio.file.Path; 6 | import me.itzg.helpers.http.LoggingResponseHandler; 7 | import me.itzg.helpers.http.OutputResponseHandler; 8 | import org.apache.hc.core5.http.HttpEntity; 9 | 10 | class PrintWriterHandler extends LoggingResponseHandler implements OutputResponseHandler { 11 | private final PrintWriter writer; 12 | 13 | public PrintWriterHandler(PrintWriter writer) { 14 | this.writer = writer; 15 | } 16 | 17 | @Override 18 | public Path handleEntity(HttpEntity entity) throws IOException { 19 | 20 | EntityWriter.write(entity, writer); 21 | 22 | // no filename to return 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/get/RequestFailedException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.get; 2 | 3 | import java.net.URI; 4 | import me.itzg.helpers.errors.ExitCodeProvider; 5 | import org.apache.hc.client5.http.HttpResponseException; 6 | 7 | public class RequestFailedException extends RuntimeException implements ExitCodeProvider { 8 | 9 | private final int statusCode; 10 | 11 | public RequestFailedException(URI uri, HttpResponseException e) { 12 | super(String.format("Failed to download %s: %s", uri, e.getMessage()), e); 13 | this.statusCode = e.getStatusCode(); 14 | } 15 | 16 | @Override 17 | public int exitCode() { 18 | switch (statusCode) { 19 | case 404: 20 | return 44; 21 | case 429: 22 | return 42; 23 | default: 24 | return statusCode / 100; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/get/UnexpectedContentTypeException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.get; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import me.itzg.helpers.errors.EmitsExitCode; 7 | 8 | @EmitsExitCode(300) 9 | @Getter @ToString 10 | public class UnexpectedContentTypeException extends RuntimeException { 11 | 12 | private final String parsedContentType; 13 | private final List expectedContentTypes; 14 | 15 | public UnexpectedContentTypeException(String parsedContentType, List expectedContentTypes) { 16 | this.parsedContentType = parsedContentType; 17 | this.expectedContentTypes = expectedContentTypes; 18 | } 19 | 20 | @Override 21 | public String getMessage() { 22 | return String.format("Unexpected content type '%s', expected any of %s", 23 | parsedContentType, 24 | expectedContentTypes); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/github/GithubCommands.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.github; 2 | 3 | import picocli.CommandLine.Command; 4 | 5 | @Command(name = "github", 6 | subcommands = { 7 | DownloadLatestAssetCommand.class 8 | } 9 | ) 10 | public class GithubCommands { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/github/model/Asset.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.github.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Data; 6 | 7 | @Data 8 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 9 | public class Asset { 10 | String name; 11 | String browserDownloadUrl; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/github/model/Release.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.github.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class Release { 11 | String name; 12 | List assets; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/ContentTypeValidator.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import org.apache.hc.core5.http.ClassicHttpResponse; 6 | import org.apache.hc.core5.http.ContentType; 7 | import org.apache.hc.core5.http.HttpHeaders; 8 | import org.apache.hc.core5.http.ProtocolException; 9 | 10 | public class ContentTypeValidator { 11 | final List expectedContentTypes; 12 | 13 | public ContentTypeValidator(List expectedContentTypes) { 14 | this.expectedContentTypes = expectedContentTypes; 15 | } 16 | 17 | public void validate(ClassicHttpResponse response) throws IOException { 18 | if (response.getCode() != 200) { 19 | return; 20 | } 21 | 22 | final String contentType; 23 | try { 24 | contentType = response.getHeader(HttpHeaders.CONTENT_TYPE).getValue(); 25 | } catch (ProtocolException e) { 26 | throw new IOException("Missing content type header", e); 27 | } 28 | final String parsedContentType = ContentType.parse(contentType).getMimeType(); 29 | 30 | if (expectedContentTypes.stream() 31 | .noneMatch(parsedContentType::equalsIgnoreCase)) { 32 | throw new IOException( 33 | String.format("Unexpected content type '%s', expected any of %s", parsedContentType, 34 | expectedContentTypes)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/DeriveFilenameHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.hc.core5.http.ClassicHttpResponse; 6 | import org.apache.hc.core5.http.HttpException; 7 | import org.apache.hc.core5.http.io.HttpClientResponseHandler; 8 | import org.apache.hc.core5.http.io.entity.EntityUtils; 9 | 10 | @Slf4j 11 | public class DeriveFilenameHandler implements HttpClientResponseHandler { 12 | 13 | final FilenameExtractor filenameExtractor; 14 | 15 | public DeriveFilenameHandler(LatchingUrisInterceptor interceptor) { 16 | filenameExtractor = new FilenameExtractor(interceptor); 17 | } 18 | 19 | @Override 20 | public String handleResponse(ClassicHttpResponse response) throws HttpException, IOException { 21 | final String filename = filenameExtractor.extract(response); 22 | 23 | EntityUtils.consume(response.getEntity()); 24 | 25 | return filename; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/FailedRequestException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import io.netty.handler.codec.http.HttpHeaders; 4 | import io.netty.handler.codec.http.HttpResponseStatus; 5 | import java.net.URI; 6 | import lombok.Getter; 7 | import lombok.ToString; 8 | 9 | @Getter @ToString 10 | public class FailedRequestException extends RuntimeException { 11 | 12 | private final URI uri; 13 | private final int statusCode; 14 | private final String body; 15 | private final HttpHeaders headers; 16 | 17 | /** 18 | * Reactor Netty flavor 19 | */ 20 | public FailedRequestException(HttpResponseStatus status, URI uri, String body, String msg, HttpHeaders headers) { 21 | super( 22 | String.format("HTTP request of %s failed with %s: %s", uri, status, msg) 23 | ); 24 | this.uri = uri; 25 | this.statusCode = status.code(); 26 | this.body = body; 27 | this.headers = headers; 28 | } 29 | 30 | @SuppressWarnings("unused") 31 | public static boolean isNotFound(Throwable throwable) { 32 | return isStatus(throwable, HttpResponseStatus.NOT_FOUND); 33 | } 34 | 35 | public static boolean isForbidden(Throwable throwable) { 36 | return isStatus(throwable, HttpResponseStatus.FORBIDDEN); 37 | } 38 | 39 | public static boolean isStatus(Throwable throwable, HttpResponseStatus... statuses) { 40 | if (throwable instanceof FailedRequestException) { 41 | final int actualStatus = ((FailedRequestException) throwable).getStatusCode(); 42 | for (final HttpResponseStatus status : statuses) { 43 | if (status.code() == actualStatus) { 44 | return true; 45 | } 46 | } 47 | } 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/Fetch.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import me.itzg.helpers.http.SharedFetch.Options; 5 | import org.slf4j.Logger; 6 | 7 | public class Fetch { 8 | 9 | /** 10 | * Perform a single fetch (web request) to the given URI/URL. 11 | */ 12 | public static FetchBuilderBase fetch(URI uri) { 13 | return new FetchBuilderBase<>(uri); 14 | } 15 | 16 | /** 17 | * Provides an efficient way to make multiple web requests since a single client 18 | * is shared. 19 | */ 20 | public static SharedFetch sharedFetch(String forCommand, Options options) { 21 | return new SharedFetch(forCommand, options); 22 | } 23 | 24 | private Fetch() { 25 | } 26 | 27 | public static FileDownloadStatusHandler loggingDownloadStatusHandler(Logger log) { 28 | return (status, uri, file) -> { 29 | switch (status) { 30 | case DOWNLOADING: 31 | log.debug("Downloading {}", file); 32 | break; 33 | case DOWNLOADED: 34 | log.info("Downloaded {}", file); 35 | break; 36 | case SKIP_FILE_UP_TO_DATE: 37 | log.info("The file {} is already up to date", file); 38 | break; 39 | } 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/FileDownloadStatus.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | public enum FileDownloadStatus { 4 | SKIP_FILE_EXISTS, 5 | SKIP_FILE_UP_TO_DATE, 6 | DOWNLOADING, 7 | DOWNLOADED 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/FileDownloadStatusHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Path; 5 | 6 | @FunctionalInterface 7 | public interface FileDownloadStatusHandler { 8 | 9 | void call(FileDownloadStatus status, URI uri, Path file); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/FileDownloadedHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Path; 5 | 6 | @FunctionalInterface 7 | public interface FileDownloadedHandler { 8 | 9 | void call(URI uri, Path file, long contentSizeBytes); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/FormFetchBuilder.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import reactor.netty.http.client.HttpClientForm; 5 | 6 | import java.util.function.Consumer; 7 | 8 | @Slf4j 9 | public class FormFetchBuilder extends FetchBuilderBase { 10 | private final Consumer formCallback; 11 | 12 | protected FormFetchBuilder(State state, Consumer formCallback) { 13 | super(state); 14 | this.formCallback = formCallback; 15 | } 16 | 17 | public ObjectFetchBuilder toObject(Class type) { 18 | return super.toObject(type, client -> client 19 | .headers(this::applyHeaders) 20 | .followRedirect(true) 21 | .doOnRequest(debugLogRequest(log, "form post")) 22 | .post() 23 | .uri(uri()) 24 | .sendForm((httpClientRequest, httpClientForm) -> 25 | formCallback.accept(httpClientForm) 26 | ) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/HttpClientException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter @ToString 8 | public class HttpClientException extends RuntimeException { 9 | 10 | private final HttpResponseStatus status; 11 | private final String body; 12 | 13 | public HttpClientException(HttpResponseStatus status, String body) { 14 | super("HttpClient request failed: " + status); 15 | this.status = status; 16 | this.body = body; 17 | } 18 | 19 | public static boolean isNotFound(Throwable throwable) { 20 | if (throwable instanceof HttpClientException) { 21 | return ((HttpClientException) throwable).isNotFound(); 22 | } 23 | return false; 24 | } 25 | 26 | @SuppressWarnings("unused") 27 | public boolean isNotFound() { 28 | return status.code() == 404; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/LatchingUrisInterceptor.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import java.util.Deque; 5 | import java.util.concurrent.ConcurrentLinkedDeque; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.apache.hc.client5.http.classic.ExecChain; 9 | import org.apache.hc.client5.http.classic.ExecChain.Scope; 10 | import org.apache.hc.client5.http.classic.ExecChainHandler; 11 | import org.apache.hc.client5.http.protocol.RedirectLocations; 12 | import org.apache.hc.core5.http.ClassicHttpRequest; 13 | import org.apache.hc.core5.http.ClassicHttpResponse; 14 | 15 | @Slf4j 16 | public class LatchingUrisInterceptor implements ExecChainHandler { 17 | 18 | private final Deque uris = new ConcurrentLinkedDeque<>(); 19 | 20 | @SneakyThrows 21 | @Override 22 | public ClassicHttpResponse execute(ClassicHttpRequest request, Scope scope, ExecChain chain) { 23 | log.debug("Intercepting request uri={}", request.getUri()); 24 | 25 | uris.push(request.getUri()); 26 | 27 | final ClassicHttpResponse resp = chain.proceed(request, scope); 28 | final RedirectLocations redirectLocations = scope.clientContext.getRedirectLocations(); 29 | if (redirectLocations != null) { 30 | log.debug("Post-request capturing redirectLocations={}", redirectLocations.getAll()); 31 | 32 | uris.addAll(redirectLocations.getAll()); 33 | } 34 | return resp; 35 | } 36 | 37 | public URI getLastRequestedUri() { 38 | return uris.peekLast(); 39 | } 40 | 41 | public void reset() { 42 | uris.clear(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/LoggingResponseHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler; 6 | import org.apache.hc.core5.http.ClassicHttpResponse; 7 | import org.slf4j.Logger; 8 | 9 | @Slf4j 10 | public abstract class LoggingResponseHandler extends AbstractHttpClientResponseHandler { 11 | 12 | static void logResponse(Logger log, ClassicHttpResponse response) { 13 | log.debug("Response: status={}, reason={}, headers={}", 14 | response.getCode(), response.getReasonPhrase(), response.getHeaders()); 15 | } 16 | 17 | @Override 18 | public T handleResponse(ClassicHttpResponse response) throws IOException { 19 | logResponse(log, response); 20 | return super.handleResponse(response); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/NotModifiedHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.hc.core5.http.ClassicHttpResponse; 7 | import org.apache.hc.core5.http.HttpException; 8 | import org.apache.hc.core5.http.HttpStatus; 9 | 10 | @Slf4j 11 | public class NotModifiedHandler implements OutputResponseHandler { 12 | private final Path file; 13 | private final OutputResponseHandler delegate; 14 | private final boolean logProgressEach; 15 | 16 | public NotModifiedHandler(Path file, OutputResponseHandler delegate, boolean logProgressEach) { 17 | this.file = file; 18 | this.delegate = delegate; 19 | this.logProgressEach = logProgressEach; 20 | } 21 | 22 | @Override 23 | public Path handleResponse(ClassicHttpResponse response) throws HttpException, IOException { 24 | LoggingResponseHandler.logResponse(log, response); 25 | 26 | if (response.getCode() == HttpStatus.SC_NOT_MODIFIED) { 27 | if (logProgressEach) { 28 | log.info("Skipping {} since it is already up to date", file); 29 | } else { 30 | log.debug("Skipping {} since it is already up to date", file); 31 | } 32 | return file; 33 | } 34 | return delegate.handleResponse(response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/ObjectListFetchBuilder.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import reactor.core.publisher.Mono; 5 | 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | public class ObjectListFetchBuilder extends FetchBuilderBase> { 10 | 11 | private final ObjectFetchBuilder delegate; 12 | 13 | ObjectListFetchBuilder(State state, Class type, ObjectMapper objectMapper) { 14 | super(state); 15 | 16 | delegate = new ObjectFetchBuilder<>(state, type, true, objectMapper); 17 | } 18 | 19 | public List execute() throws IOException { 20 | return delegate.assembleToList() 21 | .block(); 22 | } 23 | 24 | public Mono> assemble() { 25 | return delegate.assembleToList(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/OutputResponseHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import org.apache.hc.core5.http.io.HttpClientResponseHandler; 6 | 7 | public interface OutputResponseHandler extends HttpClientResponseHandler { 8 | 9 | default void setExpectedContentTypes(List contentTypes) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/OutputToDirectoryHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.List; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.hc.core5.http.ClassicHttpResponse; 10 | import org.apache.hc.core5.http.HttpException; 11 | 12 | @Slf4j 13 | public class OutputToDirectoryHandler implements OutputResponseHandler { 14 | 15 | private final Path directory; 16 | final FilenameExtractor filenameExtractor; 17 | private final boolean logProgressEach; 18 | private ContentTypeValidator contentTypeValidator; 19 | 20 | public OutputToDirectoryHandler(Path directory, 21 | LatchingUrisInterceptor interceptor, boolean logProgressEach) { 22 | this.directory = directory; 23 | filenameExtractor = new FilenameExtractor(interceptor); 24 | this.logProgressEach = logProgressEach; 25 | } 26 | 27 | @Override 28 | public void setExpectedContentTypes(List contentTypes) { 29 | this.contentTypeValidator = new ContentTypeValidator(contentTypes); 30 | } 31 | 32 | @Override 33 | public Path handleResponse(ClassicHttpResponse response) throws HttpException, IOException { 34 | LoggingResponseHandler.logResponse(log, response); 35 | if (contentTypeValidator != null) { 36 | contentTypeValidator.validate(response); 37 | } 38 | 39 | final String filename = filenameExtractor.extract(response); 40 | 41 | final Path filePath = directory.resolve(filename); 42 | 43 | if (logProgressEach) { 44 | log.info("Downloaded {}", filePath); 45 | } 46 | else { 47 | log.debug("Writing response content to path={}", filePath); 48 | } 49 | try (OutputStream out = Files.newOutputStream(filePath)) { 50 | response.getEntity().writeTo(out); 51 | } 52 | 53 | return filePath; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/PathOrUri.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Path; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | @ToString 9 | public class PathOrUri { 10 | @Getter 11 | final Path path; 12 | @Getter 13 | final URI uri; 14 | 15 | public static PathOrUri path(Path path) { 16 | return new PathOrUri(path, null); 17 | } 18 | 19 | public static PathOrUri uri(URI uri) { 20 | return new PathOrUri(null, uri); 21 | } 22 | 23 | protected PathOrUri(Path path, URI uri) { 24 | this.path = path; 25 | this.uri = uri; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/PathOrUriConverter.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Paths; 5 | import java.util.regex.Pattern; 6 | import picocli.CommandLine.ITypeConverter; 7 | 8 | public class PathOrUriConverter implements ITypeConverter { 9 | 10 | private static final Pattern URI_PATTERN = Pattern.compile("http(s)?://.*"); 11 | 12 | @Override 13 | public PathOrUri convert(String value) throws Exception { 14 | if (URI_PATTERN.matcher(value).matches()) { 15 | return PathOrUri.uri(URI.create(value)); 16 | } 17 | else { 18 | return PathOrUri.path(Paths.get(value)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/RequestAssembler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import reactor.netty.http.client.HttpClient; 4 | 5 | @FunctionalInterface 6 | public interface RequestAssembler { 7 | HttpClient.ResponseReceiver assembleRequest(HttpClient client); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/RequestResponseAssembler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface RequestResponseAssembler { 6 | Mono assemble(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/ResponseParsingException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.io.IOException; 4 | 5 | public class ResponseParsingException extends IOException { 6 | 7 | public ResponseParsingException(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/StringFetchBuilder.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import reactor.core.publisher.Mono; 5 | import reactor.netty.ByteBufMono; 6 | import reactor.netty.http.client.HttpClientResponse; 7 | 8 | @Slf4j 9 | public class StringFetchBuilder extends FetchBuilderBase { 10 | public StringFetchBuilder(State state) { 11 | super(state); 12 | } 13 | 14 | public Mono assemble() { 15 | return useReactiveClient(client -> 16 | client 17 | .headers(this::applyHeaders) 18 | .followRedirect(true) 19 | .doOnRequest(debugLogRequest(log, "json fetch")) 20 | .get() 21 | .uri(uri()) 22 | .responseSingle(this::handleResponse) 23 | ); 24 | } 25 | 26 | private Mono handleResponse(HttpClientResponse resp, ByteBufMono byteBufMono) { 27 | return byteBufMono.asString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/UriBuilder.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import java.net.URI; 4 | import lombok.Getter; 5 | 6 | public class UriBuilder { 7 | 8 | @Getter 9 | private final String baseUrl; 10 | 11 | protected UriBuilder(String baseUrl) { 12 | this.baseUrl = baseUrl; 13 | } 14 | 15 | public static UriBuilder withBaseUrl(String baseUrl) { 16 | return new UriBuilder(baseUrl); 17 | } 18 | 19 | @SuppressWarnings("unused") 20 | public static UriBuilder withNoBaseUrl() { 21 | return new UriBuilder(""); 22 | } 23 | 24 | public URI resolve(String path, Object... values) { 25 | return Uris.populateToUri(baseUrl + path, values); 26 | } 27 | 28 | public URI resolve(String path, Uris.QueryParameters queryParameters, Object... values) { 29 | return Uris.populateToUri(baseUrl + path, queryParameters, values); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/http/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package provides wrappers of Reactor Netty 3 | * to simplify common retrieval patterns such as 4 | *
    5 | *
  • parsing response JSON into Java objects
  • 6 | *
  • retrieving a file into a known output name with handling of up to date checks
  • 7 | *
  • retrieving a file without prior knowledge of the resulting filename
  • 8 | *
9 | * Examples: 10 | *
{@code
11 |  * Fetch.fetch(URI.create(...))
12 |  *   .toDirectory(dest)
13 |  *   .skipUpToDate(true)
14 |  *   .assemble()
15 |  *   .block()
16 |  * }
17 | */ 18 | package me.itzg.helpers.http; -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/json/ObjectMappers.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.json; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | 8 | public class ObjectMappers { 9 | private static final ObjectMapper DEFAULT = new ObjectMapper() 10 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 11 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 12 | .registerModule(new JavaTimeModule()); 13 | 14 | public static ObjectMapper defaultMapper() { 15 | return DEFAULT; 16 | } 17 | 18 | private ObjectMappers() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/logger/CustomHighlight.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.logger; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import ch.qos.logback.core.pattern.color.ANSIConstants; 6 | import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; 7 | 8 | public class CustomHighlight extends ForegroundCompositeConverterBase { 9 | 10 | @Override 11 | protected String getForegroundColorCode(ILoggingEvent event) { 12 | Level level = event.getLevel(); 13 | switch (level.toInt()) { 14 | case Level.ERROR_INT: 15 | return ANSIConstants.BOLD + ANSIConstants.RED_FG; // same as default color scheme 16 | case Level.WARN_INT: 17 | return ANSIConstants.YELLOW_FG;// same as default color scheme 18 | case Level.INFO_INT: 19 | default: 20 | return ANSIConstants.DEFAULT_FG; 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/lombok.config: -------------------------------------------------------------------------------- 1 | config.stopbubbling=true 2 | lombok.accessors.chain=true -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ExcludeIncludesContent.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 4 | import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import lombok.Data; 8 | 9 | /** 10 | * Similar to {@link me.itzg.helpers.curseforge.ExcludeIncludesContent}, but trimmed down to match 11 | * supported functionality 12 | */ 13 | @JsonSchemaTitle("Mods Exclude File Content") 14 | @Data 15 | public class ExcludeIncludesContent { 16 | 17 | @JsonPropertyDescription("Mods/files by slug|id to exclude for all modpacks") 18 | private Set globalExcludes; 19 | @JsonPropertyDescription("Mods by slug|id to force include for all modpacks") 20 | private Set globalForceIncludes; 21 | 22 | @JsonPropertyDescription("Specific exclude/includes by modpack slug") 23 | private Map modpacks; 24 | 25 | @Data 26 | public static class ExcludeIncludes { 27 | @JsonPropertyDescription("Mods by slug|id to exclude for this modpack") 28 | private Set excludes; 29 | @JsonPropertyDescription("Mods by slug|id to force include for this modpack") 30 | private Set forceIncludes; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/FetchedPack.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.nio.file.Path; 4 | import lombok.Value; 5 | 6 | @Value 7 | public class FetchedPack { 8 | Path mrPackFile; 9 | 10 | String projectSlug; 11 | 12 | String versionId; 13 | 14 | /** 15 | * Human-readable version 16 | */ 17 | String versionNumber; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/FilePackFetcher.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.nio.file.attribute.FileTime; 8 | import lombok.extern.slf4j.Slf4j; 9 | import me.itzg.helpers.errors.InvalidParameterException; 10 | import reactor.core.publisher.Mono; 11 | 12 | @Slf4j 13 | public class FilePackFetcher implements ModrinthPackFetcher { 14 | 15 | private final ProjectRef projectRef; 16 | 17 | public FilePackFetcher(ProjectRef projectRef) { 18 | if (!projectRef.isFileUri()) { 19 | throw new IllegalArgumentException("Requires a projectRef with a file URI"); 20 | } 21 | this.projectRef = projectRef; 22 | } 23 | 24 | @Override 25 | public Mono fetchModpack(ModrinthModpackManifest prevManifest) { 26 | final Path file = Paths.get(projectRef.getProjectUri()); 27 | if (!Files.exists(file)) { 28 | throw new InvalidParameterException("Local modpack file does not exist: " + file); 29 | } 30 | 31 | return Mono.just( 32 | new FetchedPack(file, projectRef.getIdOrSlug(), deriveVersionId(), "local") 33 | ); 34 | } 35 | 36 | private String deriveVersionId() { 37 | final Path file = Paths.get(projectRef.getProjectUri()); 38 | 39 | try { 40 | final FileTime fileTime = Files.getLastModifiedTime(file); 41 | return fileTime.toString(); 42 | } catch (IOException e) { 43 | log.warn("Unable to retrieve modified file time", e); 44 | return "unknown"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/Installation.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import lombok.Data; 6 | import me.itzg.helpers.modrinth.model.ModpackIndex; 7 | 8 | @Data 9 | public class Installation { 10 | 11 | ModpackIndex index; 12 | List files; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/LegacyModrinthManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.time.Instant; 4 | import java.util.Set; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.extern.jackson.Jacksonized; 8 | 9 | @Data 10 | @Builder 11 | @Jacksonized 12 | public class LegacyModrinthManifest { 13 | 14 | public static final String FILENAME = ".modrinth-files.manifest"; 15 | 16 | Instant timestamp; 17 | 18 | Set files; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/Loader.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum Loader { 7 | fabric("mods", null), 8 | quilt("mods", fabric), 9 | forge("mods", null), 10 | neoforge("mods", forge), 11 | bukkit("plugins", null), 12 | spigot("plugins", null), 13 | paper("plugins", spigot), 14 | pufferfish("plugins", paper), 15 | leaf("plugins", paper), 16 | purpur("plugins", paper), 17 | bungeecord("plugins", null), 18 | velocity("plugins", null), 19 | datapack(null, null); 20 | 21 | private final String type; 22 | private final Loader compatibleWith; 23 | 24 | Loader(String type, Loader compatibleWith) { 25 | this.type = type; 26 | this.compatibleWith = compatibleWith; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ModpackLoader.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | /** 4 | * Valid loader values for modpacks 5 | */ 6 | public enum ModpackLoader { 7 | fabric, 8 | forge, 9 | quilt, 10 | neoforge; 11 | 12 | public Loader asLoader() { 13 | return Loader.valueOf(this.name()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcher.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.net.URI; 4 | import java.nio.charset.StandardCharsets; 5 | import java.nio.file.Path; 6 | import java.util.Base64; 7 | import lombok.extern.slf4j.Slf4j; 8 | import reactor.core.publisher.Mono; 9 | 10 | @Slf4j 11 | public class ModrinthHttpPackFetcher implements ModrinthPackFetcher { 12 | private final ModrinthApiClient apiClient; 13 | private final Path destFilePath; 14 | private final URI modpackUri; 15 | 16 | ModrinthHttpPackFetcher(ModrinthApiClient apiClient, Path basePath, URI uri) { 17 | this.apiClient = apiClient; 18 | this.destFilePath = basePath.resolve("modpack.mrpack"); 19 | this.modpackUri = uri; 20 | } 21 | 22 | @Override 23 | public Mono fetchModpack(ModrinthModpackManifest prevManifest) { 24 | return apiClient.downloadFileFromUrl( 25 | destFilePath, modpackUri, 26 | (uri, file, contentSizeBytes) -> 27 | log.info("Downloaded {}", destFilePath) 28 | ) 29 | .map(mrPackFile -> new FetchedPack(mrPackFile, "custom", deriveVersionId(), deriveVersionName())); 30 | } 31 | 32 | private String deriveVersionName() { 33 | final int lastSlash = modpackUri.getPath().lastIndexOf('/'); 34 | return lastSlash > 0 ? modpackUri.getPath().substring(lastSlash + 1) 35 | : "unknown"; 36 | } 37 | 38 | private String deriveVersionId() { 39 | return Base64.getUrlEncoder().encodeToString(modpackUri.toString().getBytes(StandardCharsets.UTF_8)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ModrinthManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.util.List; 4 | import lombok.Getter; 5 | import lombok.experimental.SuperBuilder; 6 | import lombok.extern.jackson.Jacksonized; 7 | import me.itzg.helpers.files.BaseManifest; 8 | 9 | @SuperBuilder 10 | @Getter 11 | @Jacksonized 12 | public class ModrinthManifest extends BaseManifest { 13 | 14 | public static final String ID = "modrinth"; 15 | 16 | List projects; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ModrinthModpackManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.util.Map; 4 | import lombok.Getter; 5 | import lombok.experimental.SuperBuilder; 6 | import lombok.extern.jackson.Jacksonized; 7 | import me.itzg.helpers.files.BaseManifest; 8 | import me.itzg.helpers.modrinth.model.DependencyId; 9 | 10 | @SuperBuilder 11 | @Getter 12 | @Jacksonized 13 | public class ModrinthModpackManifest extends BaseManifest { 14 | public static final String ID = "modrinth-modpack"; 15 | 16 | private String projectSlug; 17 | private String versionId; 18 | 19 | private Map dependencies; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ModrinthPackFetcher.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface ModrinthPackFetcher { 6 | 7 | /** 8 | * @return the fetched modpack or empty if the requested modpack was already up-to-date 9 | */ 10 | Mono fetchModpack(ModrinthModpackManifest prevManifest); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/NoApplicableVersionsException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | import me.itzg.helpers.modrinth.model.Project; 8 | import me.itzg.helpers.modrinth.model.Version; 9 | import me.itzg.helpers.modrinth.model.VersionType; 10 | 11 | @Getter 12 | @ToString 13 | public class NoApplicableVersionsException extends RuntimeException { 14 | 15 | private final Project project; 16 | private final List versions; 17 | private final VersionType versionType; 18 | 19 | public NoApplicableVersionsException(Project project, List versions, VersionType versionType) { 20 | super( 21 | String.format("No candidate versions of '%s' [%s] matched versionType=%s", 22 | project.getTitle(), 23 | versions.stream().map(version -> version.getVersionNumber() + "=" + version.getVersionType()) 24 | .collect(Collectors.joining(", ")), 25 | versionType 26 | ) 27 | ); 28 | 29 | this.project = project; 30 | this.versions = versions; 31 | this.versionType = versionType; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/NoFilesAvailableException.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import lombok.Getter; 4 | import lombok.ToString; 5 | import me.itzg.helpers.modrinth.model.Project; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | @Getter 9 | @ToString 10 | public class NoFilesAvailableException extends RuntimeException { 11 | 12 | private final Project project; 13 | private final Loader loader; 14 | private final String gameVersion; 15 | 16 | public NoFilesAvailableException(Project project, @Nullable Loader loader, String gameVersion) { 17 | super(String.format("No files are available for the project '%s' for loader %s and Minecraft version %s", 18 | project.getTitle(), loader, gameVersion 19 | )); 20 | 21 | this.project = project; 22 | this.loader = loader; 23 | this.gameVersion = gameVersion; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/ResolvedProject.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | import me.itzg.helpers.modrinth.model.Project; 6 | 7 | @RequiredArgsConstructor 8 | @Data 9 | public class ResolvedProject { 10 | final private ProjectRef projectRef; 11 | final private Project project; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/Constants.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public class Constants { 4 | 5 | public static final String LOADER_DATAPACK = "datapack"; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/DependencyId.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum DependencyId { 6 | minecraft, 7 | forge, 8 | @JsonProperty("fabric-loader") 9 | fabricLoader, 10 | @JsonProperty("quilt-loader") 11 | quiltLoader, 12 | neoforge 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/DependencyType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum DependencyType { 4 | required, 5 | optional, 6 | incompatible, 7 | embedded 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/Env.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum Env { 4 | client, 5 | server 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/EnvType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum EnvType { 4 | required, 5 | optional, 6 | unsupported 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/ModpackIndex.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.net.URI; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * See spec 11 | */ 12 | @Data 13 | public class ModpackIndex { 14 | int formatVersion; 15 | String game; 16 | String versionId; 17 | String name; 18 | String summary; 19 | List files; 20 | Map dependencies; 21 | 22 | @Data 23 | public static class ModpackFile { 24 | String path; 25 | Map hashes; 26 | Map env; 27 | List downloads; 28 | long fileSize; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/Project.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Data; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Spec 11 | */ 12 | @Data 13 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 14 | public class Project { 15 | String slug; 16 | 17 | String id; 18 | 19 | String title; 20 | 21 | ProjectType projectType; 22 | 23 | ServerSide serverSide; 24 | 25 | List versions; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/ProjectType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum ProjectType { 4 | mod, 5 | modpack, 6 | resourcepack, 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/ServerSide.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum ServerSide { 4 | required, 5 | optional, 6 | unsupported, 7 | /** 8 | * Not a documented value, but dynmap project was responding with this. 9 | */ 10 | unknown 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/Version.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.time.Instant; 6 | import java.util.List; 7 | import lombok.Data; 8 | 9 | @Data 10 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 11 | public class Version { 12 | 13 | private String id; 14 | 15 | private String projectId; 16 | 17 | private String name; 18 | 19 | private Instant datePublished; 20 | 21 | private String versionNumber; 22 | 23 | private VersionType versionType; 24 | 25 | private List files; 26 | 27 | private List dependencies; 28 | 29 | private List gameVersions; 30 | 31 | private List loaders; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/VersionDependency.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class VersionDependency { 8 | @JsonProperty("version_id") 9 | String versionId; 10 | 11 | @JsonProperty("project_id") 12 | String projectId; 13 | 14 | @JsonProperty("dependency_type") 15 | DependencyType dependencyType; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/VersionFile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | import java.util.Map; 4 | import lombok.Data; 5 | 6 | /** 7 | * Refer to files of getversion 8 | */ 9 | @Data 10 | public class VersionFile { 11 | 12 | /** 13 | * key is either sha512 or sha1 14 | */ 15 | Map hashes; 16 | 17 | String url; 18 | 19 | String filename; 20 | 21 | boolean primary; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/modrinth/model/VersionType.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth.model; 2 | 3 | public enum VersionType { 4 | release, 5 | beta, 6 | alpha; 7 | 8 | public boolean sufficientFor(VersionType levelRequired) { 9 | return this.ordinal() <= levelRequired.ordinal(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/mvn/MavenMetadata.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.mvn; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class MavenMetadata { 10 | 11 | String groupId; 12 | String artifactId; 13 | Versioning versioning; 14 | 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | @Data 17 | public static class Versioning { 18 | String latest; 19 | String release; 20 | @JacksonXmlElementWrapper(localName = "versions") 21 | List version; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/PaperManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper; 2 | 3 | import java.net.URI; 4 | import lombok.Getter; 5 | import lombok.experimental.SuperBuilder; 6 | import lombok.extern.jackson.Jacksonized; 7 | import me.itzg.helpers.files.BaseManifest; 8 | 9 | @Getter 10 | @SuperBuilder 11 | @Jacksonized 12 | public class PaperManifest extends BaseManifest { 13 | 14 | public static final String ID = "papermc"; 15 | 16 | String minecraftVersion; 17 | 18 | String project; 19 | 20 | int build; 21 | 22 | URI customDownloadUrl; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/BuildInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.Map; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class BuildInfo { 11 | int build; 12 | ReleaseChannel channel; 13 | Map downloads; 14 | 15 | @Data 16 | public static class DownloadInfo { 17 | String name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/ProjectInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class ProjectInfo { 11 | List versions; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/ReleaseChannel.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum ReleaseChannel { 6 | @JsonProperty("default") 7 | DEFAULT, 8 | @JsonProperty("experimental") 9 | EXPERIMENTAL; 10 | 11 | @Override 12 | public String toString() { 13 | return name().toLowerCase(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/VersionBuilds.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | /** 9 | * Response from version-builds-controller 10 | */ 11 | @Data 12 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 13 | public class VersionBuilds { 14 | String version; 15 | List builds; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/VersionInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class VersionInfo { 11 | List builds; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/paper/model/VersionMeta.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.paper.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Data; 6 | 7 | /** 8 | * Represents version.json content from server jar 9 | */ 10 | @Data 11 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 12 | public class VersionMeta { 13 | String id; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/FileFormat.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | 7 | public interface FileFormat { 8 | TypeReference> MAP_TYPE 9 | = new TypeReference>() {}; 10 | 11 | /** 12 | * @return supported file suffixes, without leading dot 13 | */ 14 | String[] getFileSuffixes(); 15 | 16 | String getName(); 17 | 18 | Map decode(String content) throws IOException; 19 | 20 | String encode(Map content) throws IOException; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/ObjectMapperFileFormat.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | 7 | public class ObjectMapperFileFormat implements FileFormat { 8 | 9 | private final ObjectMapper objectMapper; 10 | private final String name; 11 | private final String[] fileSuffixes; 12 | 13 | protected ObjectMapperFileFormat(ObjectMapper objectMapper, String name, String... fileSuffixes) { 14 | this.objectMapper = objectMapper; 15 | this.name = name; 16 | this.fileSuffixes = fileSuffixes; 17 | } 18 | 19 | @Override 20 | public String[] getFileSuffixes() { 21 | return fileSuffixes; 22 | } 23 | 24 | @Override 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | @Override 30 | public Map decode(String content) throws IOException { 31 | return objectMapper.readValue(content, MAP_TYPE); 32 | } 33 | 34 | @Override 35 | public String encode(Map content) throws IOException { 36 | return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(content); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/TomlFileFormat.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch; 2 | 3 | import com.fasterxml.jackson.dataformat.toml.TomlMapper; 4 | 5 | public class TomlFileFormat extends ObjectMapperFileFormat { 6 | 7 | public TomlFileFormat() { 8 | super(new TomlMapper(), "toml", "toml"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/YamlFileFormat.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch; 2 | 3 | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; 5 | 6 | public class YamlFileFormat extends ObjectMapperFileFormat { 7 | 8 | public YamlFileFormat() { 9 | super(new YAMLMapper() 10 | .configure(YAMLGenerator.Feature.WRITE_DOC_START_MARKER, false) 11 | .configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true), 12 | "yaml", 13 | "yaml", "yml" 14 | ); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchAddOperation.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | 10 | @EqualsAndHashCode(callSuper = true) 11 | @Data 12 | public class PatchAddOperation extends PatchOperation { 13 | @JsonProperty(required = true) 14 | @JsonPropertyDescription("The JSON path to the array to add to.") 15 | String path; 16 | 17 | @JsonProperty(required = true) 18 | @JsonPropertyDescription(VALUE_DESCRIPTION) 19 | JsonNode value; 20 | 21 | @JsonProperty("value-type") 22 | @JsonPropertyDescription(VALUE_TYPE_DESCRIPTION) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | String valueType; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchDefinition.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 6 | import java.util.List; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class PatchDefinition { 11 | @JsonProperty(required = true) 12 | @JsonPropertyDescription("Path to the file to patch") 13 | String file; 14 | 15 | @JsonPropertyDescription("If non-null, declares a specifically supported format name: json, yaml. " + 16 | "Otherwise, the file format is detected by the file's suffix.") 17 | @JsonProperty("file-format") 18 | @JsonInclude(JsonInclude.Include.NON_NULL) 19 | String fileFormat; 20 | 21 | @JsonProperty(required = true) 22 | List ops; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchOperation.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | 6 | @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) 7 | @JsonSubTypes({ 8 | @JsonSubTypes.Type(value = PatchSetOperation.class, name = "$set"), 9 | @JsonSubTypes.Type(value = PatchPutOperation.class, name = "$put"), 10 | @JsonSubTypes.Type(value = PatchAddOperation.class, name = "$add"), 11 | }) 12 | public abstract class PatchOperation { 13 | protected static final String VALUE_DESCRIPTION = "The value to set." + 14 | " If the given value is a string, variable placeholders of the form ${...} will be replaced" + 15 | " and the resulting string can be converted by setting value-type."; 16 | protected static final String VALUE_TYPE_DESCRIPTION = "Used to convert string values into other value types:" + 17 | " int, float, bool, auto," + 18 | " list of int, list of float, list of bool, list of auto"; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchPutOperation.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | 10 | @EqualsAndHashCode(callSuper = true) 11 | @Data 12 | public class PatchPutOperation extends PatchOperation { 13 | @JsonProperty(required = true) 14 | @JsonPropertyDescription("The JSON path to the object containing key to set") 15 | String path; 16 | 17 | @JsonProperty(required = true) 18 | @JsonPropertyDescription("The key to set") 19 | String key; 20 | 21 | /** 22 | * The value to set, which may contain embedded variable placeholders with the syntax ${...} 23 | */ 24 | @JsonProperty(required = true) 25 | @JsonPropertyDescription(VALUE_DESCRIPTION) 26 | JsonNode value; 27 | 28 | @JsonPropertyDescription(VALUE_TYPE_DESCRIPTION) 29 | @JsonProperty("value-type") 30 | @JsonInclude(JsonInclude.Include.NON_NULL) 31 | String valueType; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchSet.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class PatchSet { 9 | List patches; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/patch/model/PatchSetOperation.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.patch.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyDescription; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | 10 | @EqualsAndHashCode(callSuper = true) 11 | @Data 12 | public class PatchSetOperation extends PatchOperation { 13 | @JsonProperty(required = true) 14 | @JsonPropertyDescription("The JSON path to the field to set.") 15 | String path; 16 | 17 | @JsonProperty(required = true) 18 | @JsonPropertyDescription(VALUE_DESCRIPTION) 19 | JsonNode value; 20 | 21 | @JsonProperty("value-type") 22 | @JsonPropertyDescription(VALUE_TYPE_DESCRIPTION) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | String valueType; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/properties/PropertyDefinition.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.properties; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class PropertyDefinition { 9 | String env; 10 | List allowed; 11 | Map mappings; 12 | boolean remove; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/purpur/PurpurManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.purpur; 2 | 3 | import java.net.URI; 4 | import lombok.Getter; 5 | import lombok.experimental.SuperBuilder; 6 | import lombok.extern.jackson.Jacksonized; 7 | import me.itzg.helpers.files.BaseManifest; 8 | 9 | @Getter 10 | @SuperBuilder 11 | @Jacksonized 12 | public class PurpurManifest extends BaseManifest { 13 | 14 | public static final String ID = "purpur"; 15 | 16 | String minecraftVersion; 17 | 18 | String build; 19 | 20 | URI customDownloadUrl; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/purpur/model/ProjectInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.purpur.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class ProjectInfo { 11 | List versions; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/purpur/model/VersionInfo.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.purpur.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import java.util.List; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class VersionInfo { 11 | private Builds builds; 12 | 13 | @Data 14 | public static class Builds { 15 | private String latest; 16 | private List all; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/purpur/model/VersionMeta.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.purpur.model; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Data; 6 | 7 | /** 8 | * Represents version.json content from server jar 9 | */ 10 | @Data 11 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 12 | public class VersionMeta { 13 | String id; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/quilt/QuiltManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.quilt; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | @Getter 9 | @SuperBuilder 10 | @Jacksonized 11 | public class QuiltManifest extends BaseManifest { 12 | public static final String ID = "quilt"; 13 | 14 | String minecraftVersion; 15 | 16 | String loaderVersion; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/singles/Asciify.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.concurrent.Callable; 6 | import picocli.CommandLine.Command; 7 | 8 | @Command(name = "asciify", 9 | description = "Converts UTF-8 on stdin to ASCII by escaping Unicode characters") 10 | public class Asciify implements Callable { 11 | 12 | private static final int MAX_ASCII = 0x7f; 13 | 14 | @Override 15 | public Integer call() throws Exception { 16 | // buffer will auto-grow if there happens to be more input 17 | final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024); 18 | final byte[] readBuffer = new byte[1024]; 19 | int len; 20 | while ((len = System.in.read(readBuffer)) >= 0) { 21 | if (len > 0) { 22 | buffer.write(readBuffer, 0, len); 23 | } 24 | } 25 | 26 | final String asUtf8 = new String(buffer.toByteArray(), StandardCharsets.UTF_8); 27 | 28 | for (int i = 0; i < asUtf8.length(); ++i) { 29 | final char c = asUtf8.charAt(i); 30 | if (c > MAX_ASCII) { 31 | //noinspection RedundantStringFormatCall 32 | System.out.print(String.format("\\u%04x", (int) c)); 33 | } else { 34 | System.out.print(c); 35 | } 36 | } 37 | System.out.flush(); 38 | 39 | return 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/singles/HashCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import java.security.MessageDigest; 4 | import java.util.concurrent.Callable; 5 | import org.apache.commons.codec.binary.Hex; 6 | import picocli.CommandLine.Command; 7 | import picocli.CommandLine.ExitCode; 8 | 9 | @Command(name = "hash", 10 | description = "Outputs an MD5 hash of the standard input" 11 | ) 12 | public class HashCommand implements Callable { 13 | 14 | @Override 15 | public Integer call() throws Exception { 16 | final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 17 | 18 | final byte[] buffer = new byte[1000]; 19 | while (true) { 20 | final int len = System.in.read(buffer); 21 | if (len < 0) { 22 | break; 23 | } 24 | messageDigest.update(buffer, 0, len); 25 | } 26 | 27 | System.out.println(Hex.encodeHexString(messageDigest.digest())); 28 | 29 | return ExitCode.OK; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/singles/MoreCollections.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.Objects; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public class MoreCollections { 11 | 12 | private MoreCollections() { 13 | } 14 | 15 | @SuppressWarnings("unused") 16 | public static Set makeNonNull(Set in) { 17 | return in != null ? in : Collections.emptySet(); 18 | } 19 | 20 | @SuppressWarnings("unused") 21 | public static Set combine(Set in1, Set in2) { 22 | return Stream.of(in1, in2) 23 | .filter(Objects::nonNull) 24 | .flatMap(Collection::stream) 25 | .collect(Collectors.toSet()); 26 | } 27 | 28 | /** 29 | * @return stream from collection if non-null, otherwise an empty stream 30 | */ 31 | public static Stream safeStreamFrom(Collection collection) { 32 | return collection != null ? collection.stream() : Stream.empty(); 33 | } 34 | public static Stream safeStreamFrom(Set set) { 35 | return set != null ? set.stream() : Stream.empty(); 36 | } 37 | 38 | public static boolean equalsIgnoreOrder(Collection lhs, Collection rhs) { 39 | if (lhs == null && rhs == null) { 40 | return true; 41 | } 42 | if (lhs == null || rhs == null) { 43 | return false; 44 | } 45 | return lhs.size() == rhs.size() 46 | && lhs.containsAll(rhs); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/singles/NetworkInterfacesCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import java.net.NetworkInterface; 4 | import java.net.SocketException; 5 | import java.util.Enumeration; 6 | import java.util.concurrent.Callable; 7 | import picocli.CommandLine.Command; 8 | import picocli.CommandLine.ExitCode; 9 | import picocli.CommandLine.Option; 10 | 11 | @Command(name = "network-interfaces", 12 | description = "Provides simple operations to list network interface names and check existence" 13 | ) 14 | public class NetworkInterfacesCommand implements Callable { 15 | 16 | @Option(names = "--check") 17 | String ifNameToCheck; 18 | 19 | @Option(names = "--include-loopback") 20 | boolean includeLoopback; 21 | 22 | @Override 23 | public Integer call() throws Exception { 24 | if (ifNameToCheck != null) { 25 | final NetworkInterface nic = NetworkInterface.getByName(ifNameToCheck); 26 | return allowedNic(nic) ? ExitCode.OK 27 | : ExitCode.SOFTWARE; 28 | } 29 | 30 | final Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); 31 | while (interfaces.hasMoreElements()) { 32 | final NetworkInterface nic = interfaces.nextElement(); 33 | if (allowedNic(nic)) { 34 | System.out.println(nic.getName()); 35 | } 36 | } 37 | 38 | return ExitCode.OK; 39 | } 40 | 41 | private boolean allowedNic(NetworkInterface nic) throws SocketException { 42 | return nic != null 43 | && (includeLoopback || !nic.isLoopback()) 44 | && nic.isUp(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/singles/TestLoggingCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import picocli.CommandLine.Command; 5 | import picocli.CommandLine.ExitCode; 6 | 7 | import java.util.concurrent.Callable; 8 | 9 | @Command(name = "test-logging-levels") 10 | @Slf4j 11 | public class TestLoggingCommand implements Callable { 12 | 13 | @Override 14 | public Integer call() throws Exception { 15 | log.error("This is an error"); 16 | log.warn("This is a warning"); 17 | log.info("This is an info"); 18 | log.debug("This is a debug"); 19 | return ExitCode.OK; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/sync/CopyingFileProcessor.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.sync; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | 9 | import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; 10 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 11 | 12 | @Slf4j 13 | public class CopyingFileProcessor implements FileProcessor { 14 | @Override 15 | public void processFile(Path srcFile, Path destFile) throws IOException { 16 | log.info("Copying {} -> {}", srcFile, destFile); 17 | 18 | Files.copy(srcFile, destFile, COPY_ATTRIBUTES, REPLACE_EXISTING); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/sync/FileProcessor.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.sync; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | 6 | @FunctionalInterface 7 | public interface FileProcessor { 8 | void processFile(Path srcFile, Path destFile) throws IOException; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/sync/MultiCopyManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.sync; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | @SuperBuilder 9 | @Getter 10 | @Jacksonized 11 | public class MultiCopyManifest extends BaseManifest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/sync/Sync.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.sync; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | import lombok.ToString; 7 | import lombok.extern.slf4j.Slf4j; 8 | import me.itzg.helpers.McImageHelper; 9 | import picocli.CommandLine; 10 | import picocli.CommandLine.Command; 11 | import picocli.CommandLine.Option; 12 | import picocli.CommandLine.Parameters; 13 | 14 | @Command(name = "sync", 15 | description = "Synchronizes the contents of one directory to another.") 16 | @ToString 17 | @Slf4j 18 | public class Sync implements Callable { 19 | @Option(names = {"-h", "--help"}, usageHelp = true, description = "Show this usage and exit") 20 | @ToString.Exclude 21 | boolean showHelp; 22 | 23 | @Option(names = "--skip-newer-in-destination", 24 | // this is same as rsync's --update option 25 | description = "Skip any files that exist in the destination and have a newer modification time than the source.") 26 | boolean skipNewerInDestination; 27 | 28 | /** 29 | * Allows for this to be command-line "compatible" with sync-and-interpolate subcommand. 30 | */ 31 | @CommandLine.Unmatched 32 | @ToString.Exclude 33 | List extra; 34 | 35 | @Parameters(arity = "2..*", description = "src... dest directories", 36 | split = McImageHelper.SPLIT_COMMA_NL, splitSynopsisLabel = McImageHelper.SPLIT_SYNOPSIS_COMMA_NL) 37 | List srcDest; 38 | 39 | @Override 40 | public Integer call() throws Exception { 41 | log.debug("Configured with {}", this); 42 | 43 | return SynchronizingFileVisitor.walkDirectories(srcDest, skipNewerInDestination, new CopyingFileProcessor()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/ExistingFileBehavior.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users; 2 | 3 | public enum ExistingFileBehavior { 4 | SYNCHRONIZE, 5 | MERGE, 6 | SKIP 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/Type.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users; 2 | 3 | public enum Type { 4 | JAVA_WHITELIST, 5 | JAVA_OPS 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/UserApi.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users; 2 | 3 | import me.itzg.helpers.users.model.JavaUser; 4 | 5 | public interface UserApi { 6 | 7 | JavaUser resolveUser(String input); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/UserApiProvider.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users; 2 | 3 | public enum UserApiProvider { 4 | mojang, 5 | playerdb 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/ext/MojangProfile.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users.ext; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class MojangProfile { 7 | String id; 8 | String name; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/ext/PlayerdbPlayer.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users.ext; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class PlayerdbPlayer { 7 | /** 8 | * Mojang raw ID, which is a UUID without dashes 9 | */ 10 | private String rawId; 11 | private String id; 12 | private String username; 13 | } -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/ext/PlayerdbResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users.ext; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class PlayerdbResponse{ 7 | private String code; 8 | private Data data; 9 | private boolean success; 10 | private String message; 11 | 12 | @lombok.Data 13 | public static class Data { 14 | private PlayerdbPlayer player; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/model/JavaOp.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users.model; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.experimental.SuperBuilder; 6 | import lombok.extern.jackson.Jacksonized; 7 | 8 | @Data 9 | @EqualsAndHashCode(callSuper = true) 10 | @SuperBuilder 11 | @Jacksonized 12 | public class JavaOp extends JavaUser { 13 | 14 | final int level; 15 | 16 | final boolean bypassesPlayerLimit; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/users/model/JavaUser.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.users.model; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | 7 | @Data @SuperBuilder 8 | @Jacksonized 9 | public class JavaUser { 10 | final String name; 11 | 12 | final String uuid; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/vanillatweaks/LegacyManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.extern.jackson.Jacksonized; 6 | 7 | import java.time.Instant; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | @Data 12 | @Builder @Jacksonized 13 | public class LegacyManifest { 14 | Instant timestamp; 15 | 16 | List shareCodes; 17 | 18 | Set files; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/vanillatweaks/VanillaTweaksManifest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.SuperBuilder; 5 | import lombok.extern.jackson.Jacksonized; 6 | import me.itzg.helpers.files.BaseManifest; 7 | 8 | import java.util.List; 9 | 10 | 11 | @Getter 12 | @SuperBuilder 13 | @Jacksonized 14 | public class VanillaTweaksManifest extends BaseManifest { 15 | List shareCodes; 16 | List packFiles; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/vanillatweaks/model/PackDefinition.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks.model; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class PackDefinition { 9 | 10 | Type type; 11 | 12 | String version; 13 | 14 | Map> packs; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/vanillatweaks/model/Type.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks.model; 2 | 3 | @SuppressWarnings("SpellCheckingInspection") 4 | public enum Type { 5 | resourcepacks, 6 | datapacks, 7 | craftingtweaks 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/vanillatweaks/model/ZipLinkResponse.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ZipLinkResponse { 7 | 8 | String status; 9 | 10 | String link; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/versions/Comparison.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.versions; 2 | 3 | public enum Comparison { 4 | lt 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/versions/JavaReleaseCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.versions; 2 | 3 | import java.util.concurrent.Callable; 4 | import picocli.CommandLine.Command; 5 | import picocli.CommandLine.ExitCode; 6 | 7 | @Command(name = "java-release", description = "Outputs the Java release number, such as 8, 11, 17") 8 | public class JavaReleaseCommand implements Callable { 9 | 10 | @Override 11 | public Integer call() throws Exception { 12 | final String versionStr = System.getProperty("java.version"); 13 | 14 | final String[] parts = versionStr.split("\\."); 15 | 16 | try { 17 | final int major = Integer.parseInt(parts[0]); 18 | final Integer minor = parts.length >= 2 ? Integer.parseInt(parts[1]) : null; 19 | if (major == 1 && minor != null) { 20 | System.out.println(minor); 21 | } 22 | else if (major >= 9) { 23 | System.out.println(major); 24 | } 25 | else { 26 | System.err.println("Unexpected version: " + versionStr); 27 | return ExitCode.SOFTWARE; 28 | } 29 | } catch (NumberFormatException e) { 30 | System.err.println("Malformed version: " + versionStr); 31 | return ExitCode.SOFTWARE; 32 | } 33 | 34 | return ExitCode.OK; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/versions/ResolveMinecraftVersionCommand.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.versions; 2 | 3 | import java.util.concurrent.Callable; 4 | import me.itzg.helpers.http.Fetch; 5 | import me.itzg.helpers.http.SharedFetch; 6 | import me.itzg.helpers.http.SharedFetchArgs; 7 | import picocli.CommandLine.ArgGroup; 8 | import picocli.CommandLine.Command; 9 | import picocli.CommandLine.ExitCode; 10 | import picocli.CommandLine.Parameters; 11 | 12 | @Command(name = "resolve-minecraft-version", description = "Resolves and validate latest, snapshot, and specific versions") 13 | public class ResolveMinecraftVersionCommand implements Callable { 14 | 15 | @Parameters(arity = "1") 16 | String inputVersion; 17 | 18 | @ArgGroup(exclusive = false) 19 | SharedFetchArgs sharedFetchArgs = new SharedFetchArgs(); 20 | 21 | @Override 22 | public Integer call() throws Exception { 23 | try (SharedFetch sharedFetch = Fetch.sharedFetch("resolve-minecraft-version", sharedFetchArgs.options())) { 24 | final String resolved = new MinecraftVersionsApi(sharedFetch) 25 | .resolve(inputVersion) 26 | .block(); 27 | if (resolved == null) { 28 | System.err.println("Unable to resolve version from "+inputVersion); 29 | return ExitCode.USAGE; 30 | } 31 | 32 | System.out.println(resolved); 33 | } 34 | 35 | return ExitCode.OK; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/me/itzg/helpers/versions/VersionManifestV2.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.versions; 2 | 3 | import java.net.URI; 4 | import java.util.List; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class VersionManifestV2 { 9 | public enum VersionType { 10 | release, 11 | snapshot, 12 | old_beta, 13 | old_alpha 14 | } 15 | 16 | @Data 17 | public static class Latest { 18 | private String release; 19 | private String snapshot; 20 | } 21 | 22 | @Data 23 | public static class Version { 24 | private String id; 25 | private VersionType type; 26 | private URI url; 27 | } 28 | 29 | private Latest latest; 30 | 31 | private List versions; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | INFO 7 | ACCEPT 8 | DENY 9 | 10 | 12 | 13 | %customHighlight([mc-image-helper] %d{HH:mm:ss.SSS} %-5level : %msg%n) 14 | 15 | 16 | 17 | true 18 | 19 | INFO 20 | DENY 21 | ACCEPT 22 | 23 | System.err 24 | 26 | 27 | %customHighlight([mc-image-helper] %d{HH:mm:ss.SSS} %-5level : %msg%n) 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/CharsetDetectorTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class CharsetDetectorTest { 11 | 12 | @Test 13 | void canDetectUtf8() throws IOException { 14 | // from https://en.wikipedia.org/wiki/UTF-8#Examples 15 | final byte[] content = new byte[]{ 16 | 0x24, 17 | (byte) 0xC2, (byte) 0xA2, 18 | (byte) 0xe0, (byte) 0xa4, (byte) 0xb9, 19 | // https://en.wikipedia.org/wiki/Hwair 20 | (byte) 0xf0, (byte) 0x90, (byte) 0x8d, (byte) 0x88 21 | }; 22 | 23 | final CharsetDetector.Result result = CharsetDetector.detect(content); 24 | assertThat(result.getCharset()).isEqualTo(StandardCharsets.UTF_8); 25 | // last one takes two UTF-16 characters, so takes up size=2 26 | assertThat(result.getContent().length()).isEqualTo(5); 27 | assertThat(result.getContent().toString()).isEqualTo("\u0024\u00a2\u0939\uD800\uDF48"); 28 | } 29 | 30 | @Test 31 | void canDetectIso8859_1() throws IOException { 32 | // from https://en.wikipedia.org/wiki/UTF-8#Examples 33 | final byte[] content = new byte[]{ 34 | // $ 35 | 0x24, 36 | // https://en.wikipedia.org/wiki/Cent_(currency) 37 | (byte) 0xa2, 38 | // https://en.wikipedia.org/wiki/%C3%9D 39 | (byte) 0xfd 40 | }; 41 | 42 | final CharsetDetector.Result result = CharsetDetector.detect(content); 43 | assertThat(result.getCharset()).isEqualTo(StandardCharsets.ISO_8859_1); 44 | assertThat(result.getContent().length()).isEqualTo(3); 45 | assertThat(result.getContent().toString()).isEqualTo("\u0024\u00a2\u00fd"); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/LatchingExecutionExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import picocli.CommandLine; 6 | 7 | @Getter 8 | @Slf4j 9 | public class LatchingExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { 10 | 11 | Exception executionException; 12 | 13 | @Override 14 | public int handleExecutionException(Exception e, CommandLine commandLine, CommandLine.ParseResult parseResult) { 15 | this.executionException = e; 16 | log.error("Exception during command execution", e); 17 | return CommandLine.ExitCode.SOFTWARE; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/MoreAssertions.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers; 2 | 3 | import java.util.Arrays; 4 | import org.assertj.core.api.ListAssert; 5 | 6 | public class MoreAssertions { 7 | 8 | public static ListAssert assertThatLines(String content) { 9 | final String[] lines = content.split("\n|\r\n|\r"); 10 | return new ListAssert<>(Arrays.stream(lines, 0, 11 | lines[lines.length - 1].isEmpty() ? lines.length - 1 : lines.length 12 | )); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/TestLoggingAppender.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers; 2 | 3 | import ch.qos.logback.classic.spi.LoggingEvent; 4 | import ch.qos.logback.core.AppenderBase; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class TestLoggingAppender extends AppenderBase { 9 | 10 | private static final List events = new ArrayList<>(); 11 | 12 | @Override 13 | protected void append(LoggingEvent e) { 14 | events.add(e); 15 | } 16 | 17 | public static List getEvents() { 18 | return events; 19 | } 20 | 21 | public static void reset() { 22 | events.clear(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/curseforge/CurseForgeApiClientTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 7 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 8 | import java.util.Collections; 9 | import me.itzg.helpers.cache.ApiCachingDisabled; 10 | import me.itzg.helpers.http.SharedFetch.Options; 11 | import org.intellij.lang.annotations.Language; 12 | import org.junit.jupiter.api.Test; 13 | 14 | @WireMockTest 15 | class CurseForgeApiClientTest { 16 | 17 | @Test 18 | void apiKeyHeaderIsTrimmed(WireMockRuntimeInfo wmInfo) { 19 | @Language("JSON") final String body = "{\"data\": []}"; 20 | stubFor(get("/v1/categories?gameId=test&classesOnly=true") 21 | .willReturn(aResponse() 22 | .withBody(body) 23 | .withHeader("Content-Type", "application/json") 24 | ) 25 | ); 26 | 27 | final CategoryInfo result; 28 | try (CurseForgeApiClient client = new CurseForgeApiClient(wmInfo.getHttpBaseUrl(), "key\n", Options.builder().build(), 29 | "test", new ApiCachingDisabled() 30 | )) { 31 | result = client.loadCategoryInfo(Collections.singleton("mc-mods")) 32 | .block(); 33 | } 34 | 35 | assertThat(result).isNotNull(); 36 | 37 | verify(getRequestedFor(urlEqualTo("/v1/categories?gameId=test&classesOnly=true")) 38 | .withHeader("x-api-key", equalTo("key")) 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/curseforge/CurseForgeInstallerTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.curseforge; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 9 | import org.junit.jupiter.api.io.TempDir; 10 | 11 | class CurseForgeInstallerTest { 12 | @TempDir 13 | Path tempDir; 14 | 15 | /* 16 | Scenarios to test 17 | 18 | ### 19 | Duplicated slug for modpack and mod (hyperion) 20 | 21 | ### 22 | Exclude/include by "gameVersions" 23 | 24 | "gameVersions": [ 25 | "Client", 26 | "1.16.5", 27 | "Forge" 28 | ], 29 | 30 | Reject 31 | ### Oculus mc1.16.5-1.4.5 32 | GET https://api.curse.tools/v1/cf/mods/581495/files/4300427 33 | 34 | 35 | Keep 36 | ### [Fabric] Resourceful Lib 1.2.2 37 | GET https://api.curse.tools/v1/cf/mods/570073/files/4326308 38 | "gameVersions": [ 39 | "1.19.3", 40 | "Fabric", 41 | "Client", 42 | "Server" 43 | ], 44 | 45 | ### Patchouli-1.19.2-77.jar 46 | GET https://api.curse.tools/v1/cf/mods/306770/files/4031402 47 | "gameVersions": [ 48 | "1.19.2", 49 | "Forge" 50 | ], 51 | 52 | */ 53 | 54 | @Test 55 | @EnabledIfSystemProperty(named = "testEnableManualTests", matches = "true", disabledReason = "For manual recording") 56 | void testManual() throws IOException { 57 | final Path resultsFile = tempDir.resolve(".results.env"); 58 | 59 | final CurseForgeInstaller installer = new CurseForgeInstaller(tempDir, resultsFile); 60 | installer.install("all-the-mods-8", "1.0.4", null); 61 | 62 | assertThat(tempDir) 63 | .isNotEmptyDirectory(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/env/InterpolatorTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.when; 5 | 6 | import java.io.IOException; 7 | import me.itzg.helpers.env.Interpolator.Result; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | class InterpolatorTest { 15 | 16 | @Mock 17 | EnvironmentVariablesProvider varProvider; 18 | 19 | @Test 20 | void typicalReplacements() throws IOException { 21 | when(varProvider.get("CFG_VAR_FILE")) 22 | .thenReturn(null); 23 | when(varProvider.get("CFG_VAR")) 24 | .thenReturn("new-value"); 25 | 26 | final Interpolator interpolator = new Interpolator(varProvider, "CFG_"); 27 | final Result result = interpolator.interpolate("${IGNORE_THIS}\n${CFG_VAR}\n"); 28 | assertThat(result.getContent()) 29 | .isEqualTo("${IGNORE_THIS}\nnew-value\n"); 30 | assertThat(result.getReplacementCount()).isEqualTo(1); 31 | } 32 | 33 | @Test 34 | void interpolateToValueWithDollarSign() throws IOException { 35 | when(varProvider.get("CFG_HAS_DOLLAR_VALUE_FILE")) 36 | .thenReturn(null); 37 | when(varProvider.get("CFG_HAS_DOLLAR_VALUE")) 38 | .thenReturn("$(SOME_VALUE)"); 39 | 40 | final Interpolator interpolator = new Interpolator(varProvider, "CFG_"); 41 | final Result result = interpolator.interpolate("Interpolate this: ${CFG_HAS_DOLLAR_VALUE}"); 42 | assertThat(result.getContent()) 43 | .isEqualTo("Interpolate this: $(SOME_VALUE)"); 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/env/MappedEnvVarProvider.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.env; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class MappedEnvVarProvider implements EnvironmentVariablesProvider { 9 | 10 | final Map values = new HashMap<>(); 11 | 12 | private MappedEnvVarProvider() { 13 | 14 | } 15 | 16 | public static MappedEnvVarProvider of(String... nameValue) { 17 | assertThat(nameValue.length).isEven(); 18 | 19 | final MappedEnvVarProvider provider = new MappedEnvVarProvider(); 20 | for (int i = 0; i < nameValue.length; i += 2) { 21 | provider.values.put(nameValue[i], nameValue[i + 1]); 22 | } 23 | 24 | return provider; 25 | } 26 | 27 | @Override 28 | public String get(String name) { 29 | return values.get(name); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/fabric/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "game_version": "1.19.2", 4 | "loader_version": "0.14.11", 5 | "installer_version": "0.11.1" 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/fabric/research.http: -------------------------------------------------------------------------------- 1 | ### 2 | GET https://meta.fabricmc.net/v2/versions/loader/1.19.2 3 | 4 | ### 5 | GET https://meta.fabricmc.net/v2/versions/loader/1.19.3 6 | 7 | ### 8 | GET https://meta.fabricmc.net/v2/versions/loader/1.12.2 9 | 10 | ### 11 | GET https://meta.fabricmc.net/v2/versions/loader/{{game_version}}/{{loader_version}}/server/json 12 | 13 | ### 14 | GET https://meta.fabricmc.net/v2/versions/installer 15 | 16 | ### 17 | GET https://meta.fabricmc.net/v2/versions/game 18 | 19 | ### 20 | GET https://meta.fabricmc.net/v2/versions/loader/{{game_version}}/{{loader_version}}/{{installer_version}}/server/jar 21 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/files/AntPathMatcherTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import static java.util.Collections.singletonList; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.util.Arrays; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class AntPathMatcherTest { 10 | 11 | @Test 12 | void literal() { 13 | final AntPathMatcher matcher = new AntPathMatcher(Arrays.asList("one", "two")); 14 | 15 | assertThat(matcher.matches("one")) 16 | .isTrue(); 17 | assertThat(matcher.matches("two")) 18 | .isTrue(); 19 | assertThat(matcher.matches("twooo")) 20 | .isFalse(); 21 | assertThat(matcher.matches("three")) 22 | .isFalse(); 23 | } 24 | 25 | @Test 26 | void fileSuffixTopDir() { 27 | final AntPathMatcher matcher = new AntPathMatcher(singletonList("*.jar")); 28 | 29 | assertThat(matcher.matches("mod.jar")) 30 | .isTrue(); 31 | } 32 | 33 | @Test 34 | void butNotFileSuffixInSubdir() { 35 | final AntPathMatcher matcher = new AntPathMatcher(singletonList("*.jar")); 36 | 37 | assertThat(matcher.matches("mods/mod.jar")) 38 | .isFalse(); 39 | } 40 | 41 | @Test 42 | void fileSuffixInSubdir() { 43 | final AntPathMatcher matcher = new AntPathMatcher(singletonList("**/*.jar")); 44 | 45 | assertThat(matcher.matches("mods/mod.jar")) 46 | .isTrue(); 47 | assertThat(matcher.matches("extra/mods/mod.jar")) 48 | .isTrue(); 49 | } 50 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/files/ManifestsTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.Collections; 9 | import lombok.Getter; 10 | import lombok.experimental.SuperBuilder; 11 | import lombok.extern.jackson.Jacksonized; 12 | import org.apache.commons.lang3.RandomStringUtils; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.io.TempDir; 15 | 16 | class ManifestsTest { 17 | 18 | @TempDir 19 | Path tempDir; 20 | 21 | @Getter @SuperBuilder @Jacksonized 22 | static class EmptyManifest extends BaseManifest { 23 | 24 | } 25 | 26 | @Test 27 | void loadFailsGracefullyWhenInvalid() throws IOException { 28 | final String id = RandomStringUtils.randomAlphabetic(5); 29 | 30 | final Path manifestFile = tempDir.resolve(String.format(".%s-manifest.json", id)); 31 | Files.write(manifestFile, Collections.singletonList("not json")); 32 | 33 | final EmptyManifest manifest = Manifests.load(tempDir, id, EmptyManifest.class); 34 | assertThat(manifest).isNull(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/files/TomlPathCommandTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOutNormalized; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.nio.file.Paths; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.ValueSource; 9 | import picocli.CommandLine; 10 | import picocli.CommandLine.ExitCode; 11 | 12 | class TomlPathCommandTest { 13 | 14 | @ParameterizedTest 15 | @ValueSource(strings = {"$.bind", ".bind"}) 16 | void extractsVelocityBind(String queryPath) throws Exception { 17 | final String out = tapSystemOutNormalized(() -> { 18 | final int exitCode = new CommandLine(new TomlPathCommand()) 19 | .execute( 20 | "--file", Paths.get("src/test/resources/velocity.toml").toString(), 21 | queryPath 22 | ); 23 | 24 | assertThat(exitCode).isEqualTo(ExitCode.OK); 25 | 26 | }); 27 | 28 | assertThat(out).isEqualTo("0.0.0.0:25565\n"); 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/files/YamlPathCommandTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.files; 2 | 3 | import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOut; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import picocli.CommandLine; 8 | 9 | class YamlPathCommandTest { 10 | 11 | @Test 12 | void pickOutFieldFromServerSetupConfig() throws Exception { 13 | final String output = tapSystemOut(() -> { 14 | final int exitCode = new CommandLine(new YamlPathCommand()) 15 | .execute( 16 | "--file", "src/test/resources/server-setup-config.yaml", 17 | ".install.baseInstallPath" 18 | ); 19 | assertThat(exitCode).isEqualTo(0); 20 | } 21 | ); 22 | 23 | assertThat(output).isEqualToIgnoringNewLines("setup/"); 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/http/LenientUriConverterTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.URI; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class LenientUriConverterTest { 9 | 10 | @Test 11 | void leavesPlusAsIs() throws Exception { 12 | final URI result = new LenientUriConverter().convert( 13 | "https://media.forgecdn.net/files/3482/169/Valhelsia+3-3.4.4-SERVER.zip"); 14 | 15 | // can't use URI#getPath() since it decodes away the %2B encoding of + 16 | assertThat(result.getRawPath()).isEqualTo("/files/3482/169/Valhelsia+3-3.4.4-SERVER.zip"); 17 | } 18 | 19 | @Test 20 | void convertsSquareBrackets() throws Exception { 21 | final URI result = new LenientUriConverter().convert( 22 | "https://files.forgecdn.net/files/2320/259/[1.10.x]FenceOverhaul-1.2.1.jar"); 23 | 24 | assertThat(result.getRawPath()).isEqualTo("/files/2320/259/%5B1.10.x%5DFenceOverhaul-1.2.1.jar"); 25 | } 26 | 27 | @Test 28 | void leavesLegalUriAsIs() throws Exception { 29 | final URI result = new LenientUriConverter().convert( 30 | "https://cdn.modrinth.com/data/P7dR8mSH/versions/0.55.3+1.19/fabric-api-0.55.3%2B1.19.jar" 31 | ); 32 | 33 | assertThat(result).isEqualTo( 34 | URI.create("https://cdn.modrinth.com/data/P7dR8mSH/versions/0.55.3+1.19/fabric-api-0.55.3%2B1.19.jar")); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/http/ObjectListFetchBuilderTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.http; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 4 | import static me.itzg.helpers.http.Fetch.fetch; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import com.github.tomakehurst.wiremock.client.WireMock; 8 | import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 9 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 10 | import java.io.IOException; 11 | import java.net.URI; 12 | import java.util.List; 13 | import lombok.Data; 14 | import org.junit.jupiter.api.Test; 15 | 16 | @WireMockTest 17 | class ObjectListFetchBuilderTest { 18 | 19 | @Data 20 | static class Entry { 21 | private String name; 22 | } 23 | 24 | @Test 25 | void testBasicScenario(WireMockRuntimeInfo wm) throws IOException { 26 | stubFor( 27 | get("/content") 28 | .withHeader("accept", WireMock.equalTo("application/json")) 29 | .willReturn( 30 | jsonResponse( 31 | "[\n" 32 | + " {\n" 33 | + " \"name\": \"alpha\"\n" 34 | + " },\n" 35 | + " {\n" 36 | + " \"name\": \"beta\"\n" 37 | + " }\n" 38 | + "]", 200) 39 | ) 40 | ); 41 | 42 | final List results = fetch(URI.create(wm.getHttpBaseUrl() + "/content")) 43 | .toObjectList(Entry.class) 44 | .assemble() 45 | .block(); 46 | 47 | assertThat(results) 48 | .hasSize(2) 49 | .extracting("name") 50 | .contains("alpha", "beta"); 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/lombok.config: -------------------------------------------------------------------------------- 1 | config.stopbubbling=true 2 | lombok.accessors.chain=true -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/modrinth/ModrinthHttpPackFetcherTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.modrinth; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 7 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.nio.file.Path; 11 | import me.itzg.helpers.http.SharedFetchArgs; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.io.TempDir; 14 | 15 | @WireMockTest 16 | public class ModrinthHttpPackFetcherTest { 17 | @Test 18 | void fetchesMrpackViaHttp( 19 | WireMockRuntimeInfo wm, @TempDir Path tempDir 20 | ) throws URISyntaxException { 21 | String modpackUrlPath = "/files/modpack/test.mrpack"; 22 | String expectedModpackData = "test modpack data"; 23 | 24 | ModrinthApiClient apiClient = new ModrinthApiClient( 25 | wm.getHttpBaseUrl(), "unit-test", new SharedFetchArgs().options()); 26 | 27 | stubFor(get(modpackUrlPath).willReturn(ok().withBody(expectedModpackData))); 28 | 29 | ModrinthHttpPackFetcher fetcherUT = new ModrinthHttpPackFetcher( 30 | apiClient, tempDir, new URI(wm.getHttpBaseUrl() + modpackUrlPath)); 31 | 32 | final FetchedPack fetchedPack = fetcherUT.fetchModpack(null).block(); 33 | assertThat(fetchedPack).isNotNull(); 34 | assertThat(fetchedPack.getMrPackFile()).content() 35 | .isEqualTo(expectedModpackData); 36 | assertThat(fetchedPack.getProjectSlug()).isNotBlank(); 37 | assertThat(fetchedPack.getVersionId()).isNotBlank(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/singles/HashCommandTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.singles; 2 | 3 | import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOut; 4 | import static com.github.stefanbirkner.systemlambda.SystemLambda.withTextFromSystemIn; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import picocli.CommandLine; 9 | 10 | class HashCommandTest { 11 | 12 | @Test 13 | void simple() throws Exception { 14 | final String output = tapSystemOut(() -> 15 | withTextFromSystemIn("testing").execute(() -> { 16 | final int exitCode = new CommandLine(new HashCommand()).execute(); 17 | assertThat(exitCode).isEqualTo(0); 18 | }) 19 | ); 20 | 21 | assertThat(output).satisfiesAnyOf( 22 | // on Linux with \n 23 | s -> assertThat(s).isEqualToIgnoringNewLines("eb1a3227cdc3fedbaec2fe38bf6c044a"), 24 | // on Windows with \r\n 25 | s -> assertThat(s).isEqualToIgnoringNewLines("92940b605f0952c4e67b7b8c000cee17") 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/java/me/itzg/helpers/vanillatweaks/VanillaTweaksCommandTest.java: -------------------------------------------------------------------------------- 1 | package me.itzg.helpers.vanillatweaks; 2 | 3 | import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; 4 | import com.github.tomakehurst.wiremock.junit5.WireMockTest; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.io.TempDir; 7 | import picocli.CommandLine; 8 | import picocli.CommandLine.ExitCode; 9 | 10 | import java.nio.file.Path; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | @WireMockTest 15 | class VanillaTweaksCommandTest { 16 | 17 | @Test 18 | void testOneOfEachSharecode(WireMockRuntimeInfo wmInfo, @TempDir Path tempDir) { 19 | wmInfo.getWireMock().loadMappingsFrom("src/test/resources/vanillatweaks"); 20 | 21 | // https://vanillatweaks.net/share#4mwbtQ resource pack 22 | // https://vanillatweaks.net/share#rG4JRm data pack 23 | // https://vanillatweaks.net/share#sgMq8u crafting tweaks 24 | final int exitCode = new CommandLine( 25 | new VanillaTweaksCommand() 26 | ) 27 | .execute( 28 | "--base-url", wmInfo.getHttpBaseUrl(), 29 | "--share-codes=4mwbtQ,rG4JRm,sgMq8u", 30 | "--output-directory", tempDir.toString() 31 | ); 32 | 33 | assertThat(exitCode).isEqualTo(ExitCode.OK); 34 | 35 | assertThat(tempDir.resolve("resourcepacks").resolve("VanillaTweaks_98060d2.zip")) 36 | .hasContent("4mwbtQ"); 37 | 38 | final Path datapacksDir = tempDir.resolve("world").resolve("datapacks"); 39 | assertThat(datapacksDir.resolve("datapack.txt")) 40 | .hasContent("rG4JRm"); 41 | assertThat(datapacksDir.resolve("VanillaTweaks_978e11f.zip")) 42 | .hasContent("sgMq8u"); 43 | } 44 | } -------------------------------------------------------------------------------- /src/test/resources/ModrinthCommandTest/__files/NOTES.md: -------------------------------------------------------------------------------- 1 | ## Mocking Modrinth CDN URLs 2 | 3 | In response JSON replace `https://cdn.modrinth.com` with `{{request.baseUrl}}/cdn` -------------------------------------------------------------------------------- /src/test/resources/__files/fabric-empty-launcher.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/src/test/resources/__files/fabric-empty-launcher.jar -------------------------------------------------------------------------------- /src/test/resources/__files/folia/projects-folia.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "folia", 3 | "project_name": "Folia", 4 | "version_groups": [ 5 | "1.19", 6 | "1.20" 7 | ], 8 | "versions": [ 9 | "1.19.4", 10 | "1.20.1", 11 | "1.20.2", 12 | "1.20.4", 13 | "1.20.6" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/test/resources/__files/github/release-with-sources-jar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app v1.3.0", 3 | "assets": [ 4 | { 5 | "name": "app-1.3.0-sources.jar", 6 | "browser_download_url": "{{request.baseUrl}}/invalid.jar" 7 | }, 8 | { 9 | "name": "app-1.3.0.jar", 10 | "browser_download_url": "{{request.baseUrl}}/download/{{parameters.filename}}" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/test/resources/__files/modrinth/project-version-chunky-fabric-1.21.1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "game_versions": [ 4 | "1.21", 5 | "1.21.1" 6 | ], 7 | "loaders": [ 8 | "fabric" 9 | ], 10 | "id": "dPliWter", 11 | "project_id": "fALzjamp", 12 | "author_id": "nfBsvT1J", 13 | "featured": false, 14 | "name": "Chunky 1.4.16", 15 | "version_number": "1.4.16", 16 | "changelog": "", 17 | "changelog_url": null, 18 | "date_published": "2024-06-17T06:00:13.853652Z", 19 | "downloads": 134137, 20 | "version_type": "release", 21 | "status": "listed", 22 | "requested_status": null, 23 | "files": [ 24 | { 25 | "hashes": { 26 | "sha512": "7e862f4db563bbb5cfa8bc0c260c9a97b7662f28d0f8405355c33d7b4100ce05378b39ed37c5d75d2919a40c244a3011bb4ba63f9d53f10d50b11b32656ea395", 27 | "sha1": "2b68d2bd507cd9f45fdd637ba32e8a47de6bda35" 28 | }, 29 | "url": "https://cdn.modrinth.com/data/fALzjamp/versions/dPliWter/Chunky-1.4.16.jar", 30 | "filename": "Chunky-1.4.16.jar", 31 | "primary": true, 32 | "size": 343847, 33 | "file_type": null 34 | } 35 | ], 36 | "dependencies": [ 37 | { 38 | "version_id": null, 39 | "project_id": "P7dR8mSH", 40 | "file_name": null, 41 | "dependency_type": "required" 42 | } 43 | ] 44 | } 45 | ] -------------------------------------------------------------------------------- /src/test/resources/cf-excludeInclude-ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalExcludes": [ 3 | "581495", 4 | "363363", 5 | "448233", 6 | "574856", 7 | "441114", 8 | "521480", 9 | "433760", 10 | "568563", 11 | "274259", 12 | "787666", 13 | "700629" 14 | ], 15 | "globalForceIncludes": [ 16 | "656526" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/test/resources/cf-excludeInclude-slugs.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalExcludes": [ 3 | "oculus", 4 | "extreme-sound-muffler", 5 | "entityculling", 6 | "rubidium", 7 | "nekos-enchanted-books", 8 | "skin-layers-3d", 9 | "not-enough-animations", 10 | "entity-texture-features-fabric", 11 | "carry-on", 12 | "ignitioncoil", 13 | "defensive-measures" 14 | ], 15 | "globalForceIncludes": [ 16 | "revelationary" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/test/resources/content/excluded/notthis.txt: -------------------------------------------------------------------------------- 1 | Should stay at ${CFG_TEST1} -------------------------------------------------------------------------------- /src/test/resources/content/mainmenu.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/src/test/resources/content/mainmenu.json -------------------------------------------------------------------------------- /src/test/resources/content/never.txt: -------------------------------------------------------------------------------- 1 | Should stay at ${CFG_TEST2} -------------------------------------------------------------------------------- /src/test/resources/content/plain.txt: -------------------------------------------------------------------------------- 1 | No variables in here 2 | -------------------------------------------------------------------------------- /src/test/resources/content/testing.txt: -------------------------------------------------------------------------------- 1 | test1=${CFG_TEST1} 2 | test2=${CFG_TEST2} 3 | ignored=${IGNORE_TEST1} 4 | test3=${CFG_TEST3} 5 | test4=${CFG_TEST4} 6 | one -------------------------------------------------------------------------------- /src/test/resources/curseforge/mappings/README.md: -------------------------------------------------------------------------------- 1 | Without the following args within the test case 2 | 3 | ``` 4 | "--api-base-url", wm.baseUrl(), 5 | "--api-key", "test", 6 | ``` 7 | 8 | Started Wiremock and a recording 9 | 10 | Set `CF_API_BASE_URL` to "http://localhost:8080" 11 | 12 | Ran the test in IntelliJ with `CF_API_KEY` env var set to a real API key. 13 | 14 | Copied the mappings into this directory 15 | 16 | Removed from each: 17 | - `persistent` 18 | - `scenarioName` 19 | - `requiredScenarioState` 20 | - `insertionIndex` 21 | 22 | Added to the mods/files: 23 | - `"transformers": ["response-template"]` 24 | - Changed the `downloadUrl` values to use a response template placeholder -------------------------------------------------------------------------------- /src/test/resources/curseforge/mappings/cdn.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/files/.+" 5 | }, 6 | "response": { 7 | "body": "{{request.path.[1]}}", 8 | "headers": { 9 | "Content-Disposition": "attachment; filename=\"{{request.path.[1]}}\"" 10 | }, 11 | "transformers": ["response-template"] 12 | } 13 | } -------------------------------------------------------------------------------- /src/test/resources/curseforge/mappings/v1_mods_31043.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "v1_mods_31043", 3 | "request" : { 4 | "url" : "/v1/mods/31043", 5 | "method" : "GET" 6 | }, 7 | "response" : { 8 | "status" : 200, 9 | "jsonBody" : { 10 | "data": { 11 | "name": "WorldEdit for Bukkit", 12 | "links": { 13 | "websiteUrl": "https://www.curseforge.com/minecraft/bukkit-plugins/worldedit", 14 | "wikiUrl": "https://worldedit.enginehub.org/en/latest/", 15 | "issuesUrl": "https://github.com/EngineHub/WorldEdit/issues", 16 | "sourceUrl": "https://github.com/EngineHub/WorldEdit" 17 | } 18 | } 19 | }, 20 | "headers" : { 21 | "Content-Type" : "application/json; charset=utf-8" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/resources/fabric/mappings/v2_versions_loader_1_19_2_server_jar-GET.json: -------------------------------------------------------------------------------- 1 | { 2 | "request" : { 3 | "url" : "/v2/versions/loader/1.19.2/0.14.12/0.11.1/server/jar", 4 | "method" : "GET" 5 | }, 6 | "response" : { 7 | "status" : 200, 8 | "body": "fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1", 9 | "headers" : { 10 | "Content-Type" : "application/java-archive", 11 | "Content-Disposition" : "attachment; filename=\"fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1.jar\"" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/resources/fabric/mappings/v2_versions_loader_1_19_2_server_jar-HEAD.json: -------------------------------------------------------------------------------- 1 | { 2 | "request" : { 3 | "url" : "/v2/versions/loader/1.19.2/0.14.12/0.11.1/server/jar", 4 | "method" : "HEAD" 5 | }, 6 | "response" : { 7 | "status" : 200, 8 | "headers" : { 9 | "Content-Type" : "application/java-archive", 10 | "Content-Disposition" : "attachment; filename=\"fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1.jar\"", 11 | "Last-Modified" : "Tue, 20 Dec 2022 20:56:00 GMT" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/resources/fabric/mappings/v2_versions_loader_1_19_3_server_jar-GET.json: -------------------------------------------------------------------------------- 1 | { 2 | "request" : { 3 | "url" : "/v2/versions/loader/1.19.3/0.14.12/0.11.1/server/jar", 4 | "method" : "GET" 5 | }, 6 | "response" : { 7 | "status" : 200, 8 | "body": "fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1", 9 | "headers" : { 10 | "Content-Type" : "application/java-archive", 11 | "Content-Disposition" : "attachment; filename=\"fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar\"" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/resources/fabric/mappings/v2_versions_loader_1_19_3_server_jar-HEAD.json: -------------------------------------------------------------------------------- 1 | { 2 | "request" : { 3 | "url" : "/v2/versions/loader/1.19.3/0.14.12/0.11.1/server/jar", 4 | "method" : "HEAD" 5 | }, 6 | "response" : { 7 | "status" : 200, 8 | "headers" : { 9 | "Content-Type" : "application/java-archive", 10 | "Content-Disposition" : "attachment; filename=\"fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar\"", 11 | "Cache-Control" : "public, max-age=86400", 12 | "Last-Modified" : "Tue, 20 Dec 2022 20:56:00 GMT" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/test/resources/fabric/test-file.txt: -------------------------------------------------------------------------------- 1 | This is just test content -------------------------------------------------------------------------------- /src/test/resources/forge/forge-1.20.2-48.1.0-installer-trimmed.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzg/mc-image-helper/7b201352733f2613582555bff693e6b446e889a7/src/test/resources/forge/forge-1.20.2-48.1.0-installer-trimmed.jar -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | INFO 5 | ACCEPT 6 | DENY 7 | 8 | 10 | 11 | [mc-image-helper] %d{HH:mm:ss.SSS} %-5level : %msg%n 12 | 13 | 14 | 15 | 16 | INFO 17 | DENY 18 | ACCEPT 19 | 20 | System.err 21 | 23 | 24 | [mc-image-helper] %d{HH:mm:ss.SSS} %-5level : %msg%n 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test/resources/mvn/__files/packwiz_maven-metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | link.infra.packwiz 4 | packwiz-installer 5 | 6 | v0.5.12 7 | v0.5.12 8 | 9 | v0.5.0 10 | v0.5.1 11 | v0.5.2 12 | v0.5.3 13 | v0.5.4 14 | v0.5.5 15 | v0.5.6 16 | v0.5.7 17 | v0.5.8 18 | v0.5.9 19 | v0.5.10 20 | v0.5.11 21 | v0.5.12 22 | 23 | 20230806195740 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/paper-env.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | velocity-support: 3 | enabled: false 4 | online-mode: false 5 | secret: '${CFG_VELOCITY_SECRET}' 6 | -------------------------------------------------------------------------------- /src/test/resources/paper-out.yml: -------------------------------------------------------------------------------- 1 | settings: 2 | velocity-support: 3 | enabled: false 4 | online-mode: false 5 | secret: 'SuperSecretValue' 6 | -------------------------------------------------------------------------------- /src/test/resources/patch-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "patches": [ 3 | { 4 | "file": "/data/paper.yml", 5 | "ops": [ 6 | { 7 | "$set": { 8 | "path": "$.verbose", 9 | "value": true 10 | } 11 | }, 12 | { 13 | "$set": { 14 | "path": "$.settings['velocity-support'].enabled", 15 | "value": "${CFG_VELOCITY_ENABLED}", 16 | "value-type": "bool" 17 | } 18 | }, 19 | { 20 | "$put": { 21 | "path": "$.settings", 22 | "key": "my-test-setting", 23 | "value": "testing" 24 | } 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setInJson.json: -------------------------------------------------------------------------------- 1 | { 2 | "outer":{ 3 | "field1":"new value" 4 | } 5 | } -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setInJson5.json5: -------------------------------------------------------------------------------- 1 | { 2 | "outer":{ 3 | "field1":"new value" 4 | }, 5 | "unquoted":"and you can quote me on that", 6 | "singleQuotes":"I can use \"double quotes\" here", 7 | "lineBreaks":"Look, Mom! \nNo \\n's!", 8 | "leadingDecimalPoint":0.8675309, 9 | "andTrailing":8675309.0, 10 | "positiveSign":1, 11 | "trailingComma":"in objects", 12 | "andIn":[ "arrays" ], 13 | "backwardsCompatible":"with JSON" 14 | } -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setInToml.toml: -------------------------------------------------------------------------------- 1 | outer.field1 = 'new value' 2 | outer.field2 = 'value2' 3 | outer.field3 = 'value3' 4 | -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setInYaml.yaml: -------------------------------------------------------------------------------- 1 | outer: 2 | field1: new value 3 | field2: value2 4 | field3: value3 -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setNativeTypes.yaml: -------------------------------------------------------------------------------- 1 | outer: 2 | field1: 5 3 | field2: 5.1 4 | field3: true 5 | field4: 6 | - 5 7 | - 6 8 | - 7 -------------------------------------------------------------------------------- /src/test/resources/patch/expected-setWithEnv.yaml: -------------------------------------------------------------------------------- 1 | outer: 2 | field1: value from env 3 | field2: value2 4 | field3: "${GETS_IGNORED}" -------------------------------------------------------------------------------- /src/test/resources/patch/testing-with-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "outer": { 3 | "array": [] 4 | } 5 | } -------------------------------------------------------------------------------- /src/test/resources/patch/testing-with-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "outer": { 3 | // some comment 4 | "field1": "old" 5 | } 6 | } -------------------------------------------------------------------------------- /src/test/resources/patch/testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "outer": { 3 | "field1": "old" 4 | } 5 | } -------------------------------------------------------------------------------- /src/test/resources/patch/testing.json5: -------------------------------------------------------------------------------- 1 | { 2 | // comments 3 | outer: { 4 | field1: "old" 5 | }, 6 | unquoted: 'and you can quote me on that', 7 | singleQuotes: 'I can use "double quotes" here', 8 | lineBreaks: "Look, Mom! \ 9 | No \\n's!", 10 | leadingDecimalPoint: .8675309, andTrailing: 8675309., 11 | positiveSign: +1, 12 | trailingComma: 'in objects', andIn: ['arrays',], 13 | "backwardsCompatible": "with JSON", 14 | } -------------------------------------------------------------------------------- /src/test/resources/patch/testing.toml: -------------------------------------------------------------------------------- 1 | [outer] 2 | field1 = "value1" 3 | field2 = "value2" 4 | field3 = "value3" -------------------------------------------------------------------------------- /src/test/resources/patch/testing.yaml: -------------------------------------------------------------------------------- 1 | outer: 2 | field1: value1 3 | field2: value2 4 | field3: value3 -------------------------------------------------------------------------------- /src/test/resources/properties/with-escapes.txt: -------------------------------------------------------------------------------- 1 | motd=\u00A7c\u00A7lT\u00A76\u00A7le\u00A7e\u00A7ls\u00A7a\u00A7lt\u00A73\u00A7li\u00A79\u00A7ln\u00A75\u00A7lg \u00A76\u00A7l1\u00A7e\u00A7l2\u00A7a\u00A7l3 -------------------------------------------------------------------------------- /src/test/resources/properties/with-unicode.txt: -------------------------------------------------------------------------------- 1 | motd=§c§lT§6§le§e§ls§a§lt§3§li§9§ln§5§lg §6§l1§e§l2§a§l3 -------------------------------------------------------------------------------- /src/test/resources/server-setup-config.yaml: -------------------------------------------------------------------------------- 1 | # Version of the specs, only for internal usage if this format should ever change drastically 2 | _specver: 2 3 | 4 | # settings regarding the installation of the modpack 5 | install: 6 | 7 | # The base path where the server should be installed to, ~ for current path 8 | baseInstallPath: setup/ 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/test3.txt: -------------------------------------------------------------------------------- 1 | value3 2 | -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_sharecodephp-af3b07fa-cb39-44f3-b8cb-871076311d74.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "af3b07fa-cb39-44f3-b8cb-871076311d74", 3 | "name" : "assets_server_sharecodephp", 4 | "request" : { 5 | "url" : "/assets/server/sharecode.php?code=rG4JRm", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "body" : "{\"type\":\"datapacks\",\"version\":\"1.19\",\"packs\":{\"survival\":[\"armor statues\"]},\"result\":\"ok\"}", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:39 GMT", 13 | "Content-Type" : "text/html; charset=UTF-8", 14 | "Vary" : "Accept-Encoding" 15 | } 16 | }, 17 | "uuid" : "af3b07fa-cb39-44f3-b8cb-871076311d74", 18 | "persistent" : false, 19 | "insertionIndex" : 23 20 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_sharecodephp-cf1755e0-a0c4-4020-a97d-adc860882596.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "cf1755e0-a0c4-4020-a97d-adc860882596", 3 | "name" : "assets_server_sharecodephp", 4 | "request" : { 5 | "url" : "/assets/server/sharecode.php?code=4mwbtQ", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "body" : "{\"type\":\"resourcepacks\",\"version\":\"1.19\",\"packs\":{\"aesthetic\":[\"AlternateBlockDestruction\"]},\"result\":\"ok\"}", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:39 GMT", 13 | "Content-Type" : "text/html; charset=UTF-8", 14 | "Vary" : "Accept-Encoding" 15 | } 16 | }, 17 | "uuid" : "cf1755e0-a0c4-4020-a97d-adc860882596", 18 | "persistent" : false, 19 | "insertionIndex" : 21 20 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_sharecodephp-d520ebaa-7346-4228-af50-bee647057b5b.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "d520ebaa-7346-4228-af50-bee647057b5b", 3 | "name" : "assets_server_sharecodephp", 4 | "request" : { 5 | "url" : "/assets/server/sharecode.php?code=sgMq8u", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "body" : "{\"type\":\"craftingtweaks\",\"version\":\"1.19\",\"packs\":{\"quality-of-life\":[\"back to blocks\"]},\"result\":\"ok\"}", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:39 GMT", 13 | "Content-Type" : "text/html; charset=UTF-8", 14 | "Vary" : "Accept-Encoding" 15 | } 16 | }, 17 | "uuid" : "d520ebaa-7346-4228-af50-bee647057b5b", 18 | "persistent" : false, 19 | "insertionIndex" : 22 20 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_zipcraftingtweaksphp-c9d8e902-29f4-4537-bd14-18232974d32e.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "c9d8e902-29f4-4537-bd14-18232974d32e", 3 | "name" : "assets_server_zipcraftingtweaksphp", 4 | "request" : { 5 | "url" : "/assets/server/zipcraftingtweaks.php", 6 | "method" : "POST", 7 | "bodyPatterns" : [ { 8 | "equalTo" : "packs=%7B%22quality-of-life%22%3A%5B%22back+to+blocks%22%5D%7D&version=1.19", 9 | "caseInsensitive" : false 10 | } ] 11 | }, 12 | "response" : { 13 | "status" : 200, 14 | "body" : "{\"status\":\"success\",\"link\":\"\\/download\\/VanillaTweaks_c826799.zip\"}", 15 | "headers" : { 16 | "Date" : "Thu, 20 Apr 2023 00:58:40 GMT", 17 | "Content-Type" : "text/html; charset=UTF-8" 18 | } 19 | }, 20 | "uuid" : "c9d8e902-29f4-4537-bd14-18232974d32e", 21 | "persistent" : false, 22 | "insertionIndex" : 28 23 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_zipdatapacksphp-92d91d61-e30a-4313-86c7-c84d97fe656f.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "92d91d61-e30a-4313-86c7-c84d97fe656f", 3 | "name" : "assets_server_zipdatapacksphp", 4 | "request" : { 5 | "url" : "/assets/server/zipdatapacks.php", 6 | "method" : "POST", 7 | "bodyPatterns" : [ { 8 | "equalTo" : "packs=%7B%22survival%22%3A%5B%22armor+statues%22%5D%7D&version=1.19", 9 | "caseInsensitive" : false 10 | } ] 11 | }, 12 | "response" : { 13 | "status" : 200, 14 | "body" : "{\"status\":\"success\",\"link\":\"\\/download\\/VanillaTweaks_d952917_UNZIP_ME.zip\"}", 15 | "headers" : { 16 | "Date" : "Thu, 20 Apr 2023 00:58:39 GMT", 17 | "Content-Type" : "text/html; charset=UTF-8" 18 | } 19 | }, 20 | "uuid" : "92d91d61-e30a-4313-86c7-c84d97fe656f", 21 | "persistent" : false, 22 | "insertionIndex" : 24 23 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/assets_server_zipresourcepacksphp-c8bbd8c7-7417-4bc5-8452-8c9ba68d766b.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "c8bbd8c7-7417-4bc5-8452-8c9ba68d766b", 3 | "name" : "assets_server_zipresourcepacksphp", 4 | "request" : { 5 | "url" : "/assets/server/zipresourcepacks.php", 6 | "method" : "POST", 7 | "bodyPatterns" : [ { 8 | "equalTo" : "packs=%7B%22aesthetic%22%3A%5B%22AlternateBlockDestruction%22%5D%7D&version=1.19", 9 | "caseInsensitive" : false 10 | } ] 11 | }, 12 | "response" : { 13 | "status" : 200, 14 | "body" : "{\"status\":\"success\",\"link\":\"\\/download\\/VanillaTweaks_r201938.zip\"}", 15 | "headers" : { 16 | "Date" : "Thu, 20 Apr 2023 00:58:39 GMT", 17 | "Content-Type" : "text/html; charset=UTF-8" 18 | } 19 | }, 20 | "uuid" : "c8bbd8c7-7417-4bc5-8452-8c9ba68d766b", 21 | "persistent" : false, 22 | "insertionIndex" : 25 23 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/download_vanillatweaks_c826799zip-4e6d2f20-a904-44db-83da-dea1e3f32b9f.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "4e6d2f20-a904-44db-83da-dea1e3f32b9f", 3 | "name" : "download_vanillatweaks_c826799zip", 4 | "request" : { 5 | "url" : "/download/VanillaTweaks_c826799.zip", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "body" : "sgMq8u", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:40 GMT", 13 | "Content-Type" : "application/zip", 14 | "Last-Modified" : "Thu, 20 Apr 2023 00:58:40 GMT", 15 | "ETag" : "W/\"162dd-5f9ba0a934514\"", 16 | "Cache-Control" : "max-age=1800" 17 | } 18 | }, 19 | "uuid" : "4e6d2f20-a904-44db-83da-dea1e3f32b9f", 20 | "persistent" : false, 21 | "insertionIndex" : 29 22 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/download_vanillatweaks_d952917_unzip_mezip-d6ff5215-fe9c-446c-b8dc-0f01a3b8226f.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "d6ff5215-fe9c-446c-b8dc-0f01a3b8226f", 3 | "name" : "download_vanillatweaks_d952917_unzip_mezip", 4 | "request" : { 5 | "url" : "/download/VanillaTweaks_d952917_UNZIP_ME.zip", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "base64Body" : "UEsDBBQAAAAAAEB8llbjzDGkBgAAAAYAAAAMAAAAZGF0YXBhY2sudHh0ckc0SlJtUEsBAhQAFAAAAAAAQHyWVuPMMaQGAAAABgAAAAwAAAAAAAAAAQAgAAAAAAAAAGRhdGFwYWNrLnR4dFBLBQYAAAAAAQABADoAAAAwAAAAAAA=", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:40 GMT", 13 | "Content-Type" : "application/zip", 14 | "Last-Modified" : "Thu, 20 Apr 2023 00:58:39 GMT", 15 | "ETag" : "W/\"b9ad-5f9ba0a8e72b1\"", 16 | "Cache-Control" : "max-age=1800" 17 | } 18 | }, 19 | "uuid" : "d6ff5215-fe9c-446c-b8dc-0f01a3b8226f", 20 | "persistent" : false, 21 | "insertionIndex" : 26 22 | } -------------------------------------------------------------------------------- /src/test/resources/vanillatweaks/mappings/download_vanillatweaks_r201938zip-5467e5f1-f733-4946-9ca3-00253d66b80a.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "5467e5f1-f733-4946-9ca3-00253d66b80a", 3 | "name" : "download_vanillatweaks_r201938zip", 4 | "request" : { 5 | "url" : "/download/VanillaTweaks_r201938.zip", 6 | "method" : "GET" 7 | }, 8 | "response" : { 9 | "status" : 200, 10 | "body" : "4mwbtQ", 11 | "headers" : { 12 | "Date" : "Thu, 20 Apr 2023 00:58:40 GMT", 13 | "Content-Type" : "application/zip", 14 | "Last-Modified" : "Thu, 20 Apr 2023 00:58:39 GMT", 15 | "ETag" : "W/\"2377-5f9ba0a8eefb1\"", 16 | "Cache-Control" : "max-age=1800" 17 | } 18 | }, 19 | "uuid" : "5467e5f1-f733-4946-9ca3-00253d66b80a", 20 | "persistent" : false, 21 | "insertionIndex" : 27 22 | } --------------------------------------------------------------------------------