diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c4300a..dd78388 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1.0) -project(nbteditor VERSION 1.1) -set (NBTEDITOR_VERSION 1.1) +project(nbteditor VERSION 1.2) +set (NBTEDITOR_VERSION 1.2) find_package(Qt5Widgets) diff --git a/src/File/ByteBuffer.cpp b/src/File/ByteBuffer.cpp index dfc2581..193b8c6 100644 --- a/src/File/ByteBuffer.cpp +++ b/src/File/ByteBuffer.cpp @@ -27,6 +27,14 @@ namespace File { return (((unsigned short) reader->ReadByte()) << 8) | reader->ReadByte(); } + jint ByteBuffer::ReadThreeBytesInt() { + Byte* bytes = ReadBytes(3); + jint number = ((bytes[0] & 0x0F) << 16) | ((bytes[1] & 0xFF) << 8) | ((bytes[2] & 0xFF)); + + delete[] bytes; + return number; + } + jint ByteBuffer::ReadInt() { Byte* bytes = ReadBytes(4); std::reverse(bytes, bytes + 4); // Big-Endian to Little-Endian diff --git a/src/File/ByteBuffer.h b/src/File/ByteBuffer.h index 8ffce83..c8b093d 100644 --- a/src/File/ByteBuffer.h +++ b/src/File/ByteBuffer.h @@ -15,6 +15,7 @@ namespace File { jbyte ReadSignedByte(); jshort ReadShort(); unsigned short ReadUShort(); + jint ReadThreeBytesInt(); // Used in minecraft level format jint ReadInt(); jlong ReadLong(); jdouble ReadDouble(); diff --git a/src/File/GzipByteReader.cpp b/src/File/GzipByteReader.cpp index 5c7cf27..eed7d3c 100644 --- a/src/File/GzipByteReader.cpp +++ b/src/File/GzipByteReader.cpp @@ -7,7 +7,7 @@ #include namespace File { - GzipByteReader::GzipByteReader(Byte* data, uint dataLength) : finishedRead(false), bufferLength(0), offset(0), bufferOffset(0) { + GzipByteReader::GzipByteReader(Byte* data, uint dataLength, bool gzip) : gzip(gzip), finishedRead(false), bufferLength(0), offset(0), bufferOffset(0) { memset(&stream, 0, sizeof stream); stream.zalloc = (alloc_func)0; stream.zfree = (free_func)0; @@ -18,7 +18,8 @@ namespace File { buffer = new Byte[GZIP_BUFFER_SIZE]; - int result = inflateInit2(&stream, MAX_WBITS | 16); + int zlibFlags = (gzip ? (MAX_WBITS | 16) : MAX_WBITS); + int result = inflateInit2(&stream, zlibFlags); if (result != 0) throw Exception::GzipException("ZLIB init failed: " + std::to_string(result)); FillNextBuffer(); diff --git a/src/File/GzipByteReader.h b/src/File/GzipByteReader.h index 4f4a2db..ea6ea74 100644 --- a/src/File/GzipByteReader.h +++ b/src/File/GzipByteReader.h @@ -5,7 +5,7 @@ namespace File { class GzipByteReader : public ByteReader { public: - GzipByteReader(Byte* data, uint dataLength); + GzipByteReader(Byte* data, uint dataLength, bool gzip = true); ~GzipByteReader(); Byte ReadByte(); Byte* ReadBytes(uint length); @@ -15,10 +15,9 @@ namespace File { void FillNextBuffer(); Byte* GetBuffer() { return buffer; } - protected: -// void FillNextBuffer(); - private: + bool gzip; + z_stream stream; bool finishedRead; diff --git a/src/File/GzipByteWriter.cpp b/src/File/GzipByteWriter.cpp index 7813791..b8e96cc 100644 --- a/src/File/GzipByteWriter.cpp +++ b/src/File/GzipByteWriter.cpp @@ -7,7 +7,7 @@ namespace File { - GzipByteWriter::GzipByteWriter(ByteWriter* parentWriter) : bufferLength(0), bufferOffset(0), parentWriter(parentWriter) { + GzipByteWriter::GzipByteWriter(ByteWriter* parentWriter, bool gzip) : gzip(gzip), bufferLength(0), bufferOffset(0), parentWriter(parentWriter) { memset(&stream, 0, sizeof stream); stream.zalloc = (alloc_func)0; stream.zfree = (free_func)0; @@ -19,7 +19,8 @@ namespace File { stream.avail_in = bufferOffset; stream.next_in = buffer; - int result = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY); + int zlibFlags = (gzip ? (MAX_WBITS | 16) : MAX_WBITS); + int result = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, zlibFlags, 8, Z_DEFAULT_STRATEGY); if (result != 0) throw Exception::GzipException("ZLIB deflate init failed: " + std::to_string(result)); } diff --git a/src/File/GzipByteWriter.h b/src/File/GzipByteWriter.h index c0f3929..ac84398 100644 --- a/src/File/GzipByteWriter.h +++ b/src/File/GzipByteWriter.h @@ -5,7 +5,7 @@ namespace File { class GzipByteWriter : public ByteWriter { public: - GzipByteWriter(ByteWriter* parentWriter); + GzipByteWriter(ByteWriter* parentWriter, bool gzip = true); ~GzipByteWriter(); void WriteByte(const Byte byte); @@ -19,6 +19,7 @@ namespace File { void DoGzipCompression(int mode, int expectedResult); private: + bool gzip; z_stream stream; Byte* buffer; diff --git a/src/File/MemoryByteWriter.cpp b/src/File/MemoryByteWriter.cpp new file mode 100644 index 0000000..dc177a5 --- /dev/null +++ b/src/File/MemoryByteWriter.cpp @@ -0,0 +1,62 @@ +#include "MemoryByteWriter.h" +#include "Exception/StreamOverflowException.h" +#include +#include + +namespace File { + + MemoryByteWriter::MemoryByteWriter() : bufferOffset(0) { + buffer = new Byte[WRITE_BUFFER_SIZE]; + bufferSize = WRITE_BUFFER_SIZE; + } + + MemoryByteWriter::~MemoryByteWriter() { + delete[] buffer; + bufferOffset = 0; + bufferSize = 0; + } + + void MemoryByteWriter::WriteByte(const Byte byte) { + if (bufferOffset >= bufferSize) + IncreaseBufferSize(); + + buffer[bufferOffset++] = byte; + } + + void MemoryByteWriter::WriteBytes(const Byte* bytes, uint length) { + uint writtenBytes = 0; + while (writtenBytes < length) { + if (bufferOffset >= bufferSize) + IncreaseBufferSize(); + + uint bytesToWrite = std::min(bufferSize - bufferOffset, length - writtenBytes); + std::memcpy(buffer + bufferOffset, bytes + writtenBytes, bytesToWrite); + + bufferOffset += bytesToWrite; + writtenBytes += bytesToWrite; + } + } + + NBT::NBTArray MemoryByteWriter::GetByteArray() { + Byte* resultArray = new Byte[bufferOffset]; + std::memcpy(resultArray, buffer, bufferOffset); + + NBT::NBTArray returnArray((int)bufferOffset, resultArray); + return returnArray; + } + + void MemoryByteWriter::SetBufferByte(uint offset, const Byte byte) { + if (offset >= bufferSize) + throw Exception::StreamOverflowException(offset, 1, bufferSize); + buffer[offset] = byte; + } + + void MemoryByteWriter::IncreaseBufferSize() { + int newSize = bufferSize * 2; + Byte* newBuffer = new Byte[newSize]; + std::memcpy(newBuffer, buffer, bufferSize); + + buffer = newBuffer; + bufferSize = newSize; + } +} \ No newline at end of file diff --git a/src/File/MemoryByteWriter.h b/src/File/MemoryByteWriter.h index 8cf0ebe..09b97d7 100644 --- a/src/File/MemoryByteWriter.h +++ b/src/File/MemoryByteWriter.h @@ -1,5 +1,7 @@ #pragma once +#include "Globals.h" #include "ByteWriter.h" +#include "NBT/NBTEntry.h" namespace File { class MemoryByteWriter : public ByteWriter { @@ -9,11 +11,19 @@ namespace File { void WriteByte(const Byte byte); void WriteBytes(const Byte* bytes, uint length); - void Flush(); + void Flush() {} + + NBT::NBTArray GetByteArray(); + uint GetOffset() { return bufferOffset; } + + void SetBufferByte(uint offset, const Byte byte); + + protected: + void IncreaseBufferSize(); private: - Byte* data; - uint dataLength; - uint dataOffset; + Byte* buffer; + uint bufferOffset; + uint bufferSize; }; } \ No newline at end of file diff --git a/src/File/WriteBuffer.cpp b/src/File/WriteBuffer.cpp index 8a1dd7f..9100dfa 100644 --- a/src/File/WriteBuffer.cpp +++ b/src/File/WriteBuffer.cpp @@ -24,6 +24,12 @@ namespace File { WriteByte(val & 0xFF); } + void WriteBuffer::WriteThreeBytesInt(jint val) { + WriteByte((val >> 16) & 0x0F); + WriteByte((val >> 8) & 0xFF); + WriteByte(val & 0xFF); + } + void WriteBuffer::WriteInt(jint val) { Byte bytes[4]; memcpy(bytes, &val, 4); diff --git a/src/File/WriteBuffer.h b/src/File/WriteBuffer.h index adb8a1f..c754e7a 100644 --- a/src/File/WriteBuffer.h +++ b/src/File/WriteBuffer.h @@ -14,6 +14,7 @@ namespace File { void WriteSignedByte(jbyte val); void WriteShort(jshort val); + void WriteThreeBytesInt(jint val); void WriteInt(jint val); void WriteLong(jlong val); void WriteDouble(jdouble val); diff --git a/src/Globals.h b/src/Globals.h index 22f0f3b..63e8562 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -8,6 +8,7 @@ static_assert(sizeof(double) == 8, "Assuming that a double is 8 byte long"); typedef unsigned char Byte; typedef unsigned int uint; const unsigned int GZIP_BUFFER_SIZE = 128; +const unsigned int WRITE_BUFFER_SIZE = 256; const unsigned int MIN_TAG = 1; const unsigned int MAX_TAG = 11; diff --git a/src/NBT/NBTCompound.cpp b/src/NBT/NBTCompound.cpp index 2ffbda9..47ca9f8 100644 --- a/src/NBT/NBTCompound.cpp +++ b/src/NBT/NBTCompound.cpp @@ -7,8 +7,18 @@ namespace NBT { + NBTCompound::NBTCompound() : noFree(false) { + } + + NBTCompound::NBTCompound(NBTCompound* oldCompound) : noFree(true) { + for (auto& entry : oldCompound->entries) { + entries.push_back(entry); + } + } + NBTCompound::~NBTCompound() { - Free(); + if (!noFree) + Free(); } void NBTCompound::Free() { diff --git a/src/NBT/NBTCompound.h b/src/NBT/NBTCompound.h index b335144..2f1e49b 100644 --- a/src/NBT/NBTCompound.h +++ b/src/NBT/NBTCompound.h @@ -2,7 +2,6 @@ #include "NBTEntry.h" #include "Globals.h" #include -#include namespace File { class ByteBuffer; @@ -14,7 +13,8 @@ namespace NBT { class NBTCompound { public: - NBTCompound() {} + NBTCompound(); + NBTCompound(NBTCompound* oldCompound); // Create a copy ~NBTCompound(); void Free(); @@ -29,7 +29,7 @@ namespace NBT { bool RemoveEntry(NBTEntry* entry); private: + bool noFree; std::vector entries; - static const NBTTag** tagsByType; }; } \ No newline at end of file diff --git a/src/NBT/NBTEntry.h b/src/NBT/NBTEntry.h index 3591888..8f2db32 100644 --- a/src/NBT/NBTEntry.h +++ b/src/NBT/NBTEntry.h @@ -1,8 +1,8 @@ #pragma once #include #include "NBTType.h" -#include #include +#include "Globals.h" namespace NBT { @@ -41,4 +41,12 @@ namespace NBT { type = NbtEnd; } }; + + struct RegionChunkInformation { + int relX; + int relZ; + int offset; + int lastChange; + Byte roundedSize; + }; } \ No newline at end of file diff --git a/src/NBT/NBTReader.cpp b/src/NBT/NBTReader.cpp index d05b3fa..e2a425b 100644 --- a/src/NBT/NBTReader.cpp +++ b/src/NBT/NBTReader.cpp @@ -3,11 +3,14 @@ #include "File/ByteBuffer.h" #include "File/GzipByteReader.h" #include "File/MemoryByteReader.h" +#include "File/MemoryByteWriter.h" #include "File/WriteBuffer.h" #include "File/GzipByteWriter.h" #include "File/FileByteWriter.h" #include "Exception/NBTException.h" #include "Exception/FileException.h" +#include "Exception/StreamOverflowException.h" +#include "NBTHelper.h" #include #include @@ -106,4 +109,172 @@ namespace NBT { delete fileWriter; } + NBTCompound* NBTReader::LoadRegionFile(const char* filePath) { + ifstream ifs(filePath, ios::binary | ios::ate); + if (!ifs) + throw Exception::FileException(strerror(errno), filePath, errno); + + uint length = ifs.tellg(); + ifs.seekg(0, ios::beg); + + // Need to be in heap because the stack is too small + Byte* bytes = new Byte[length]; + ifs.read((char*)bytes, length); + ifs.close(); + + NBTCompound* compound = LoadRegionData(bytes, length); + delete[] bytes; + return compound; + } + + NBTCompound* NBTReader::LoadRegionData(Byte* data, uint length) { + File::MemoryByteReader reader(data, length); + File::ByteBuffer buffer(&reader); + + RegionChunkInformation chunks[1024]; + for (int i = 0; i < 1024; i++) { + jint offset = buffer.ReadThreeBytesInt(); + chunks[i].roundedSize = buffer.ReadByte(); + chunks[i].relX = i % 32; + chunks[i].relZ = (int) (i / 32.0); + chunks[i].offset = offset; + } + + // Read chunk last modified dates + for (int i = 0; i < 1024; i++) { + chunks[i].lastChange = buffer.ReadInt(); + } + + NBTCompound* rootCompound = new NBTCompound(); + + // Read chunk data + for (int i = 0; i < 1024; i++) { + RegionChunkInformation chunkInfo = chunks[i]; + if (chunkInfo.offset == 0) + continue; + + uint offset = (chunkInfo.offset - 2) * 4096 + 8192; + if (offset + 3 >= length) + throw Exception::StreamOverflowException(offset, 3, length); + + uint chunkSize = (((data[offset] & 0x0F) << 24) | ((data[offset + 1] & 0xFF) << 16) | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF)) - 1; + if (offset + 5 + chunkSize >= length) + throw Exception::StreamOverflowException(offset + 5, chunkSize, length); + + Byte compressionFormat = data[offset + 4]; + if (compressionFormat != 2) + throw Exception::Exception(QString("Chunk %1,%2 has unknown compression format %3, only 2 (zlib) is supported.").arg(chunkInfo.relX).arg(chunkInfo.relZ).arg(compressionFormat).toStdString()); + + File::GzipByteReader chunkReader(data + offset + 5, chunkSize, false); + File::ByteBuffer chunkBuffer(&chunkReader); + NBTCompound* compound = LoadFromUncompressedStream(&chunkBuffer); + + if (compound->FindName("LastChange") == NULL) { + NBTEntry* lastChangeEntry = new NBTEntry("LastChange", NbtInt); + lastChangeEntry->value = new int(chunkInfo.lastChange); + compound->AddEntry(lastChangeEntry); + } + + NBTEntry* entry = new NBTEntry(QString::number(chunkInfo.relX) + ", " + QString::number(chunkInfo.relZ), NbtCompound); + entry->value = compound; + rootCompound->AddEntry(entry); + } + + return rootCompound; + } + + void NBTReader::SaveRegionToFile(const char* filePath, NBTCompound* compound) { + File::FileByteWriter* fileWriter = NULL; + + Byte fillBytes[4096]; + std::fill(fillBytes, fillBytes + 4096, 0); + + try { + File::MemoryByteWriter dataWriter; + RegionChunkInformation chunks[1024]; + + for (int i = 0; i < 1024; i++) { + uint offset = dataWriter.GetOffset(); + int relX = i % 32; + int relZ = (int) (i / 32.0); + + NBTEntry* chunkEntry = compound->FindName(QString::number(relX) + ", " + QString::number(relZ)); + NBTCompound* chunkCompound = (chunkEntry == NULL ? NULL : NBTHelper::GetCompound(*chunkEntry)); + if (chunkCompound == NULL || chunkCompound->GetEntries().empty()) { + chunks[i].lastChange = 0; + chunks[i].offset = 0; + chunks[i].roundedSize = 0; + continue; + } + + // Needed for removing of the LastChange entry + NBTCompound chunkCompoundCopy(chunkCompound); + + NBTEntry* lastChange = chunkCompound->FindName("LastChange"); + if (lastChange != NULL) { + chunks[i].lastChange = NBTHelper::GetInt(*lastChange); + chunkCompoundCopy.RemoveEntry(lastChange); + } else { + chunks[i].lastChange = 0; + } + + // Write placeholder for chunk size and compression format + dataWriter.WriteBytes(fillBytes, 4); + dataWriter.WriteByte(2); // Zlib compression + + // Write chunk nbt data + { + File::GzipByteWriter gzipWriter(&dataWriter, false); + File::WriteBuffer buffer(&gzipWriter); + + buffer.WriteByte(NbtCompound); + buffer.WriteString(""); + chunkCompoundCopy.Write(&buffer); + gzipWriter.Finish(); + } + + // Override pre-written placeholder with the correct chunk size + { + uint newOffset = dataWriter.GetOffset(); + int chunkSize = (newOffset - offset) - 4; + int kilobyteSize = (newOffset - offset) / 4096 + 1; + + dataWriter.SetBufferByte(offset, (chunkSize >> 24) & 0x0F); + dataWriter.SetBufferByte(offset + 1, (chunkSize >> 16) & 0xFF); + dataWriter.SetBufferByte(offset + 2, (chunkSize >> 8) & 0xFF); + dataWriter.SetBufferByte(offset + 3, chunkSize & 0xFF); + + chunks[i].roundedSize = kilobyteSize; + chunks[i].offset = offset / 4096 + 2; + + // Fill up to 4096 bytes + int rest = newOffset % 4096; + if (rest != 0) + dataWriter.WriteBytes(fillBytes, 4096 - rest); + } + } + + fileWriter = new File::FileByteWriter(std::string(filePath)); + File::WriteBuffer buffer(fileWriter); + + for (int i = 0; i < 1024; i++) { + RegionChunkInformation chunkInfo = chunks[i]; + buffer.WriteThreeBytesInt(chunkInfo.offset); + buffer.WriteByte(chunkInfo.roundedSize); + } + for (int i = 0; i < 1024; i++) + buffer.WriteInt(chunks[i].lastChange); + + // Add dataWriter data to file stream + NBT::NBTArray chunkBytes = dataWriter.GetByteArray(); + buffer.WriteBytes(chunkBytes.array, chunkBytes.length); + } catch (const Exception::Exception& ex) { + if (fileWriter != NULL) + delete fileWriter; + throw ex; + } + + delete fileWriter; + } + } \ No newline at end of file diff --git a/src/NBT/NBTReader.h b/src/NBT/NBTReader.h index dcdc375..cbcf971 100644 --- a/src/NBT/NBTReader.h +++ b/src/NBT/NBTReader.h @@ -9,5 +9,10 @@ namespace NBT { static void SaveToFile(const char* filePath, NBTCompound* compound); static void SaveToFileUncompressed(const char* filePath, NBTCompound* compound); + + static NBTCompound* LoadRegionFile(const char* filePath); + static NBTCompound* LoadRegionData(Byte* data, uint length); + + static void SaveRegionToFile(const char* filePath, NBTCompound* compound); }; } \ No newline at end of file diff --git a/src/NBT/NBTType.h b/src/NBT/NBTType.h index 1303d26..c4b9c89 100644 --- a/src/NBT/NBTType.h +++ b/src/NBT/NBTType.h @@ -18,6 +18,7 @@ namespace NBT { enum NBTFileType { NbtGzipCompressed = 0, - NbtUncompressed = 1 + NbtUncompressed = 1, + NbtAnvilRegion = 2 }; } \ No newline at end of file diff --git a/src/UI/MainForm.cpp b/src/UI/MainForm.cpp index 6eed4b0..ec13d44 100644 --- a/src/UI/MainForm.cpp +++ b/src/UI/MainForm.cpp @@ -54,8 +54,16 @@ namespace UI { } void MainForm::loadFile(QString file) { + NBT::NBTFileType oldFileType = currentFileType; try { - NBT::NBTCompound* compound = NBT::NBTReader::LoadFromFile(file.toStdString().c_str(), ¤tFileType); + NBT::NBTCompound* compound; + if (file.endsWith(".mca")) { + compound = NBT::NBTReader::LoadRegionFile(file.toStdString().c_str()); + currentFileType = NBT::NbtAnvilRegion; + } else { + compound = NBT::NBTReader::LoadFromFile(file.toStdString().c_str(), ¤tFileType); + } + NBT::NBTEntry* rootEntry = new NBT::NBTEntry("", NBT::NbtCompound); rootEntry->value = compound; @@ -71,6 +79,7 @@ namespace UI { delete oldModel; disableSaving(); } catch (const Exception::Exception& ex) { + currentFileType = oldFileType; QMessageBox::critical(this, tr("Can't load file"), tr("The NBT file could not be loaded:\n%1").arg(QString(ex.what())), QMessageBox::Ok, QMessageBox::Ok); return; } @@ -89,6 +98,9 @@ namespace UI { case NBT::NbtGzipCompressed: NBT::NBTReader::SaveToFile(file.toStdString().c_str(), compound); break; + case NBT::NbtAnvilRegion: + NBT::NBTReader::SaveRegionToFile(file.toStdString().c_str(), compound); + break; } currentFile = file; diff --git a/src/UI/NBTTreeModel.h b/src/UI/NBTTreeModel.h index 7e726eb..a5183dd 100644 --- a/src/UI/NBTTreeModel.h +++ b/src/UI/NBTTreeModel.h @@ -16,7 +16,7 @@ namespace UI { public: NBTTreeModel(NBT::NBTEntry* rootEntry); - ~NBTTreeModel(); + virtual ~NBTTreeModel(); QVariant data(const QModelIndex& index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;