Add extension path type

This commit is contained in:
elsid 2025-09-14 11:32:35 +02:00
parent b5196b2fd1
commit 878f9f8433
No known key found for this signature in database
GPG Key ID: B845CB9FEE18AB40
11 changed files with 255 additions and 95 deletions

View File

@ -12,6 +12,8 @@ namespace Misc::ResourceHelpers
constexpr VFS::Path::NormalizedView sound("sound");
constexpr VFS::Path::NormalizedView textures("textures");
constexpr VFS::Path::NormalizedView bookart("bookart");
constexpr VFS::Path::ExtensionView mp3("mp3");
constexpr VFS::Path::ExtensionView b("b");
TEST(MiscResourceHelpersCorrectSoundPath, shouldKeepWavExtensionIfExistsInVfs)
{
@ -29,74 +31,74 @@ namespace Misc::ResourceHelpers
TEST(MiscResourceHelpersCorrectSoundPath, shouldKeepWavExtensionIfBothExistsInVfs)
{
constexpr VFS::Path::NormalizedView wav("sound/foo.wav");
constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3");
constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav");
constexpr VFS::Path::NormalizedView mp3Path("sound/foo.mp3");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({
{ wav, nullptr },
{ mp3, nullptr },
{ wavPath, nullptr },
{ mp3Path, nullptr },
});
EXPECT_EQ(correctSoundPath(wav, *vfs), "sound/foo.wav");
EXPECT_EQ(correctSoundPath(wavPath, *vfs), "sound/foo.wav");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtentionIfDoesNotExistInVfs)
TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtensionIfDoesNotExistInVfs)
{
constexpr VFS::Path::NormalizedView path("sound/foo.wav");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/foo.mp3");
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/foo.mp3");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtentionIfBothExistInVfs)
TEST(MiscResourceHelpersCorrectResourcePath, shouldFallbackToGivenExtensionIfBothExistInVfs)
{
constexpr VFS::Path::NormalizedView wav("sound/foo.wav");
constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3");
constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav");
constexpr VFS::Path::NormalizedView mp3Path("sound/foo.mp3");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({
{ wav, nullptr },
{ mp3, nullptr },
{ wavPath, nullptr },
{ mp3Path, nullptr },
});
EXPECT_EQ(correctResourcePath({ { sound } }, wav, *vfs, "mp3"), "sound/foo.mp3");
EXPECT_EQ(correctResourcePath({ { sound } }, wavPath, *vfs, mp3), "sound/foo.mp3");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldKeepExtentionIfExistInVfs)
TEST(MiscResourceHelpersCorrectResourcePath, shouldKeepExtensionIfExistInVfs)
{
constexpr VFS::Path::NormalizedView wav("sound/foo.wav");
constexpr VFS::Path::NormalizedView wavPath("sound/foo.wav");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({
{ wav, nullptr },
{ wavPath, nullptr },
});
EXPECT_EQ(correctResourcePath({ { sound } }, wav, *vfs, "mp3"), "sound/foo.wav");
EXPECT_EQ(correctResourcePath({ { sound } }, wavPath, *vfs, mp3), "sound/foo.wav");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldPrefixWithGivenTopDirectory)
{
constexpr VFS::Path::NormalizedView path("foo.mp3");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/foo.mp3");
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/foo.mp3");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldChangeTopDirectoryAndKeepExtensionIfOriginalExistInVfs)
{
constexpr VFS::Path::NormalizedView path("bookart/foo.a");
constexpr VFS::Path::NormalizedView a("textures/foo.a");
constexpr VFS::Path::NormalizedView aPath("textures/foo.a");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({
{ a, nullptr },
{ aPath, nullptr },
});
EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, "b"), "textures/foo.a");
EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, b), "textures/foo.a");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldChangeTopDirectoryAndChangeExtensionIfFallbackExistInVfs)
{
constexpr VFS::Path::NormalizedView path("bookart/foo.a");
constexpr VFS::Path::NormalizedView b("textures/foo.b");
constexpr VFS::Path::NormalizedView bPath("textures/foo.b");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({
{ b, nullptr },
{ bPath, nullptr },
});
EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, "b"), "textures/foo.b");
EXPECT_EQ(correctResourcePath({ { textures, bookart } }, path, *vfs, b), "textures/foo.b");
}
TEST(MiscResourceHelpersCorrectResourcePath, shouldHandlePathEqualToDirectory)
{
constexpr VFS::Path::NormalizedView path("sound");
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, "mp3"), "sound/sound");
EXPECT_EQ(correctResourcePath({ { sound } }, path, *vfs, mp3), "sound/sound");
}
struct MiscResourceHelpersCorrectResourcePathShouldRemoveExtraPrefix : TestWithParam<VFS::Path::NormalizedView>
@ -106,7 +108,7 @@ namespace Misc::ResourceHelpers
TEST_P(MiscResourceHelpersCorrectResourcePathShouldRemoveExtraPrefix, shouldMatchExpected)
{
const std::unique_ptr<const VFS::Manager> vfs = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctResourcePath({ { sound } }, GetParam(), *vfs, "mp3"), "sound/foo.mp3");
EXPECT_EQ(correctResourcePath({ { sound } }, GetParam(), *vfs, mp3), "sound/foo.mp3");
}
const std::vector<VFS::Path::NormalizedView> pathsWithPrefix = {

View File

@ -10,6 +10,13 @@ namespace VFS::Path
{
using namespace testing;
template <class T0, class T1>
struct TypePair
{
using Type0 = T0;
using Type1 = T1;
};
struct VFSPathIsNormalizedTest : TestWithParam<std::pair<std::string_view, bool>>
{
};
@ -53,6 +60,80 @@ namespace VFS::Path
EXPECT_EQ(value, "foo");
}
TEST(VFSPathExtensionViewTest, shouldSupportDefaultConstructor)
{
constexpr ExtensionView extension;
EXPECT_TRUE(extension.empty());
EXPECT_EQ(extension.value(), "");
}
TEST(VFSPathExtensionViewTest, shouldSupportConstexprConstructorFromConstCharPtr)
{
constexpr ExtensionView extension("png");
EXPECT_FALSE(extension.empty());
EXPECT_EQ(extension.value(), "png");
}
TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionOnNotNormalizedValue)
{
EXPECT_THROW([] { ExtensionView("PNG"); }(), std::invalid_argument);
}
TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsExtensionSeparator)
{
EXPECT_THROW([] { ExtensionView(".png"); }(), std::invalid_argument);
}
TEST(VFSPathExtensionViewTest, constructorShouldThrowExceptionIfValueContainsSeparator)
{
EXPECT_THROW([] { ExtensionView("/png"); }(), std::invalid_argument);
}
template <class T>
struct VFSPathExtensionViewOperatorsTest : Test
{
};
TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest);
TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsEqual)
{
using Type0 = typename TypeParam::Type0;
using Type1 = typename TypeParam::Type1;
const Type0 extension{ "png" };
const Type1 otherEqual{ "png" };
const Type1 otherNotEqual{ "jpg" };
EXPECT_EQ(extension, otherEqual);
EXPECT_EQ(otherEqual, extension);
EXPECT_NE(extension, otherNotEqual);
EXPECT_NE(otherNotEqual, extension);
}
TYPED_TEST_P(VFSPathExtensionViewOperatorsTest, supportsLess)
{
using Type0 = typename TypeParam::Type0;
using Type1 = typename TypeParam::Type1;
const Type0 extension{ "png" };
const Type1 otherEqual{ "png" };
const Type1 otherLess{ "jpg" };
const Type1 otherGreater{ "tga" };
EXPECT_FALSE(extension < otherEqual);
EXPECT_FALSE(otherEqual < extension);
EXPECT_LT(otherLess, extension);
EXPECT_FALSE(extension < otherLess);
EXPECT_LT(extension, otherGreater);
EXPECT_FALSE(otherGreater < extension);
}
REGISTER_TYPED_TEST_SUITE_P(VFSPathExtensionViewOperatorsTest, supportsEqual, supportsLess);
using VFSPathExtensionViewOperatorsTypePairs
= Types<TypePair<ExtensionView, ExtensionView>, TypePair<ExtensionView, const char*>,
TypePair<ExtensionView, std::string>, TypePair<ExtensionView, std::string_view>>;
INSTANTIATE_TYPED_TEST_SUITE_P(
Typed, VFSPathExtensionViewOperatorsTest, VFSPathExtensionViewOperatorsTypePairs);
TEST(VFSPathNormalizedTest, shouldSupportDefaultConstructor)
{
const Normalized value;
@ -131,41 +212,34 @@ namespace VFS::Path
TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceAfterLastDot)
{
Normalized value("foo/bar.a");
ASSERT_TRUE(value.changeExtension("so"));
EXPECT_EQ(value.value(), "foo/bar.so");
}
TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnNotNormalizedExtension)
{
Normalized value("foo/bar.a");
EXPECT_THROW(value.changeExtension("\\SO"), std::invalid_argument);
Normalized value("foo/ba.r.a");
constexpr ExtensionView extension("so");
ASSERT_TRUE(value.changeExtension(extension));
EXPECT_EQ(value.value(), "foo/ba.r.so");
}
TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithoutADot)
{
Normalized value("foo/bar");
ASSERT_FALSE(value.changeExtension("so"));
constexpr ExtensionView extension("so");
ASSERT_FALSE(value.changeExtension(extension));
EXPECT_EQ(value.value(), "foo/bar");
}
TEST(VFSPathNormalizedTest, changeExtensionShouldIgnorePathWithDotBeforeSeparator)
{
Normalized value("foo.bar/baz");
ASSERT_FALSE(value.changeExtension("so"));
constexpr ExtensionView extension("so");
ASSERT_FALSE(value.changeExtension(extension));
EXPECT_EQ(value.value(), "foo.bar/baz");
}
TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithDot)
TEST(VFSPathNormalizedTest, changeExtensionShouldReplaceWithShorterExtension)
{
Normalized value("foo.a");
EXPECT_THROW(value.changeExtension(".so"), std::invalid_argument);
}
TEST(VFSPathNormalizedTest, changeExtensionShouldThrowExceptionOnExtensionWithSeparator)
{
Normalized value("foo.a");
EXPECT_THROW(value.changeExtension("so/"), std::invalid_argument);
Normalized value("foo/bar.nif");
constexpr ExtensionView extension("kf");
ASSERT_TRUE(value.changeExtension(extension));
EXPECT_EQ(value.value(), "foo/bar.kf");
}
TEST(VFSPathNormalizedTest, filenameShouldReturnLastComponentOfThePath)
@ -218,20 +292,14 @@ namespace VFS::Path
REGISTER_TYPED_TEST_SUITE_P(VFSPathNormalizedOperatorsTest, supportsEqual, supportsLess);
template <class T0, class T1>
struct TypePair
{
using Type0 = T0;
using Type1 = T1;
};
using VFSPathNormalizedOperatorsTypePairs
= Types<TypePair<Normalized, Normalized>, TypePair<Normalized, const char*>,
TypePair<Normalized, std::string>, TypePair<Normalized, std::string_view>,
TypePair<Normalized, NormalizedView>, TypePair<NormalizedView, Normalized>,
TypePair<NormalizedView, const char*>, TypePair<NormalizedView, std::string>,
TypePair<NormalizedView, std::string_view>, TypePair<NormalizedView, NormalizedView>>;
using TypePairs = Types<TypePair<Normalized, Normalized>, TypePair<Normalized, const char*>,
TypePair<Normalized, std::string>, TypePair<Normalized, std::string_view>,
TypePair<Normalized, NormalizedView>, TypePair<NormalizedView, Normalized>,
TypePair<NormalizedView, const char*>, TypePair<NormalizedView, std::string>,
TypePair<NormalizedView, std::string_view>, TypePair<NormalizedView, NormalizedView>>;
INSTANTIATE_TYPED_TEST_SUITE_P(Typed, VFSPathNormalizedOperatorsTest, TypePairs);
INSTANTIATE_TYPED_TEST_SUITE_P(Typed, VFSPathNormalizedOperatorsTest, VFSPathNormalizedOperatorsTypePairs);
TEST(VFSPathNormalizedViewTest, shouldSupportConstructorFromNormalized)
{

View File

@ -659,21 +659,21 @@ namespace MWRender
path.replace(extensionStart, path.size() - extensionStart, "/");
constexpr VFS::Path::ExtensionView kf("kf");
for (const VFS::Path::Normalized& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(path))
{
if (Misc::getFileExtension(name) == "kf")
{
if (name.extension() == kf)
addSingleAnimSource(name, baseModel);
}
}
}
void Animation::addAnimSource(std::string_view model, const std::string& baseModel)
{
constexpr VFS::Path::ExtensionView kf("kf");
constexpr VFS::Path::ExtensionView nif("nif");
VFS::Path::Normalized kfname(model);
if (Misc::getFileExtension(kfname) == "nif")
kfname.changeExtension("kf");
if (kfname.extension() == nif)
kfname.changeExtension(kf);
addSingleAnimSource(kfname, baseModel);
@ -757,10 +757,12 @@ namespace MWRender
// Get the blending rules
if (Settings::game().mSmoothAnimTransitions)
{
constexpr VFS::Path::ExtensionView yaml("yaml");
// Note, even if the actual config is .json - we should send a .yaml path to AnimBlendRulesManager, the
// manager will check for .json if it will not find a specified .yaml file.
VFS::Path::Normalized blendConfigPath(kfname);
blendConfigPath.changeExtension("yaml");
blendConfigPath.changeExtension(yaml);
// globalBlendConfigPath is only used with actors! Objects have no default blending.
constexpr VFS::Path::NormalizedView globalBlendConfigPath("animations/animation-config.yaml");

View File

@ -744,10 +744,12 @@ namespace MWRender
if (activeGrid && type != ESM::REC_STAT && type != ESM::REC_STAT4)
{
model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS());
if (Misc::getFileExtension(model) == "nif")
constexpr VFS::Path::ExtensionView nif("nif");
if (model.extension() == nif)
{
VFS::Path::Normalized kfname = model;
kfname.changeExtension("kf");
constexpr VFS::Path::ExtensionView kf("kf");
kfname.changeExtension(kf);
if (mSceneManager->getVFS()->exists(kfname))
continue;
}

View File

@ -20,6 +20,7 @@ namespace MWSound
namespace
{
constexpr VFS::Path::NormalizedView soundDir("sound");
constexpr VFS::Path::ExtensionView mp3("mp3");
struct AudioParams
{
@ -202,9 +203,8 @@ namespace MWSound
SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound)
{
VFS::Path::Normalized path
= Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, VFS::Path::toNormalized(sound.mSoundFile),
*MWBase::Environment::get().getResourceSystem()->getVFS(), "mp3");
VFS::Path::Normalized path = Misc::ResourceHelpers::correctResourcePath({ { soundDir } },
VFS::Path::toNormalized(sound.mSoundFile), *MWBase::Environment::get().getResourceSystem()->getVFS(), mp3);
float volume = 1, min = 1, max = 255; // TODO: needs research
SoundBuffer& sfx = mSoundBuffers.emplace_back(std::move(path), volume, min, max);
mBufferNameMap.emplace(soundId, &sfx);
@ -213,9 +213,8 @@ namespace MWSound
SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound)
{
VFS::Path::Normalized path
= Misc::ResourceHelpers::correctResourcePath({ { soundDir } }, VFS::Path::toNormalized(sound.mSoundFile),
*MWBase::Environment::get().getResourceSystem()->getVFS(), "mp3");
VFS::Path::Normalized path = Misc::ResourceHelpers::correctResourcePath({ { soundDir } },
VFS::Path::toNormalized(sound.mSoundFile), *MWBase::Environment::get().getResourceSystem()->getVFS(), mp3);
float volume = 1, min = 1, max = 255; // TODO: needs research
// TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to
// ESMStore.

View File

@ -122,10 +122,12 @@ namespace MWWorld
if (!vfs.exists(mesh))
continue;
if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif")
constexpr VFS::Path::ExtensionView nif("nif");
if (Misc::getFileName(mesh).starts_with('x') && mesh.extension() == nif)
{
kfname = mesh;
kfname.changeExtension("kf");
constexpr VFS::Path::ExtensionView kf("kf");
kfname.changeExtension(kf);
if (vfs.exists(kfname))
mPreloadedObjects.insert(mKeyframeManager->get(kfname));
}

View File

@ -19,7 +19,10 @@ namespace
constexpr VFS::Path::NormalizedView bookart("bookart");
constexpr VFS::Path::NormalizedView icons("icons");
constexpr VFS::Path::NormalizedView materials("materials");
constexpr std::string_view dds("dds");
constexpr VFS::Path::ExtensionView dds("dds");
constexpr VFS::Path::ExtensionView kf("kf");
constexpr VFS::Path::ExtensionView nif("nif");
constexpr VFS::Path::ExtensionView mp3("mp3");
bool changeExtension(std::string& path, std::string_view ext)
{
@ -67,7 +70,7 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path)
// If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension.
VFS::Path::Normalized Misc::ResourceHelpers::correctResourcePath(
std::span<const VFS::Path::NormalizedView> topLevelDirectories, VFS::Path::NormalizedView resPath,
const VFS::Manager& vfs, std::string_view ext)
const VFS::Manager& vfs, VFS::Path::ExtensionView ext)
{
VFS::Path::Normalized correctedPath;
@ -167,8 +170,8 @@ VFS::Path::Normalized Misc::ResourceHelpers::correctActorModelPath(
mdlname.insert(mdlname.begin(), 'x');
VFS::Path::Normalized kfname(mdlname);
if (Misc::getFileExtension(mdlname) == "nif")
kfname.changeExtension("kf");
if (kfname.extension() == nif)
kfname.changeExtension(kf);
if (!vfs->exists(kfname))
return VFS::Path::Normalized(resPath);
@ -215,16 +218,16 @@ VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs)
{
// Note: likely should be replaced with
// return correctResourcePath({ { "sound" } }, resPath, vfs, "mp3");
// return correctResourcePath({ { "sound" } }, resPath, vfs, mp3);
// but there is a slight difference in behaviour:
// - `correctResourcePath(..., "mp3")` first checks `.mp3`, then tries the original extension
// - `correctResourcePath(..., mp3)` first checks `.mp3`, then tries the original extension
// - the implementation below first tries the original extension, then falls back to `.mp3`.
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if (!vfs.exists(resPath))
{
VFS::Path::Normalized sound(resPath);
sound.changeExtension("mp3");
sound.changeExtension(mp3);
return sound;
}
return VFS::Path::Normalized(resPath);

View File

@ -26,7 +26,7 @@ namespace Misc
{
bool changeExtensionToDds(std::string& path);
VFS::Path::Normalized correctResourcePath(std::span<const VFS::Path::NormalizedView> topLevelDirectories,
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs, std::string_view ext = {});
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs, VFS::Path::ExtensionView ext = {});
VFS::Path::Normalized correctTexturePath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs);
VFS::Path::Normalized correctIconPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs);
VFS::Path::Normalized correctBookartPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs);

View File

@ -52,7 +52,8 @@ namespace Resource
, mPath(path)
, mVFS(&vfs)
{
mPath.changeExtension("txt");
constexpr VFS::Path::ExtensionView txt("txt");
mPath.changeExtension(txt);
}
bool RetrieveAnimationsVisitor::belongsToLeftUpperExtremity(const std::string& name)
@ -214,7 +215,8 @@ namespace Resource
return osg::ref_ptr<const SceneUtil::KeyframeHolder>(static_cast<SceneUtil::KeyframeHolder*>(obj.get()));
osg::ref_ptr<SceneUtil::KeyframeHolder> loaded(new SceneUtil::KeyframeHolder);
if (Misc::getFileExtension(name.value()) == "kf")
constexpr VFS::Path::ExtensionView kf("kf");
if (name.extension() == kf)
{
auto file = std::make_shared<Nif::NIFFile>(name);
Nif::Reader reader(*file, mEncoder);

View File

@ -947,10 +947,20 @@ namespace Resource
osg::ref_ptr<osg::Node> SceneManager::loadErrorMarker()
{
constexpr VFS::Path::ExtensionView meshTypes[] = {
VFS::Path::ExtensionView("nif"),
VFS::Path::ExtensionView("osg"),
VFS::Path::ExtensionView("osgt"),
VFS::Path::ExtensionView("osgb"),
VFS::Path::ExtensionView("osgx"),
VFS::Path::ExtensionView("osg2"),
VFS::Path::ExtensionView("dae"),
};
try
{
VFS::Path::Normalized path("meshes/marker_error.****");
for (const auto meshType : { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" })
for (const VFS::Path::ExtensionView meshType : meshTypes)
{
path.changeExtension(meshType);
if (mVFS->exists(path))

View File

@ -107,6 +107,67 @@ namespace VFS::Path
return std::find_if(begin, end, [](char v) { return v == extensionSeparator || v == separator; });
}
inline constexpr bool isExtension(std::string_view value)
{
return isNormalized(value) && findSeparatorOrExtensionSeparator(value.begin(), value.end()) == value.end();
}
class NormalizedView;
class ExtensionView
{
public:
constexpr ExtensionView() noexcept = default;
constexpr explicit ExtensionView(const char* value)
: mValue(value)
{
if (!isExtension(mValue))
throw std::invalid_argument(
"ExtensionView value is invalid extension: \"" + std::string(mValue) + "\"");
}
constexpr std::string_view value() const noexcept { return mValue; }
constexpr bool empty() const noexcept { return mValue.empty(); }
friend constexpr bool operator==(const ExtensionView& lhs, const ExtensionView& rhs) = default;
friend constexpr bool operator==(const ExtensionView& lhs, const auto& rhs) { return lhs.mValue == rhs; }
#if defined(_MSC_VER) && _MSC_VER <= 1935
friend constexpr bool operator==(const auto& lhs, const ExtensionView& rhs)
{
return lhs == rhs.mValue;
}
#endif
friend constexpr bool operator<(const ExtensionView& lhs, const ExtensionView& rhs)
{
return lhs.mValue < rhs.mValue;
}
friend constexpr bool operator<(const ExtensionView& lhs, const auto& rhs)
{
return lhs.mValue < rhs;
}
friend constexpr bool operator<(const auto& lhs, const ExtensionView& rhs)
{
return lhs < rhs.mValue;
}
friend std::ostream& operator<<(std::ostream& stream, const ExtensionView& value)
{
return stream << value.mValue;
}
private:
std::string_view mValue;
friend class NormalizedView;
};
class Normalized;
class NormalizedView
@ -191,6 +252,15 @@ namespace VFS::Path
return result;
}
constexpr ExtensionView extension() const
{
ExtensionView result;
if (const std::size_t position = mValue.find_last_of(extensionSeparator);
position != std::string_view::npos)
result.mValue = mValue.substr(position + 1);
return result;
}
private:
std::string_view mValue;
};
@ -238,18 +308,13 @@ namespace VFS::Path
operator const std::string&() const { return mValue; }
bool changeExtension(std::string_view extension)
bool changeExtension(ExtensionView extension)
{
if (!isNormalized(extension))
throw std::invalid_argument("Not normalized extension: " + std::string(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);
std::transform(mValue.begin() + pos, mValue.end(), mValue.begin() + pos, normalize);
mValue.replace(pos, mValue.size(), extension.value());
return true;
}
@ -342,6 +407,11 @@ namespace VFS::Path
return NormalizedView(*this).filename();
}
ExtensionView extension() const
{
return NormalizedView(*this).extension();
}
private:
std::string mValue;
};