1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-03-03 13:39:40 +00:00

Support variable size strings in ESM3

This commit is contained in:
elsid 2023-02-12 17:03:01 +01:00
parent beb017e699
commit 80e6d6cbe3
No known key found for this signature in database
GPG key ID: 4DE04C198CBA7625
13 changed files with 92 additions and 74 deletions

View file

@ -111,7 +111,7 @@ namespace ESSImport
{ {
// used power // used power
esm.getSubHeader(); esm.getSubHeader();
std::string id = esm.getString(32); std::string id = esm.getMaybeFixedStringSize(32);
(void)id; (void)id;
// timestamp can't be used: this is the total hours passed, calculated by // timestamp can't be used: this is the total hours passed, calculated by
// timestamp = 24 * (365 * year + cumulativeDays[month] + day) // timestamp = 24 * (365 * year + cumulativeDays[month] + day)

View file

@ -37,7 +37,7 @@ namespace ESM
ESMWriter writer; ESMWriter writer;
writer.setAuthor(author); writer.setAuthor(author);
writer.setDescription(description); writer.setDescription(description);
writer.setFormatVersion(CurrentSaveGameFormatVersion); writer.setFormatVersion(MaxLimitedSizeStringsFormatVersion);
EXPECT_THROW(writer.save(stream), std::runtime_error); EXPECT_THROW(writer.save(stream), std::runtime_error);
} }
@ -46,9 +46,9 @@ namespace ESM
std::stringstream stream; std::stringstream stream;
ESMWriter writer; ESMWriter writer;
writer.setFormatVersion(CurrentSaveGameFormatVersion); writer.setFormatVersion(MaxLimitedSizeStringsFormatVersion);
writer.save(stream); writer.save(stream);
EXPECT_THROW(writer.writeFixedSizeString(generateRandomString(33), 32), std::runtime_error); EXPECT_THROW(writer.writeMaybeFixedSizeString(generateRandomString(33), 32), std::runtime_error);
} }
} }
} }

View file

@ -17,6 +17,7 @@ namespace ESM
using namespace ::testing; using namespace ::testing;
constexpr std::array formats = { constexpr std::array formats = {
MaxLimitedSizeStringsFormatVersion,
CurrentSaveGameFormatVersion, CurrentSaveGameFormatVersion,
}; };
@ -74,15 +75,37 @@ namespace ESM
std::minstd_rand mRandom; std::minstd_rand mRandom;
std::uniform_int_distribution<short> mRefIdDistribution{ 'a', 'z' }; std::uniform_int_distribution<short> mRefIdDistribution{ 'a', 'z' };
RefId generateRandomRefId(std::size_t size = 33) std::string generateRandomString(std::size_t size)
{ {
std::string value; std::string value;
while (value.size() < size) while (value.size() < size)
value.push_back(static_cast<char>(mRefIdDistribution(mRandom))); value.push_back(static_cast<char>(mRefIdDistribution(mRandom)));
return RefId::stringRefId(value); return value;
} }
RefId generateRandomRefId(std::size_t size = 33) { return RefId::stringRefId(generateRandomString(size)); }
}; };
TEST_F(Esm3SaveLoadRecordTest, headerShouldNotChange)
{
const std::string author = generateRandomString(33);
const std::string description = generateRandomString(257);
auto stream = std::make_unique<std::stringstream>();
ESMWriter writer;
writer.setAuthor(author);
writer.setDescription(description);
writer.setFormatVersion(CurrentSaveGameFormatVersion);
writer.save(*stream);
writer.close();
ESMReader reader;
reader.open(std::move(stream), "stream");
EXPECT_EQ(reader.getAuthor(), author);
EXPECT_EQ(reader.getDesc(), description);
}
TEST_P(Esm3SaveLoadRecordTest, playerShouldNotChange) TEST_P(Esm3SaveLoadRecordTest, playerShouldNotChange)
{ {
std::minstd_rand random; std::minstd_rand random;

View file

@ -28,6 +28,8 @@ namespace ESM
FLAG_Blocked = 0x00002000 FLAG_Blocked = 0x00002000
}; };
using StringSizeType = std::uint32_t;
template <std::size_t capacity> template <std::size_t capacity>
struct FixedString struct FixedString
{ {

View file

@ -145,26 +145,10 @@ namespace ESM
std::string ESMReader::getHString() std::string ESMReader::getHString()
{ {
getSubHeader(); return std::string(getHStringView());
// Hack to make MultiMark.esp load. Zero-length strings do not
// occur in any of the official mods, but MultiMark makes use of
// them. For some reason, they break the rules, and contain a byte
// (value 0) even if the header says there is no data. If
// Morrowind accepts it, so should we.
if (mCtx.leftSub == 0 && hasMoreSubs() && !mEsm->peek())
{
// Skip the following zero byte
mCtx.leftRec--;
char c;
getT(c);
return std::string();
}
return getString(mCtx.leftSub);
} }
RefId ESMReader::getRefId() std::string_view ESMReader::getHStringView()
{ {
getSubHeader(); getSubHeader();
@ -179,10 +163,15 @@ namespace ESM
mCtx.leftRec--; mCtx.leftRec--;
char c; char c;
getT(c); getT(c);
return ESM::RefId::sEmpty; return std::string_view();
} }
return getRefId(mCtx.leftSub); return getStringView(mCtx.leftSub);
}
RefId ESMReader::getRefId()
{
return ESM::RefId::stringRefId(getHStringView());
} }
void ESMReader::skipHString() void ESMReader::skipHString()
@ -363,52 +352,42 @@ namespace ESM
* *
*************************************************************************/ *************************************************************************/
std::string ESMReader::getString(int size) std::string ESMReader::getMaybeFixedStringSize(std::size_t size)
{ {
size_t s = size; if (mHeader.mFormatVersion > MaxLimitedSizeStringsFormatVersion)
if (mBuffer.size() <= s) {
// Add some extra padding to reduce the chance of having to resize StringSizeType storedSize = 0;
// again later. getT(storedSize);
mBuffer.resize(3 * s); if (storedSize > mCtx.leftSub)
fail("String does not fit subrecord (" + std::to_string(storedSize) + " > "
+ std::to_string(mCtx.leftSub) + ")");
size = static_cast<std::size_t>(storedSize);
}
// And make sure the string is zero terminated return std::string(getStringView(size));
mBuffer[s] = 0;
// read ESM data
char* ptr = mBuffer.data();
getExact(ptr, size);
size = static_cast<int>(strnlen(ptr, size));
// Convert to UTF8 and return
if (mEncoder)
return std::string(mEncoder->getUtf8(std::string_view(ptr, size)));
return std::string(ptr, size);
} }
ESM::RefId ESMReader::getRefId(int size) std::string_view ESMReader::getStringView(std::size_t size)
{ {
size_t s = size; if (mBuffer.size() <= size)
if (mBuffer.size() <= s)
// Add some extra padding to reduce the chance of having to resize // Add some extra padding to reduce the chance of having to resize
// again later. // again later.
mBuffer.resize(3 * s); mBuffer.resize(3 * size);
// And make sure the string is zero terminated // And make sure the string is zero terminated
mBuffer[s] = 0; mBuffer[size] = 0;
// read ESM data // read ESM data
char* ptr = mBuffer.data(); char* ptr = mBuffer.data();
getExact(ptr, size); getExact(ptr, size);
size = static_cast<int>(strnlen(ptr, size)); size = strnlen(ptr, size);
// Convert to UTF8 and return // Convert to UTF8 and return
if (mEncoder) if (mEncoder != nullptr)
return ESM::RefId::stringRefId(mEncoder->getUtf8(std::string_view(ptr, size))); return mEncoder->getUtf8(std::string_view(ptr, size));
return ESM::RefId::stringRefId(std::string_view(ptr, size)); return std::string_view(ptr, size);
} }
[[noreturn]] void ESMReader::fail(const std::string& msg) [[noreturn]] void ESMReader::fail(const std::string& msg)

View file

@ -174,6 +174,7 @@ namespace ESM
// Read a string, including the sub-record header (but not the name) // Read a string, including the sub-record header (but not the name)
std::string getHString(); std::string getHString();
std::string_view getHStringView();
RefId getRefId(); RefId getRefId();
void skipHString(); void skipHString();
@ -269,10 +270,11 @@ namespace ESM
void getName(NAME& name) { getT(name); } void getName(NAME& name) { getT(name); }
void getUint(uint32_t& u) { getT(u); } void getUint(uint32_t& u) { getT(u); }
std::string getMaybeFixedStringSize(std::size_t size);
// Read the next 'size' bytes and return them as a string. Converts // Read the next 'size' bytes and return them as a string. Converts
// them from native encoding to UTF8 in the process. // them from native encoding to UTF8 in the process.
std::string getString(int size); std::string_view getStringView(std::size_t size);
ESM::RefId getRefId(int size);
void skip(std::size_t bytes) void skip(std::size_t bytes)
{ {

View file

@ -167,15 +167,26 @@ namespace ESM
endRecord(name); endRecord(name);
} }
void ESMWriter::writeFixedSizeString(const std::string& data, std::size_t size) void ESMWriter::writeMaybeFixedSizeString(const std::string& data, std::size_t size)
{ {
std::string string; std::string string;
if (!data.empty()) if (!data.empty())
string = mEncoder ? mEncoder->getLegacyEnc(data) : data; string = mEncoder ? mEncoder->getLegacyEnc(data) : data;
if (string.size() > size) if (mHeader.mFormatVersion <= MaxLimitedSizeStringsFormatVersion)
throw std::runtime_error("Fixed string data is too long: \"" + string + "\" (" {
+ std::to_string(string.size()) + " > " + std::to_string(size) + ")"); if (string.size() > size)
string.resize(size); throw std::runtime_error("Fixed string data is too long: \"" + string + "\" ("
+ std::to_string(string.size()) + " > " + std::to_string(size) + ")");
string.resize(size);
}
else
{
constexpr StringSizeType maxSize = std::numeric_limits<StringSizeType>::max();
if (string.size() > maxSize)
throw std::runtime_error("String size is too long: \"" + string.substr(0, 64) + "<...>\" ("
+ std::to_string(string.size()) + " > " + std::to_string(maxSize) + ")");
writeT(static_cast<StringSizeType>(string.size()));
}
write(string.c_str(), string.size()); write(string.c_str(), string.size());
} }

View file

@ -136,7 +136,7 @@ namespace ESM
void startSubRecord(NAME name); void startSubRecord(NAME name);
void endRecord(NAME name); void endRecord(NAME name);
void endRecord(uint32_t name); void endRecord(uint32_t name);
void writeFixedSizeString(const std::string& data, std::size_t size); void writeMaybeFixedSizeString(const std::string& data, std::size_t size);
void writeHString(const std::string& data); void writeHString(const std::string& data);
void writeHCString(const std::string& data); void writeHCString(const std::string& data);
void writeName(NAME data); void writeName(NAME data);

View file

@ -19,7 +19,8 @@ namespace ESM
inline constexpr FormatVersion MaxOldAiPackageFormatVersion = 17; inline constexpr FormatVersion MaxOldAiPackageFormatVersion = 17;
inline constexpr FormatVersion MaxOldSkillsAndAttributesFormatVersion = 18; inline constexpr FormatVersion MaxOldSkillsAndAttributesFormatVersion = 18;
inline constexpr FormatVersion MaxOldCreatureStatsFormatVersion = 19; inline constexpr FormatVersion MaxOldCreatureStatsFormatVersion = 19;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 22; inline constexpr FormatVersion MaxLimitedSizeStringsFormatVersion = 22;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 23;
} }
#endif #endif

View file

@ -12,7 +12,7 @@ namespace ESM
esm.getSubHeader(); esm.getSubHeader();
ContItem ci; ContItem ci;
esm.getT(ci.mCount); esm.getT(ci.mCount);
ci.mItem = ESM::RefId::stringRefId(esm.getString(32)); ci.mItem = ESM::RefId::stringRefId(esm.getMaybeFixedStringSize(32));
mList.push_back(ci); mList.push_back(ci);
} }
@ -22,7 +22,7 @@ namespace ESM
{ {
esm.startSubRecord("NPCO"); esm.startSubRecord("NPCO");
esm.writeT(it->mCount); esm.writeT(it->mCount);
esm.writeFixedSizeString(it->mItem.getRefIdString(), 32); esm.writeMaybeFixedSizeString(it->mItem.getRefIdString(), 32);
esm.endRecord("NPCO"); esm.endRecord("NPCO");
} }
} }

View file

@ -53,7 +53,7 @@ namespace ESM
{ {
esm.getSubHeader(); esm.getSubHeader();
SoundRef sr; SoundRef sr;
sr.mSound = ESM::RefId::stringRefId(esm.getString(32)); sr.mSound = ESM::RefId::stringRefId(esm.getMaybeFixedStringSize(32));
esm.getT(sr.mChance); esm.getT(sr.mChance);
mSoundList.push_back(sr); mSoundList.push_back(sr);
break; break;
@ -95,7 +95,7 @@ namespace ESM
for (std::vector<SoundRef>::const_iterator it = mSoundList.begin(); it != mSoundList.end(); ++it) for (std::vector<SoundRef>::const_iterator it = mSoundList.begin(); it != mSoundList.end(); ++it)
{ {
esm.startSubRecord("SNAM"); esm.startSubRecord("SNAM");
esm.writeFixedSizeString(it->mSound.getRefIdString(), 32); esm.writeMaybeFixedSizeString(it->mSound.getRefIdString(), 32);
esm.writeT(it->mChance); esm.writeT(it->mChance);
esm.endRecord("SNAM"); esm.endRecord("SNAM");
} }

View file

@ -98,7 +98,7 @@ namespace ESM
case fourCC("SCHD"): case fourCC("SCHD"):
{ {
esm.getSubHeader(); esm.getSubHeader();
mId = ESM::RefId::stringRefId(esm.getString(32)); mId = ESM::RefId::stringRefId(esm.getMaybeFixedStringSize(32));
esm.getT(mData); esm.getT(mData);
hasHeader = true; hasHeader = true;
@ -152,7 +152,7 @@ namespace ESM
varNameString.append(*it); varNameString.append(*it);
esm.startSubRecord("SCHD"); esm.startSubRecord("SCHD");
esm.writeFixedSizeString(mId.getRefIdString(), 32); esm.writeMaybeFixedSizeString(mId.getRefIdString(), 32);
esm.writeT(mData, 20); esm.writeT(mData, 20);
esm.endRecord("SCHD"); esm.endRecord("SCHD");

View file

@ -30,8 +30,8 @@ namespace ESM
esm.getSubHeader(); esm.getSubHeader();
esm.getT(mData.version); esm.getT(mData.version);
esm.getT(mData.type); esm.getT(mData.type);
mData.author.assign(esm.getString(32)); mData.author.assign(esm.getMaybeFixedStringSize(32));
mData.desc.assign(esm.getString(256)); mData.desc.assign(esm.getMaybeFixedStringSize(256));
esm.getT(mData.records); esm.getT(mData.records);
} }
@ -71,8 +71,8 @@ namespace ESM
esm.startSubRecord("HEDR"); esm.startSubRecord("HEDR");
esm.writeT(mData.version); esm.writeT(mData.version);
esm.writeT(mData.type); esm.writeT(mData.type);
esm.writeFixedSizeString(mData.author, 32); esm.writeMaybeFixedSizeString(mData.author, 32);
esm.writeFixedSizeString(mData.desc, 256); esm.writeMaybeFixedSizeString(mData.desc, 256);
esm.writeT(mData.records); esm.writeT(mData.records);
esm.endRecord("HEDR"); esm.endRecord("HEDR");