Repository navigation
Conversation
This was referenced Oct 22, 2026
This was referenced Oct 22, 2026
added 4 commits
October 22, 2026 12:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GuildJsonProvider,ArenaJsonProviderandChallengeJsonProviderall took the name of each file, minus its extension, as the id of a guild, an arena or a challenge. Nothing checked the extension, and nothing checked that the entry was a file at all.JsonFileUtils.writeAtomicallywrites to a temporary file next to its target and moves it into place afterwards. A server that died in between left that temporary file in the folder, and the file was loaded as a guild. The same went for any other file an operator put in the folder:guildExistsmatched the id against every file name, so a file callednotes.txtmade the plugin believe a guild callednotesalready existed and blocked its creation, andgetGuildreturned whatever was in that file. A folder in the data folder was handed to the file reader..json. That is what the writers produce:createGuildandupdateGuildwrite the id and.json, and the arena and challenge providers do the same. Everything else in a data folder is ignored, and ignored files are left where they are. They belong to whoever put them there.IOExceptionnaming it. The arena storage deletes in a batch, so a failure there is reported and the batch carries on with the rest.Filearguments, so a caller can hand them over the wrong way round without the compiler noticing, and the copies of the files the plugin cannot read would then land inside the folder the plugin enumerates. A wrong pair is reported at startup, with both folders named.plugins/Guilds/backups/corrupt/<timestamp>/<data-type>/, it is byte for byte, and the original stays exactly where the operator put it. The backup folder sits beside the four data folders rather than inside one, so a backup is never enumerated as a record.cooldowns.jsonis the one data file that holds a list rather than a single record, so one broken entry used to cost every entry in it: the file could not be parsed, no cooldown was loaded, and the plugin logged one error that did not say which entry was at fault. The entries are read one at a time now. A broken entry is logged with the position it sat at, the file is copied aside, and the entries around it still load.IOExceptionnaming the folder, instead of an empty list of records that looks like a server with no guilds.Relation to #850
#850(fix: refuse to start when the guild data cannot be read) rewrites the same provider and adds a test file at the same path, so the two pull requests conflict when they are merged one after the other. They are meant to stay independent: neither of them is rebased onto the other, and both targetmasteron their own.The two changes fit together:
#850makes the guild storage refuse to write to or delete a guild file it could not read when the guilds were loaded, and it stops the plugin from starting on a guild data folder it cannot read..jsonfiles in the folder, skip a record that parses but has no id, keep the files it could not read out of the save and delete paths, and copy them aside.Merging both means keeping the
.jsonfiltering and the enumeration helper (JsonFileUtils.listDataFiles) from this pull request, together with the unreadable-file protection from#850. The protection is in this pull request as well, under the names#850uses (unreadableFilesandrefuseToOverwriteUnreadableFile), so the two versions of it are one mechanism rather than two, and a record without an id is protected here in the same way a file that does not parse is.The conflict in
GuildJsonProvider.javais in the enumeration lines, the log messages and the javadoc of the shared field. The field itself, the refusal method and the two lines that add a file to it merge on their own. The conflict in the test file is an add/add one: both branches addGuildJsonProviderTestat the same path, and they cover different things (this one filters the folder and backs up what it cannot read,#850's protects a file that could not be read and fails the load when nothing can be read), so the merged file needs the test methods of both. InGuildAdapter.javathe line that builds the provider is the same on both sides now, so only the comment above it is in the way.The two branches also warn about the two folders being the wrong way round, in their own words. The warning here lives in
JsonFileUtilsand is called by all four providers, so the merged provider should call that one and drop the private copy, and the wording has to satisfy both tests: it says that the data folder "is not a folder inside the plugin's data folder" and that the two folders "may have been given the wrong way round".One detail to carry over when the two are merged:
JsonFileUtils.listDataFilesthrows anIOExceptionwhen the folder cannot be listed, so the behaviour#850gives an unreadable folder survives the merge.The backup itself comes from
CorruptDataBackup, which is committed to this branch, to#850and to the vault branch with the same bytes, so the three pull requests do not conflict on it.Cooldown backup failure
When a cooldown list had an unreadable entry, the provider kept the entry in memory and continued to allow create or delete operations. If creating its backup failed, those operations could still rewrite
cooldowns.jsonwithout a verified copy of the original.The provider now stops a mutation unless a byte-for-byte backup exists. If backup creation or verification fails, cooldown create and delete throw
IOExceptionbefore writing. The JSON list format and configuration stay unchanged.Two regression tests make the backup path unavailable, try a create or delete against a partially corrupted list, and assert that the operation fails while the original file remains byte for byte unchanged.
Follow-up checks:
./gradlew test --tests 'me.glaremasters.guilds.database.cooldowns.provider.CooldownJsonProviderTest' testJava11 spotlessCheckpassed../gradlew checkpassed, including the 1.8.8 Material probe../gradlew plugwrightTestpassed: 144 passed, 0 failed, 0 skipped on Paper 1.21.8.Validation
./gradlew test testJava11 spotlessCheck— passed. 149 tests, 0 failures, on Java 21 and on Java 11../gradlew check— passed, including the 1.8.8 Material probe (Legacy API probe: 2 Material constant(s) checked against 1.8.8, 0 allow-listed)../gradlew plugwrightTest— passed on 1.21.8 with the JSON backend: 144 passed, 0 failed, 0 skipped.GuildJsonProviderTest,ArenaJsonProviderTest,ChallengeJsonProviderTestandCooldownJsonProviderTestundersrc/test/java/me/glaremasters/guilds/database/, withJsonStorageBackupTestcovering the backups across all four storages. Each provider test puts a valid record, a leftover.tmpfile, a.txtfile, a file without an extension and a folder into a temp folder, then checks that exactly one id and one record come out, thatexistsis true for the real one only, thatgetreturns the real one, and that the ignored files are still on disk afterwards. They also cover a file that cannot be parsed, a record without an id, the folder in the data folder, and that a file the plugin could not read is neither written over nor deleted.plugins/Guilds/backups/corrupt/<timestamp>/<data-type>/with the file name kept, two corrupt files with the same name get a copy each, a backup folder that cannot be written is reported and the original is left alone, and reading the same corruption twice, or again after a restart, leaves one copy behind.getAllGuildIdsreturned[README, 11111111-1111-1111-1111-111111111111.json1234567890, 11111111-1111-1111-1111-111111111111, backup, notes],getAllGuildsreturned two guilds,guildExists("notes")was true, andgetGuild("notes")tried to parsenotes.txtas JSON.Compatibility
<id>.json,cooldowns.jsonis still a JSON list of cooldown objects, and the id is still the file name without its extension. The backups are new files in a new folder.src/mainare unchanged, and the 1.8.8 probe still passes.java.ioandjava.nio.fileonly, plus the gson tree API thatUpdateCheckeralready uses.Limitations
GUILD.JSONis read as a record.