1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-01-16 17:59:56 +00:00

Merge branch 'vfs_normalized_path_3' into 'master'

Use normalized path for correctSoundPath

See merge request OpenMW/openmw!3903
This commit is contained in:
psi29a 2024-02-26 11:21:33 +00:00
commit f2039b35d0
20 changed files with 157 additions and 47 deletions

View file

@ -6,6 +6,8 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <components/vfs/pathutil.hpp>
#include "../mwsound/type.hpp" #include "../mwsound/type.hpp"
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
@ -129,11 +131,11 @@ namespace MWBase
/// \param name of the folder that contains the playlist /// \param name of the folder that contains the playlist
/// Title music playlist is predefined /// Title music playlist is predefined
virtual void say(const MWWorld::ConstPtr& reference, const std::string& filename) = 0; virtual void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) = 0;
///< Make an actor say some text. ///< Make an actor say some text.
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS
virtual void say(const std::string& filename) = 0; virtual void say(VFS::Path::NormalizedView filename) = 0;
///< Say some text, without an actor ref ///< Say some text, without an actor ref
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS

View file

@ -653,7 +653,7 @@ namespace MWDialogue
if (Settings::gui().mSubtitles) if (Settings::gui().mSubtitles)
winMgr->messageBox(info->mResponse); winMgr->messageBox(info->mResponse);
if (!info->mSound.empty()) if (!info->mSound.empty())
sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(info->mSound)); sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(info->mSound)));
if (!info->mResultScript.empty()) if (!info->mResultScript.empty())
executeScript(info->mResultScript, actor); executeScript(info->mResultScript, actor);
} }

View file

@ -39,7 +39,7 @@ namespace
{ {
const std::string mText; const std::string mText;
const Response mResponses[3]; const Response mResponses[3];
const std::string mSound; const VFS::Path::Normalized mSound;
}; };
Step sGenerateClassSteps(int number) Step sGenerateClassSteps(int number)

View file

@ -174,12 +174,12 @@ namespace MWLua
api["say"] = sol::overload( api["say"] = sol::overload(
[luaManager = context.mLuaManager]( [luaManager = context.mLuaManager](
std::string_view fileName, const Object& object, sol::optional<std::string_view> text) { std::string_view fileName, const Object& object, sol::optional<std::string_view> text) {
MWBase::Environment::get().getSoundManager()->say(object.ptr(), std::string(fileName)); MWBase::Environment::get().getSoundManager()->say(object.ptr(), VFS::Path::Normalized(fileName));
if (text) if (text)
luaManager->addUIMessage(*text); luaManager->addUIMessage(*text);
}, },
[luaManager = context.mLuaManager](std::string_view fileName, sol::optional<std::string_view> text) { [luaManager = context.mLuaManager](std::string_view fileName, sol::optional<std::string_view> text) {
MWBase::Environment::get().getSoundManager()->say(std::string(fileName)); MWBase::Environment::get().getSoundManager()->say(VFS::Path::Normalized(fileName));
if (text) if (text)
luaManager->addUIMessage(*text); luaManager->addUIMessage(*text);
}); });
@ -227,7 +227,7 @@ namespace MWLua
soundT["maxRange"] soundT["maxRange"]
= sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMaxRange; }); = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMaxRange; });
soundT["fileName"] = sol::readonly_property([](const ESM::Sound& rec) -> std::string { soundT["fileName"] = sol::readonly_property([](const ESM::Sound& rec) -> std::string {
return VFS::Path::normalizeFilename(Misc::ResourceHelpers::correctSoundPath(rec.mSound)); return Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(rec.mSound)).value();
}); });
return LuaUtil::makeReadOnly(api); return LuaUtil::makeReadOnly(api);

View file

@ -33,7 +33,7 @@ namespace MWScript
MWScript::InterpreterContext& context MWScript::InterpreterContext& context
= static_cast<MWScript::InterpreterContext&>(runtime.getContext()); = static_cast<MWScript::InterpreterContext&>(runtime.getContext());
std::string file{ runtime.getStringLiteral(runtime[0].mInteger) }; VFS::Path::Normalized file{ runtime.getStringLiteral(runtime[0].mInteger) };
runtime.pop(); runtime.pop();
std::string_view text = runtime.getStringLiteral(runtime[0].mInteger); std::string_view text = runtime.getStringLiteral(runtime[0].mInteger);

View file

@ -1034,7 +1034,7 @@ namespace MWSound
return ret; return ret;
} }
std::pair<Sound_Handle, size_t> OpenAL_Output::loadSound(const std::string& fname) std::pair<Sound_Handle, size_t> OpenAL_Output::loadSound(VFS::Path::NormalizedView fname)
{ {
getALError(); getALError();
@ -1045,7 +1045,7 @@ namespace MWSound
try try
{ {
DecoderPtr decoder = mManager.getDecoder(); DecoderPtr decoder = mManager.getDecoder();
decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, decoder->mResourceMgr)); decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, *decoder->mResourceMgr));
ChannelConfig chans; ChannelConfig chans;
SampleType type; SampleType type;

View file

@ -7,6 +7,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <components/vfs/pathutil.hpp>
#include "al.h" #include "al.h"
#include "alc.h" #include "alc.h"
#include "alext.h" #include "alext.h"
@ -85,7 +87,7 @@ namespace MWSound
std::vector<std::string> enumerateHrtf() override; std::vector<std::string> enumerateHrtf() override;
std::pair<Sound_Handle, size_t> loadSound(const std::string& fname) override; std::pair<Sound_Handle, size_t> loadSound(VFS::Path::NormalizedView fname) override;
size_t unloadSound(Sound_Handle data) override; size_t unloadSound(Sound_Handle data) override;
bool playSound(Sound* sound, Sound_Handle data, float offset) override; bool playSound(Sound* sound, Sound_Handle data, float offset) override;

View file

@ -183,9 +183,8 @@ namespace MWSound
min = std::max(min, 1.0f); min = std::max(min, 1.0f);
max = std::max(min, max); max = std::max(min, max);
Sound_Buffer& sfx Sound_Buffer& sfx = mSoundBuffers.emplace_back(
= mSoundBuffers.emplace_back(Misc::ResourceHelpers::correctSoundPath(sound.mSound), volume, min, max); Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(sound.mSound)), volume, min, max);
VFS::Path::normalizeFilenameInPlace(sfx.mResourceName);
mBufferNameMap.emplace(soundId, &sfx); mBufferNameMap.emplace(soundId, &sfx);
return &sfx; return &sfx;

View file

@ -35,7 +35,7 @@ namespace MWSound
{ {
} }
const std::string& getResourceName() const noexcept { return mResourceName; } const VFS::Path::Normalized& getResourceName() const noexcept { return mResourceName; }
Sound_Handle getHandle() const noexcept { return mHandle; } Sound_Handle getHandle() const noexcept { return mHandle; }
@ -46,7 +46,7 @@ namespace MWSound
float getMaxDist() const noexcept { return mMaxDist; } float getMaxDist() const noexcept { return mMaxDist; }
private: private:
std::string mResourceName; VFS::Path::Normalized mResourceName;
float mVolume; float mVolume;
float mMinDist; float mMinDist;
float mMaxDist; float mMaxDist;

View file

@ -6,6 +6,7 @@
#include <vector> #include <vector>
#include <components/settings/hrtfmode.hpp> #include <components/settings/hrtfmode.hpp>
#include <components/vfs/pathutil.hpp>
#include "../mwbase/soundmanager.hpp" #include "../mwbase/soundmanager.hpp"
@ -39,7 +40,7 @@ namespace MWSound
virtual std::vector<std::string> enumerateHrtf() = 0; virtual std::vector<std::string> enumerateHrtf() = 0;
virtual std::pair<Sound_Handle, size_t> loadSound(const std::string& fname) = 0; virtual std::pair<Sound_Handle, size_t> loadSound(VFS::Path::NormalizedView fname) = 0;
virtual size_t unloadSound(Sound_Handle data) = 0; virtual size_t unloadSound(Sound_Handle data) = 0;
virtual bool playSound(Sound* sound, Sound_Handle data, float offset) = 0; virtual bool playSound(Sound* sound, Sound_Handle data, float offset) = 0;

View file

@ -172,12 +172,12 @@ namespace MWSound
return std::make_shared<FFmpeg_Decoder>(mVFS); return std::make_shared<FFmpeg_Decoder>(mVFS);
} }
DecoderPtr SoundManager::loadVoice(const std::string& voicefile) DecoderPtr SoundManager::loadVoice(VFS::Path::NormalizedView voicefile)
{ {
try try
{ {
DecoderPtr decoder = getDecoder(); DecoderPtr decoder = getDecoder();
decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, decoder->mResourceMgr)); decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, *decoder->mResourceMgr));
return decoder; return decoder;
} }
catch (std::exception& e) catch (std::exception& e)
@ -380,7 +380,7 @@ namespace MWSound
startRandomTitle(); startRandomTitle();
} }
void SoundManager::say(const MWWorld::ConstPtr& ptr, const std::string& filename) void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename)
{ {
if (!mOutput->isInitialized()) if (!mOutput->isInitialized())
return; return;
@ -412,7 +412,7 @@ namespace MWSound
return 0.0f; return 0.0f;
} }
void SoundManager::say(const std::string& filename) void SoundManager::say(VFS::Path::NormalizedView filename)
{ {
if (!mOutput->isInitialized()) if (!mOutput->isInitialized())
return; return;

View file

@ -116,7 +116,7 @@ namespace MWSound
Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound* sound); Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound* sound);
// returns a decoder to start streaming, or nullptr if the sound was not found // returns a decoder to start streaming, or nullptr if the sound was not found
DecoderPtr loadVoice(const std::string& voicefile); DecoderPtr loadVoice(VFS::Path::NormalizedView voicefile);
SoundPtr getSoundRef(); SoundPtr getSoundRef();
StreamPtr getStreamRef(); StreamPtr getStreamRef();
@ -188,11 +188,11 @@ namespace MWSound
/// \param name of the folder that contains the playlist /// \param name of the folder that contains the playlist
/// Title music playlist is predefined /// Title music playlist is predefined
void say(const MWWorld::ConstPtr& reference, const std::string& filename) override; void say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override;
///< Make an actor say some text. ///< Make an actor say some text.
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS
void say(const std::string& filename) override; void say(VFS::Path::NormalizedView filename) override;
///< Say some text, without an actor ref ///< Say some text, without an actor ref
/// \param filename name of a sound file in the VFS /// \param filename name of a sound file in the VFS

View file

@ -8,26 +8,19 @@ namespace
TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected) TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected)
{ {
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/bar.wav", nullptr } }); std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/bar.wav", nullptr } });
EXPECT_EQ(correctSoundPath("sound/bar.wav", mVFS.get()), "sound/bar.wav"); EXPECT_EQ(correctSoundPath("sound/bar.wav", *mVFS), "sound/bar.wav");
} }
TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected) TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected)
{ {
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/foo.mp3", nullptr } }); std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/foo.mp3", nullptr } });
EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); EXPECT_EQ(correctSoundPath("sound/foo.wav", *mVFS), "sound/foo.mp3");
} }
TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs) TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs)
{ {
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({}); std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); EXPECT_EQ(correctSoundPath("sound/foo.wav", *mVFS), "sound/foo.mp3");
}
TEST(CorrectSoundPath, correct_path_normalize_paths)
{
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctSoundPath("sound\\foo.wav", mVFS.get()), "sound/foo.mp3");
EXPECT_EQ(correctSoundPath("SOUND\\foo.WAV", mVFS.get()), "sound/foo.mp3");
} }
namespace namespace

View file

@ -2,6 +2,7 @@
#define TESTING_UTIL_H #define TESTING_UTIL_H
#include <filesystem> #include <filesystem>
#include <initializer_list>
#include <sstream> #include <sstream>
#include <components/misc/strings/conversion.hpp> #include <components/misc/strings/conversion.hpp>
@ -73,6 +74,12 @@ namespace TestingOpenMW
return vfs; return vfs;
} }
inline std::unique_ptr<VFS::Manager> createTestVFS(
std::initializer_list<std::pair<std::string_view, VFS::File*>> files)
{
return createTestVFS(VFS::FileMap(files.begin(), files.end()));
}
#define EXPECT_ERROR(X, ERR_SUBSTR) \ #define EXPECT_ERROR(X, ERR_SUBSTR) \
try \ try \
{ \ { \

View file

@ -65,6 +65,53 @@ namespace VFS::Path
EXPECT_EQ(stream.str(), "foo/bar/baz"); EXPECT_EQ(stream.str(), "foo/bar/baz");
} }
TEST(NormalizedTest, shouldSupportOperatorDivEqual)
{
Normalized value("foo/bar");
value /= NormalizedView("baz");
EXPECT_EQ(value.value(), "foo/bar/baz");
}
TEST(NormalizedTest, changeExtensionShouldReplaceAfterLastDot)
{
Normalized value("foo/bar.a");
ASSERT_TRUE(value.changeExtension("so"));
EXPECT_EQ(value.value(), "foo/bar.so");
}
TEST(NormalizedTest, changeExtensionShouldNormalizeExtension)
{
Normalized value("foo/bar.a");
ASSERT_TRUE(value.changeExtension("SO"));
EXPECT_EQ(value.value(), "foo/bar.so");
}
TEST(NormalizedTest, changeExtensionShouldIgnorePathWithoutADot)
{
Normalized value("foo/bar");
ASSERT_FALSE(value.changeExtension("so"));
EXPECT_EQ(value.value(), "foo/bar");
}
TEST(NormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator)
{
Normalized value("foo.bar/baz");
ASSERT_FALSE(value.changeExtension("so"));
EXPECT_EQ(value.value(), "foo.bar/baz");
}
TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithDot)
{
Normalized value("foo.a");
EXPECT_THROW(value.changeExtension(".so"), std::invalid_argument);
}
TEST(NormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithSeparator)
{
Normalized value("foo.a");
EXPECT_THROW(value.changeExtension("/so"), std::invalid_argument);
}
template <class T> template <class T>
struct NormalizedOperatorsTest : Test struct NormalizedOperatorsTest : Test
{ {
@ -135,5 +182,13 @@ namespace VFS::Path
{ {
EXPECT_THROW([] { NormalizedView("Foo\\Bar/baz"); }(), std::invalid_argument); EXPECT_THROW([] { NormalizedView("Foo\\Bar/baz"); }(), std::invalid_argument);
} }
TEST(NormalizedView, shouldSupportOperatorDiv)
{
const NormalizedView a("foo/bar");
const NormalizedView b("baz");
const Normalized result = a / b;
EXPECT_EQ(result.value(), "foo/bar/baz");
}
} }
} }

View file

@ -180,9 +180,10 @@ std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath)
return res; return res;
} }
std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath) VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(VFS::Path::NormalizedView resPath)
{ {
return "sound\\" + resPath; static constexpr VFS::Path::NormalizedView prefix("sound");
return prefix / resPath;
} }
std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath) std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath)
@ -201,17 +202,17 @@ std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath
return resPath.substr(prefix.size() + 1); return resPath.substr(prefix.size() + 1);
} }
std::string Misc::ResourceHelpers::correctSoundPath(std::string_view resPath, const VFS::Manager* vfs) VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs)
{ {
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if (!vfs->exists(resPath)) if (!vfs.exists(resPath))
{ {
std::string sound{ resPath }; VFS::Path::Normalized sound(resPath);
changeExtension(sound, ".mp3"); sound.changeExtension("mp3");
VFS::Path::normalizeFilenameInPlace(sound);
return sound; return sound;
} }
return VFS::Path::normalizeFilename(resPath); return VFS::Path::Normalized(resPath);
} }
bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id) bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id)

View file

@ -1,6 +1,8 @@
#ifndef MISC_RESOURCEHELPERS_H #ifndef MISC_RESOURCEHELPERS_H
#define MISC_RESOURCEHELPERS_H #define MISC_RESOURCEHELPERS_H
#include <components/vfs/pathutil.hpp>
#include <span> #include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
@ -37,7 +39,7 @@ namespace Misc
std::string correctMeshPath(std::string_view resPath); std::string correctMeshPath(std::string_view resPath);
// Adds "sound\\". // Adds "sound\\".
std::string correctSoundPath(const std::string& resPath); VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath);
// Adds "music\\". // Adds "music\\".
std::string correctMusicPath(const std::string& resPath); std::string correctMusicPath(const std::string& resPath);
@ -45,7 +47,7 @@ namespace Misc
// Removes "meshes\\". // Removes "meshes\\".
std::string_view meshPathForESM3(std::string_view resPath); std::string_view meshPathForESM3(std::string_view resPath);
std::string correctSoundPath(std::string_view resPath, const VFS::Manager* vfs); VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs);
/// marker objects that have a hardcoded function in the game logic, should be hidden from the player /// marker objects that have a hardcoded function in the game logic, should be hidden from the player
bool isHiddenMarker(const ESM::RefId& id); bool isHiddenMarker(const ESM::RefId& id);

View file

@ -57,6 +57,11 @@ namespace VFS
return mIndex.find(name) != mIndex.end(); return mIndex.find(name) != mIndex.end();
} }
bool Manager::exists(Path::NormalizedView name) const
{
return mIndex.find(name) != mIndex.end();
}
std::string Manager::getArchive(const Path::Normalized& name) const std::string Manager::getArchive(const Path::Normalized& name) const
{ {
for (auto it = mArchives.rbegin(); it != mArchives.rend(); ++it) for (auto it = mArchives.rbegin(); it != mArchives.rend(); ++it)

View file

@ -43,6 +43,8 @@ namespace VFS
/// @note May be called from any thread once the index has been built. /// @note May be called from any thread once the index has been built.
bool exists(const Path::Normalized& name) const; bool exists(const Path::Normalized& name) const;
bool exists(Path::NormalizedView name) const;
/// Retrieve a file by name. /// Retrieve a file by name.
/// @note Throws an exception if the file can not be found. /// @note Throws an exception if the file can not be found.
/// @note May be called from any thread once the index has been built. /// @note May be called from any thread once the index has been built.

View file

@ -11,9 +11,12 @@
namespace VFS::Path namespace VFS::Path
{ {
inline constexpr char separator = '/';
inline constexpr char extensionSeparator = '.';
inline constexpr char normalize(char c) inline constexpr char normalize(char c)
{ {
return c == '\\' ? '/' : Misc::StringUtils::toLower(c); return c == '\\' ? separator : Misc::StringUtils::toLower(c);
} }
inline constexpr bool isNormalized(std::string_view name) inline constexpr bool isNormalized(std::string_view name)
@ -21,9 +24,14 @@ namespace VFS::Path
return std::all_of(name.begin(), name.end(), [](char v) { return v == normalize(v); }); return std::all_of(name.begin(), name.end(), [](char v) { return v == normalize(v); });
} }
inline void normalizeFilenameInPlace(auto begin, auto end)
{
std::transform(begin, end, begin, normalize);
}
inline void normalizeFilenameInPlace(std::string& name) inline void normalizeFilenameInPlace(std::string& name)
{ {
std::transform(name.begin(), name.end(), name.begin(), normalize); normalizeFilenameInPlace(name.begin(), name.end());
} }
/// Normalize the given filename, making slashes/backslashes consistent, and lower-casing. /// Normalize the given filename, making slashes/backslashes consistent, and lower-casing.
@ -59,6 +67,11 @@ namespace VFS::Path
bool operator()(std::string_view left, std::string_view right) const { return pathLess(left, right); } bool operator()(std::string_view left, std::string_view right) const { return pathLess(left, right); }
}; };
inline constexpr auto findSeparatorOrExtensionSeparator(auto begin, auto end)
{
return std::find_if(begin, end, [](char v) { return v == extensionSeparator || v == separator; });
}
class Normalized; class Normalized;
class NormalizedView class NormalizedView
@ -122,7 +135,7 @@ namespace VFS::Path
{ {
} }
Normalized(const char* value) explicit Normalized(const char* value)
: Normalized(std::string_view(value)) : Normalized(std::string_view(value))
{ {
} }
@ -153,6 +166,27 @@ namespace VFS::Path
operator const std::string&() const { return mValue; } operator const std::string&() const { return mValue; }
bool changeExtension(std::string_view extension)
{
if (findSeparatorOrExtensionSeparator(extension.begin(), extension.end()) != extension.end())
throw std::invalid_argument("Invalid extension: " + std::string(extension));
const auto it = findSeparatorOrExtensionSeparator(mValue.rbegin(), mValue.rend());
if (it == mValue.rend() || *it == separator)
return false;
const std::string::difference_type pos = mValue.rend() - it;
mValue.replace(pos, mValue.size(), extension);
normalizeFilenameInPlace(mValue.begin() + pos, mValue.end());
return true;
}
Normalized& operator/=(NormalizedView value)
{
mValue.reserve(mValue.size() + value.value().size() + 1);
mValue += separator;
mValue += value.value();
return *this;
}
friend bool operator==(const Normalized& lhs, const Normalized& rhs) = default; friend bool operator==(const Normalized& lhs, const Normalized& rhs) = default;
friend bool operator==(const Normalized& lhs, const auto& rhs) { return lhs.mValue == rhs; } friend bool operator==(const Normalized& lhs, const auto& rhs) { return lhs.mValue == rhs; }
@ -207,6 +241,13 @@ namespace VFS::Path
: mValue(value.view()) : mValue(value.view())
{ {
} }
inline Normalized operator/(NormalizedView lhs, NormalizedView rhs)
{
Normalized result(lhs);
result /= rhs;
return result;
}
} }
#endif #endif