diff --git a/src/pocketmine/level/format/io/region/CorruptedRegionException.php b/src/pocketmine/level/format/io/region/CorruptedRegionException.php new file mode 100644 index 000000000..7749a3f5e --- /dev/null +++ b/src/pocketmine/level/format/io/region/CorruptedRegionException.php @@ -0,0 +1,28 @@ +regions[$index = Level::chunkHash($x, $z)])){ $this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION); + try{ + $this->regions[$index]->open(); + }catch(CorruptedRegionException $e){ + $logger = $this->level->getServer()->getLogger(); + $logger->error("Corrupted region file detected: " . $e->getMessage()); + + $this->regions[$index]->close(false); //Do not write anything to the file + + $path = $this->regions[$index]->getFilePath(); + $backupPath = $path . ".bak." . time(); + rename($path, $backupPath); + $logger->error("Corrupted region file has been backed up to " . $backupPath); + + $this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION); + $this->regions[$index]->open(); //this will create a new empty region to replace the corrupted one + } } } diff --git a/src/pocketmine/level/format/io/region/RegionException.php b/src/pocketmine/level/format/io/region/RegionException.php new file mode 100644 index 000000000..d0238daac --- /dev/null +++ b/src/pocketmine/level/format/io/region/RegionException.php @@ -0,0 +1,28 @@ +z = $regionZ; $this->levelProvider = $level; $this->filePath = $this->levelProvider->getPath() . "region/r.$regionX.$regionZ.$fileExtension"; + } + + public function open(){ $exists = file_exists($this->filePath); if(!$exists){ touch($this->filePath); + }else{ + $fileSize = filesize($this->filePath); + if($fileSize > self::MAX_REGION_FILE_SIZE){ + throw new CorruptedRegionException("Corrupted oversized region file found, should be a maximum of " . self::MAX_REGION_FILE_SIZE . " bytes, got " . $fileSize . " bytes"); + }elseif($fileSize % 4096 !== 0){ + throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB"); + } } + $this->filePointer = fopen($this->filePath, "r+b"); stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB @@ -170,9 +186,20 @@ class RegionLoader{ return $x + ($z << 5); } - public function close(){ - $this->writeLocationTable(); - fclose($this->filePointer); + /** + * Writes the region header and closes the file + * + * @param bool $writeHeader + */ + public function close(bool $writeHeader = true){ + if(is_resource($this->filePointer)){ + if($writeHeader){ + $this->writeLocationTable(); + } + + fclose($this->filePointer); + } + $this->levelProvider = null; } @@ -255,14 +282,34 @@ class RegionLoader{ fseek($this->filePointer, 0); $this->lastSector = 1; - $data = unpack("N*", fread($this->filePointer, 4 * 1024 * 2)); //1024 records * 4 bytes * 2 times + $headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH); + if(($len = strlen($headerRaw)) !== self::REGION_HEADER_LENGTH){ + throw new CorruptedRegionException("Invalid region file header, expected " . self::REGION_HEADER_LENGTH . " bytes, got " . $len . " bytes"); + } + + $data = unpack("N*", $headerRaw); + $usedOffsets = []; for($i = 0; $i < 1024; ++$i){ $index = $data[$i + 1]; + $offset = $index >> 8; + if($offset !== 0){ + fseek($this->filePointer, ($offset << 12)); + if(fgetc($this->filePointer) === false){ //Try and read from the location + throw new CorruptedRegionException("Region file location offset points to invalid location"); + }elseif(isset($usedOffsets[$offset])){ + throw new CorruptedRegionException("Found two chunk offsets pointing to the same location"); + }else{ + $usedOffsets[$offset] = true; + } + } + $this->locationTable[$i] = [$index >> 8, $index & 0xff, $data[1024 + $i + 1]]; if(($this->locationTable[$i][0] + $this->locationTable[$i][1] - 1) > $this->lastSector){ $this->lastSector = $this->locationTable[$i][0] + $this->locationTable[$i][1] - 1; } } + + fseek($this->filePointer, 0); } private function writeLocationTable(){ @@ -300,4 +347,7 @@ class RegionLoader{ return $this->z; } + public function getFilePath() : string{ + return $this->filePath; + } }