diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 8ad62af..6f42473 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -3,7 +3,7 @@ 4.0.0 ca.spottedleaf sectortool - 1.2-SNAPSHOT + 1.3-SNAPSHOT clean package diff --git a/pom.xml b/pom.xml index a9d9a21..c67f7a0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.spottedleaf sectortool - 1.2-SNAPSHOT + 1.3-SNAPSHOT UTF-8 diff --git a/src/main/java/ca/spottedleaf/io/region/SectorFile.java b/src/main/java/ca/spottedleaf/io/region/SectorFile.java index ecfc931..e94f927 100644 --- a/src/main/java/ca/spottedleaf/io/region/SectorFile.java +++ b/src/main/java/ca/spottedleaf/io/region/SectorFile.java @@ -236,8 +236,8 @@ public static boolean validate(final XXHash64 xxHash64, final ByteBuffer header, private static final int MAX_INTERNAL_ALLOCATION_BYTES = SECTOR_SIZE * (1 << SECTOR_LENGTH_BITS); - private static final int TYPE_HEADER_OFFSET_COUNT = SECTION_SIZE * SECTION_SIZE; // total number of offsets per type header - private static final int TYPE_HEADER_SECTORS = (TYPE_HEADER_OFFSET_COUNT * INT_SIZE) / SECTOR_SIZE; // total number of sectors used per type header + public static final int TYPE_HEADER_OFFSET_COUNT = SECTION_SIZE * SECTION_SIZE; // total number of offsets per type header + public static final int TYPE_HEADER_SECTORS = (TYPE_HEADER_OFFSET_COUNT * INT_SIZE) / SECTOR_SIZE; // total number of sectors used per type header // header location is just raw sector number // so, we point to the header itself to indicate absence @@ -329,9 +329,8 @@ public static long computeHash(final ByteBuffer buffer, final int offset) { private final Int2ObjectMap typeTranslationTable; private final FileHeader fileHeader = new FileHeader(); - private void checkReadOnlyHeader(final int type) { - // we want to error when a type is used which is not mapped, but we can only store into typeHeaders in write mode - // as sometimes we may need to create absent type headers + private void checkHeaderExists(final int type) { + // we want to error when a type is used which is not mapped if (this.typeTranslationTable.get(type) == null) { throw new IllegalArgumentException("Unknown type " + type); } @@ -381,44 +380,33 @@ public SectorFile(final File file, final int sectionX, final int sectionZ, this.readFileHeader(unscopedBufferChoices); } - boolean modifiedFileHeader = false; + // validate types + for (final IntIterator iterator = typeTranslationTable.keySet().iterator(); iterator.hasNext(); ) { + final int type = iterator.nextInt(); + + if (type < 0 || type >= MAX_TYPES) { + throw new IllegalStateException("Type translation table contains illegal type: " + type); + } + } + } + private TypeHeader createTypeHeader(final int type, final BufferChoices unscopedBufferChoices) throws IOException { try (final BufferChoices scopedBufferChoices = unscopedBufferChoices.scope()) { final ByteBuffer ioBuffer = scopedBufferChoices.t16k().acquireDirectBuffer(); + final int offset = this.sectorAllocator.allocate(TYPE_HEADER_SECTORS, false); // in sectors + if (offset <= 0) { + throw new IllegalStateException("Cannot allocate space for header " + this.debugType(type) + ":" + offset); + } - // make sure we have the type headers required allocated - for (final IntIterator iterator = typeTranslationTable.keySet().iterator(); iterator.hasNext(); ) { - final int type = iterator.nextInt(); - - if (type < 0 || type >= MAX_TYPES) { - throw new IllegalStateException("Type translation table contains illegal type: " + type); - } - - final TypeHeader headerData = this.typeHeaders.get(type); - if (headerData != null || readOnly) { - // allocated or unable to allocate - continue; - } - - modifiedFileHeader = true; + final TypeHeader ret = new TypeHeader(); - // need to allocate space for new type header - final int offset = this.sectorAllocator.allocate(TYPE_HEADER_SECTORS, false); // in sectors - if (offset <= 0) { - throw new IllegalStateException("Cannot allocate space for header " + this.debugType(type) + ":" + offset); - } + this.fileHeader.typeHeaderOffsets[type] = offset; + // hash will be computed by writeTypeHeader + this.typeHeaders.put(type, ret); - this.fileHeader.typeHeaderOffsets[type] = offset; - // hash will be computed by writeTypeHeader - this.typeHeaders.put(type, new TypeHeader()); + this.writeTypeHeader(ioBuffer, type, true, true); - this.writeTypeHeader(ioBuffer, type, true, false); - } - - // modified the file header, so write it back - if (modifiedFileHeader) { - this.writeFileHeader(ioBuffer); - } + return ret; } } @@ -700,6 +688,18 @@ class TentativeTypeHeader { final int type = entry.getIntKey(); final TentativeTypeHeader tentativeTypeHeader = entry.getValue(); + boolean hasData = false; + for (final int location : tentativeTypeHeader.typeHeader.locations) { + if (location != ABSENT_LOCATION) { + hasData = true; + break; + } + } + + if (!hasData) { + continue; + } + final int sectorOffset = newSectorAllocation.allocate(TYPE_HEADER_SECTORS, false); if (sectorOffset < 0) { throw new IllegalStateException("Failed to allocate type header"); @@ -717,10 +717,11 @@ class TentativeTypeHeader { boolean changes = false; - for (final Iterator> iterator = newHeaders.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { - final Int2ObjectMap.Entry entry = iterator.next(); + // make sure to use the tentative type headers, in case the tentative header was not allocated due to being empty + for (final Iterator> iterator = newTypeHeaders.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Int2ObjectMap.Entry entry = iterator.next(); final int type = entry.getIntKey(); - final TypeHeader newTypeHeader = entry.getValue(); + final TypeHeader newTypeHeader = entry.getValue().typeHeader; final TypeHeader oldTypeHeader = this.typeHeaders.get(type); boolean hasChanges; @@ -1075,7 +1076,7 @@ public boolean hasData(final int localX, final int localZ, final int type) { final TypeHeader typeHeader = this.typeHeaders.get(type); if (typeHeader == null) { - this.checkReadOnlyHeader(type); + this.checkHeaderExists(type); return false; } @@ -1123,7 +1124,7 @@ private SectorFileInput read(final BufferChoices scopedBufferChoices, final Byte final TypeHeader typeHeader = this.typeHeaders.get(type); if (typeHeader == null) { - this.checkReadOnlyHeader(type); + this.checkHeaderExists(type); return NULL_DATA; } @@ -1246,7 +1247,7 @@ public boolean delete(final BufferChoices unscopedBufferChoices, final int local final TypeHeader typeHeader = this.typeHeaders.get(type); if (typeHeader == null) { - this.checkReadOnlyHeader(type); + this.checkHeaderExists(type); return false; } @@ -1299,7 +1300,7 @@ public SectorFileOutput write(final BufferChoices scopedBufferChoices, final int throw new UnsupportedOperationException("Sectorfile is read-only"); } - if (this.typeHeaders.get(type) == null) { + if (!this.typeHeaders.containsKey(type) && !this.typeTranslationTable.containsKey(type)) { throw new IllegalArgumentException("Unknown type " + type); } @@ -1553,8 +1554,17 @@ protected ByteBuffer flush(final ByteBuffer current) throws IOException { return current; } + private void checkAndCreateTypeHeader() throws IOException { + if (SectorFile.this.typeHeaders.get(this.type) == null) { + SectorFile.this.createTypeHeader(this.type, this.scopedBufferChoices); + } + } + // assume flush() is called before this private void save() throws IOException { + // lazily create type header + this.checkAndCreateTypeHeader(); + if (this.externalFile == null) { // avoid clobbering buffer positions/limits final ByteBuffer buffer = this.buffer.duplicate(); diff --git a/src/main/java/ca/spottedleaf/sectortool/analyse/Analyse.java b/src/main/java/ca/spottedleaf/sectortool/analyse/Analyse.java index 65fe50c..72b979f 100644 --- a/src/main/java/ca/spottedleaf/sectortool/analyse/Analyse.java +++ b/src/main/java/ca/spottedleaf/sectortool/analyse/Analyse.java @@ -60,6 +60,7 @@ private static class StatsAccumulator { public long fileSectors = 0L; public long allocatedSectors = 0L; public long alternateAllocatedSectors = 0L; + public long alternateAllocatedSectorsPadded = 0L; public long dataSizeBytes = 0L; public long errors = 0L; @@ -67,6 +68,7 @@ public synchronized void accumulate(final RegionFile.AllocationStats stats) { this.fileSectors += stats.fileSectors(); this.allocatedSectors += stats.allocatedSectors(); this.alternateAllocatedSectors += stats.alternateAllocatedSectors(); + this.alternateAllocatedSectorsPadded += stats.alternateAllocatedSectorsPadded(); this.dataSizeBytes += stats.dataSizeBytes(); this.errors += (long)stats.errors(); } @@ -75,6 +77,7 @@ public void print() { System.out.println("File sectors: " + this.fileSectors); System.out.println("Allocated sectors: " + this.allocatedSectors); System.out.println("Alternate allocated sectors: " + this.alternateAllocatedSectors); + System.out.println("Alternate allocated sectors padded: " + this.alternateAllocatedSectorsPadded); System.out.println("Total data size: " + this.dataSizeBytes); System.out.println("Errors: " + this.errors); } @@ -133,7 +136,9 @@ public void run() { final RegionFile.AllocationStats stats; try { - stats = regionFile.computeStats(unscopedBufferChoices, SectorFile.SECTOR_SIZE, SectorFile.DataHeader.DATA_HEADER_LENGTH); + stats = regionFile.computeStats(unscopedBufferChoices, SectorFile.SECTOR_SIZE, + SectorFile.TYPE_HEADER_SECTORS + SectorFile.FileHeader.FILE_HEADER_TOTAL_SECTORS, + SectorFile.DataHeader.DATA_HEADER_LENGTH); } catch (final IOException ex) { synchronized (System.err) { System.err.println("Failed to read stats from regionfile '" + regionFile.file.getAbsolutePath() + "': "); diff --git a/src/main/java/ca/spottedleaf/sectortool/storage/RegionFile.java b/src/main/java/ca/spottedleaf/sectortool/storage/RegionFile.java index 8bcacf1..868a743 100644 --- a/src/main/java/ca/spottedleaf/sectortool/storage/RegionFile.java +++ b/src/main/java/ca/spottedleaf/sectortool/storage/RegionFile.java @@ -204,7 +204,7 @@ private File getExternalFile(final int x, final int z) { return new File(this.file.getParentFile(), "c." + cx + "." + cz + ".mcc"); } - public static record AllocationStats(long fileSectors, long allocatedSectors, long alternateAllocatedSectors, long dataSizeBytes, int errors) {} + public static record AllocationStats(long fileSectors, long allocatedSectors, long alternateAllocatedSectors, long alternateAllocatedSectorsPadded, long dataSizeBytes, int errors) {} private int[] getHeaderSorted() { final IntArrayList list = new IntArrayList(this.header.length); @@ -218,11 +218,12 @@ private int[] getHeaderSorted() { } public AllocationStats computeStats(final BufferChoices unscopedBufferChoices, final int alternateSectorSize, + final int alternateHeaderSectors, final int alternateOverhead) throws IOException { final long fileSectors = (this.file.length() + (SECTOR_SIZE - 1)) >> SECTOR_SHIFT; long allocatedSectors = Math.min(fileSectors, 2L); - long alternateAllocatedSectors = 0L; + long alternateAllocatedSectors = (long)alternateHeaderSectors; int errors = 0; long dataSize = 0L; @@ -272,7 +273,10 @@ public AllocationStats computeStats(final BufferChoices unscopedBufferChoices, f } } - return new AllocationStats(fileSectors, allocatedSectors, alternateAllocatedSectors, dataSize, errors); + final long diff = SECTOR_SIZE / alternateSectorSize; + final long alternateAllocatedSectorsPadded = diff <= 1L ? alternateAllocatedSectors : ((alternateAllocatedSectors + (diff - 1L)) / diff) * diff; + + return new AllocationStats(fileSectors, allocatedSectors, alternateAllocatedSectors, alternateAllocatedSectorsPadded, dataSize, errors); } public boolean read(final int x, final int z, final BufferChoices unscopedBufferChoices, final RegionFile.CustomByteArrayOutputStream decompressed) throws IOException { @@ -324,6 +328,7 @@ public int read(final int x, final int z, final BufferChoices unscopedBufferChoi final int length = compressedData.getInt(0) - BYTE_SIZE; byte type = compressedData.get(0 + INT_SIZE); compressedData.position(0 + INT_SIZE + BYTE_SIZE); + compressedData.limit(compressedData.getInt(0) + INT_SIZE); if (compressedData.remaining() < length) { throw new EOFException("Truncated data");