Follow TEXMAP texture search order

Search all texture locations with the textures/ prefix before trying bare filenames, so model-folder images cannot override prefixed library textures. Resolve nested textures across LDraw library roots and retain the selected disk path or archive entry for decoding and reuse.

Index disk-library PNG paths when loading the library to avoid repeated directory scans on the UI thread. Parse quoted filenames and escaped quotes and backslashes in all TEXMAP projection methods, and reject names that exceed texture storage.

Validated with the macOS debug build and focused checks for disk and archive precedence, nested and mixed-case paths, exact archive decoding, source reuse, reloads, symlinks, and filename parsing.
This commit is contained in:
Leonardo Zide committed 2026-10-08 10:54:38 -07:00
1 parent 7e9283fec0
commit 202035d4ea
6 files changed
+175 -126

No files matched your search

+2 -1
View File
@@ -52,8 +52,9 @@ struct lcPartBuildResult
struct lcTextureSourceSnapshot
{
QString Name;
QString FilePath;
int ArchiveType = -1;
int ArchiveIndex = -1;
};
struct lcTextureBuildResult
+103 -98
View File
@@ -110,6 +110,10 @@ void lcPiecesLibrary::Unload()
delete Texture;
mTextures.clear();
mTextureDiskEntries.clear();
for (std::map<QString, int>& Entries : mTextureArchiveEntries)
Entries.clear();
mNumOfficialPieces = 0;
@@ -578,23 +582,76 @@ lcTexture* lcPiecesLibrary::FindTexture(const char* TextureName, Project* Curren
lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories)
{
if (!TextureName[0] || strlen(TextureName) >= LC_TEXTURE_NAME_LEN)
return nullptr;
QMutexLocker LoadLock(&mLoadMutex);
QString FilePath;
QString ResolvedProjectPath;
int ArchiveType = -1;
int ArchiveIndex = -1;
const QString Name = QString::fromLatin1(TextureName);
const QString Candidates[] = { QLatin1String("textures/") + Name, Name };
const QString Folders[] = { QStringLiteral("p/"), QStringLiteral("parts/"), QStringLiteral("models/"), QStringLiteral("unofficial/p/"), QStringLiteral("unofficial/parts/") };
for (const QString& Directory : SearchDirectories)
// Complete the prefixed search before considering any unprefixed location.
for (const QString& Candidate : Candidates)
{
if (Directory.isEmpty())
continue;
for (const QString& Directory : SearchDirectories)
{
if (Directory.isEmpty())
continue;
FilePath = FindProjectTextureFile(Directory, QString::fromLatin1(TextureName));
FilePath = FindProjectTextureFile(Directory, Candidate);
if (!FilePath.isEmpty())
{
ResolvedProjectPath = Directory;
break;
}
}
if (!FilePath.isEmpty())
{
ResolvedProjectPath = Directory;
break;
for (const QString& Folder : Folders)
{
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
{
const bool Unofficial = Folder.startsWith(QLatin1String("unofficial/"));
const int Type = static_cast<int>(Unofficial ? lcZipFileType::Unofficial : lcZipFileType::Official);
QString EntryName;
if (Unofficial)
EntryName = Folder.mid(11) + Candidate + QLatin1String(".png");
else
EntryName = QLatin1String("ldraw/") + Folder + Candidate + QLatin1String(".png");
const auto Entry = mTextureArchiveEntries[Type].find(QDir::cleanPath(EntryName).toUpper());
if (Entry != mTextureArchiveEntries[Type].end())
{
ArchiveType = Type;
ArchiveIndex = Entry->second;
break;
}
}
else
{
const QString EntryName = QDir::cleanPath(Folder + Candidate + QLatin1String(".png")).toUpper();
const auto Entry = mTextureDiskEntries.find(EntryName);
if (Entry != mTextureDiskEntries.end())
{
FilePath = Entry->second;
break;
}
}
}
if (!FilePath.isEmpty() || ArchiveIndex != -1)
break;
}
if (!FilePath.isEmpty())
@@ -605,13 +662,7 @@ lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const s
for (lcTexture* Texture : mTextures)
{
if (strcmp(TextureName, Texture->mName))
continue;
if (Texture->IsTemporary() != !FilePath.isEmpty())
continue;
if (Texture->IsTemporary() && Texture->mFilePath != FilePath)
if (Texture->mFilePath != FilePath || Texture->mArchiveType != ArchiveType || Texture->mArchiveIndex != ArchiveIndex)
continue;
Texture->AddRef();
@@ -619,7 +670,7 @@ lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const s
return Texture;
}
if (FilePath.isEmpty())
if (FilePath.isEmpty() && ArchiveIndex == -1)
return nullptr;
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
@@ -627,7 +678,9 @@ lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const s
lcstrcpy(Texture->mName, TextureName);
Texture->mFilePath = FilePath;
Texture->mProjectPath = ResolvedProjectPath;
Texture->SetTemporary(true);
Texture->mArchiveType = ArchiveType;
Texture->mArchiveIndex = ArchiveIndex;
Texture->SetTemporary(!ResolvedProjectPath.isEmpty());
Texture->AddRef();
mTextures.push_back(Texture);
@@ -639,8 +692,9 @@ lcTextureSourceSnapshot lcPiecesLibrary::SnapshotTextureSource(const lcTexture*
{
lcTextureSourceSnapshot Source;
Source.Name = QString::fromLatin1(Texture->mName);
Source.FilePath = Texture->mFilePath;
Source.ArchiveType = Texture->mArchiveType;
Source.ArchiveIndex = Texture->mArchiveIndex;
return Source;
}
@@ -655,28 +709,17 @@ lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcTextureSourceSnap
if (!Source.FilePath.isEmpty())
Loaded = Result.DecodedImage->FileLoad(Source.FilePath);
else
else if (Source.ArchiveType >= 0 && Source.ArchiveType < static_cast<int>(lcZipFileType::Count) && Source.ArchiveIndex >= 0)
{
lcMemFile TextureFile;
QByteArray Name = Source.Name.toLatin1();
char FileName[2 * LC_MAXPATH];
bool Extracted = false;
{
QMutexLocker TextureLock(&mTextureMutex);
const std::unique_ptr<lcZipFile>& Archive = mZipFiles[Source.ArchiveType];
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
{
snprintf(FileName, sizeof(FileName), "ldraw/parts/textures/%s.png", Name.constData());
Extracted = mZipFiles[static_cast<int>(lcZipFileType::Official)]->ExtractFile(FileName, TextureFile);
if (!Extracted && mZipFiles[static_cast<int>(lcZipFileType::Unofficial)])
{
snprintf(FileName, sizeof(FileName), "parts/textures/%s.png", Name.constData());
Extracted = mZipFiles[static_cast<int>(lcZipFileType::Unofficial)]->ExtractFile(FileName, TextureFile);
}
}
if (Archive)
Extracted = Archive->ExtractFile(Source.ArchiveIndex, TextureFile);
}
if (Extracted)
@@ -848,6 +891,8 @@ bool lcPiecesLibrary::OpenArchive(std::unique_ptr<lcFile> File, lcZipFileType Zi
if (!ZipFile->OpenRead(std::move(File)))
return false;
mTextureArchiveEntries[static_cast<int>(ZipFileType)].clear();
std::unique_ptr<lcLibrarySource> Source(new lcLibrarySource);
Source->Type = ZipFileType != lcZipFileType::StudStyle ? lcLibrarySourceType::Library : lcLibrarySourceType::StudStyle;
@@ -882,16 +927,8 @@ bool lcPiecesLibrary::OpenArchive(std::unique_ptr<lcFile> File, lcZipFileType Zi
{
if (!memcmp(Dst, ".PNG", 4))
{
if ((ZipFileType == lcZipFileType::Official && !memcmp(Name, "LDRAW/PARTS/TEXTURES/", 21)) ||
(ZipFileType == lcZipFileType::Unofficial && !memcmp(Name, "PARTS/TEXTURES/", 15)))
{
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
mTextures.push_back(Texture);
*Dst = 0;
strncpy(Texture->mName, Name + (ZipFileType == lcZipFileType::Official ? 21 : 15), sizeof(Texture->mName)-1);
Texture->mName[sizeof(Texture->mName) - 1] = 0;
}
if (ZipFileType == lcZipFileType::Official || ZipFileType == lcZipFileType::Unofficial)
mTextureArchiveEntries[static_cast<int>(ZipFileType)].emplace(QDir::cleanPath(QString::fromLatin1(Name)).toUpper(), FileIdx);
}
continue;
@@ -1001,6 +1038,8 @@ void lcPiecesLibrary::ReadArchiveDescriptions(const QString& OfficialFileName, c
bool lcPiecesLibrary::OpenDirectory(const QDir& LibraryDir, bool ShowProgress)
{
mTextureDiskEntries.clear();
const QLatin1String BaseFolders[] = { QLatin1String(""), QLatin1String("unofficial/") };
constexpr int NumBaseFolders = LC_ARRAY_COUNT(BaseFolders);
@@ -1074,50 +1113,22 @@ bool lcPiecesLibrary::OpenDirectory(const QDir& LibraryDir, bool ShowProgress)
}
mSources.emplace_back(std::move(Source));
}
for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < LC_ARRAY_COUNT(BaseFolders); BaseFolderIdx++)
{
QDir BaseDir(LibraryDir.absoluteFilePath(QLatin1String(BaseFolders[BaseFolderIdx])));
QDir Dir(BaseDir.absoluteFilePath(QLatin1String("parts/textures/")), QLatin1String("*.png"), QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files | QDir::Hidden | QDir::Readable);
QStringList FileList = Dir.entryList();
// Index PNG paths once; UI-thread texture lookup must not enumerate library folders.
const char* TextureDirectories[] = { "p/", "parts/", "models/" };
mTextures.reserve(mTextures.size() + FileList.size());
for (int FileIdx = 0; FileIdx < FileList.size(); FileIdx++)
for (const char* TextureDirectory : TextureDirectories)
{
char Name[LC_MAXPATH];
QByteArray FileString = FileList[FileIdx].toLatin1();
const char* Src = FileString;
char* Dst = Name;
const QString ChildPath = BaseDir.absoluteFilePath(QLatin1String(TextureDirectory));
QDirIterator DirIterator(ChildPath, QStringList() << QLatin1String("*.png"), QDir::Files | QDir::Hidden | QDir::Readable, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while (*Src && Dst - Name < (int)sizeof(Name))
while (DirIterator.hasNext())
{
if (*Src >= 'a' && *Src <= 'z')
*Dst = *Src + 'A' - 'a';
else if (*Src == '\\')
*Dst = '/';
else
*Dst = *Src;
const QString FilePath = DirIterator.next();
const QString Name = QDir::cleanPath(LibraryDir.relativeFilePath(FilePath)).toUpper();
Src++;
Dst++;
mTextureDiskEntries.emplace(Name, FilePath);
}
if (Dst - Name <= 4)
continue;
Dst -= 4;
if (memcmp(Dst, ".PNG", 4))
continue;
*Dst = 0;
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
mTextures.push_back(Texture);
strncpy(Texture->mName, Name, sizeof(Texture->mName));
Texture->mName[sizeof(Texture->mName) - 1] = 0;
Texture->mFilePath = Dir.absoluteFilePath(FileList[FileIdx]);
}
}
@@ -2237,27 +2248,21 @@ void lcPiecesLibrary::UnloadUnusedParts()
bool lcPiecesLibrary::LoadTexture(lcTexture* Texture)
{
QMutexLocker Lock(&mTextureMutex);
char FileName[2*LC_MAXPATH];
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
{
lcMemFile TextureFile;
snprintf(FileName, sizeof(FileName), "ldraw/parts/textures/%s.png", Texture->mName);
if (!mZipFiles[static_cast<int>(lcZipFileType::Official)]->ExtractFile(FileName, TextureFile))
{
snprintf(FileName, sizeof(FileName), "parts/textures/%s.png", Texture->mName);
if (!mZipFiles[static_cast<int>(lcZipFileType::Unofficial)] || !mZipFiles[static_cast<int>(lcZipFileType::Unofficial)]->ExtractFile(FileName, TextureFile))
return false;
}
return Texture->Load(TextureFile);
}
else
if (!Texture->mFilePath.isEmpty())
return Texture->Load(Texture->mFilePath);
QMutexLocker Lock(&mTextureMutex);
const int Type = Texture->mArchiveType;
if (Type < 0 || Type >= static_cast<int>(lcZipFileType::Count) || Texture->mArchiveIndex < 0 || !mZipFiles[Type])
return false;
lcMemFile TextureFile;
if (!mZipFiles[Type]->ExtractFile(Texture->mArchiveIndex, TextureFile))
return false;
return Texture->Load(TextureFile);
}
void lcPiecesLibrary::ReleaseTexture(lcTexture* Texture)
+2
View File
@@ -284,6 +284,8 @@ public:
int mNumOfficialPieces;
std::vector<lcTexture*> mTextures;
std::map<QString, QString> mTextureDiskEntries;
std::map<QString, int> mTextureArchiveEntries[static_cast<int>(lcZipFileType::Count)];
QDir mLibraryDir;
+65 -27
View File
@@ -1327,6 +1327,47 @@ bool lcMeshLoader::LoadMesh(lcFile& File, lcMeshDataType MeshDataType)
return ReadMeshData(File, lcMatrix44Identity(), 16, false, MeshDataType);
}
bool lcMeshLoader::ParseTextureName(const char* Text, char* Name, size_t NameSize)
{
while (*Text && *Text <= 32)
Text++;
const bool Quoted = *Text == '"';
if (Quoted)
Text++;
size_t Length = 0;
while (*Text && (Quoted ? *Text != '"' : *Text > 32))
{
char Character = *Text++;
if (Character == '\\' && (*Text == '"' || *Text == '\\'))
Character = *Text++;
if (Length + 1 >= NameSize || Length + 1 >= LC_TEXTURE_NAME_LEN)
return false;
if (Character >= 'a' && Character <= 'z')
Character += 'A' - 'a';
else if (Character == '\\')
Character = '/';
Name[Length++] = Character;
}
if (!Length || (Quoted && *Text != '"'))
return false;
Name[Length] = 0;
if (Length > 4 && !memcmp(Name + Length - 4, ".PNG", 4))
Name[Length - 4] = 0;
return true;
}
bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform, quint32 CurrentColorCode, bool InvertWinding, lcMeshDataType MeshDataType)
{
char Buffer[1024];
@@ -1409,24 +1450,6 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
End++;
*End = 0;
auto CleanTextureName = [](char* FileName)
{
char* Ch;
for (Ch = FileName; *Ch; Ch++)
{
if (*Ch >= 'a' && *Ch <= 'z')
*Ch = *Ch + 'A' - 'a';
else if (*Ch == '\\')
*Ch = '/';
}
if (Ch - FileName > 4)
{
Ch -= 4;
if (!memcmp(Ch, ".PNG", 4))
*Ch = 0;
}
};
if (!strcmp(Token, "PLANAR"))
{
@@ -1439,13 +1462,18 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
lcVector3 (&Points)[3] = Map.Points;
sscanf(Token, "%f %f %f %f %f %f %f %f %f %s", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, Map.Name);
int NameOffset = 0;
const int Parsed = sscanf(Token, "%f %f %f %f %f %f %f %f %f %n", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, &NameOffset);
if (Parsed != 9 || !ParseTextureName(Token + NameOffset, Map.Name, sizeof(Map.Name)))
{
mTextureStack.pop_back();
continue;
}
Points[0] = lcMul31(Points[0], CurrentTransform);
Points[1] = lcMul31(Points[1], CurrentTransform);
Points[2] = lcMul31(Points[2], CurrentTransform);
CleanTextureName(Map.Name);
}
else if (!strcmp(Token, "CYLINDRICAL"))
{
@@ -1459,13 +1487,18 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
lcVector3 (&Points)[3] = Map.Points;
float& Angle = Map.Angles[0];
sscanf(Token, "%f %f %f %f %f %f %f %f %f %f %s", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, &Angle, Map.Name);
int NameOffset = 0;
const int Parsed = sscanf(Token, "%f %f %f %f %f %f %f %f %f %f %n", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, &Angle, &NameOffset);
if (Parsed != 10 || !ParseTextureName(Token + NameOffset, Map.Name, sizeof(Map.Name)))
{
mTextureStack.pop_back();
continue;
}
Points[0] = lcMul31(Points[0], CurrentTransform);
Points[1] = lcMul31(Points[1], CurrentTransform);
Points[2] = lcMul31(Points[2], CurrentTransform);
CleanTextureName(Map.Name);
}
else if (!strcmp(Token, "SPHERICAL"))
{
@@ -1480,13 +1513,18 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
float& Angle1 = Map.Angles[0];
float& Angle2 = Map.Angles[1];
sscanf(Token, "%f %f %f %f %f %f %f %f %f %f %f %s", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, &Angle1, &Angle2, Map.Name);
int NameOffset = 0;
const int Parsed = sscanf(Token, "%f %f %f %f %f %f %f %f %f %f %f %n", &Points[0].x, &Points[0].y, &Points[0].z, &Points[1].x, &Points[1].y, &Points[1].z, &Points[2].x, &Points[2].y, &Points[2].z, &Angle1, &Angle2, &NameOffset);
if (Parsed != 11 || !ParseTextureName(Token + NameOffset, Map.Name, sizeof(Map.Name)))
{
mTextureStack.pop_back();
continue;
}
Points[0] = lcMul31(Points[0], CurrentTransform);
Points[1] = lcMul31(Points[1], CurrentTransform);
Points[2] = lcMul31(Points[2], CurrentTransform);
CleanTextureName(Map.Name);
}
}
else if (!strcmp(Token, "FALLBACK"))
+1
View File
@@ -225,6 +225,7 @@ public:
Project* mCurrentProject;
protected:
static bool ParseTextureName(const char* Text, char* Name, size_t NameSize);
bool ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform, quint32 CurrentColorCode, bool InvertWinding, lcMeshDataType MeshDataType);
std::vector<lcMeshLoaderTextureMap> mTextureStack;
+2
View File
@@ -126,6 +126,8 @@ public:
char mName[LC_TEXTURE_NAME_LEN] = {};
QString mFilePath; // Absolute PNG path for disk-backed textures; empty for archive textures.
QString mProjectPath; // Directory used to scope temporary textures to their project.
int mArchiveType = -1;
int mArchiveIndex = -1;
GLuint mTexture = 0;
lcTextureState mState = lcTextureState::Unrequested;
lcTextureLoadError mLoadFailure = lcTextureLoadError::None;