Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions src/mp_audio/MasterpieceProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2500,15 +2500,6 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
return result;
}

// A Hauptwerk set puts its definitions in <root>/OrganDefinitions and its
// audio in <root>/OrganInstallationPackages, so the root is the definition's
// grandparent. deriveOrganRoot (mp_core, shared with the loader so the two
// never disagree) also copes with a set that has been reorganised with
// symlinks -- OrganDefinitions or OrganInstallationPackages relocated onto
// another drive -- where the plain parent walk can land somewhere that no
// longer has OrganInstallationPackages beside it.
const juce::File root(mp::deriveOrganRoot(odfFile.getFullPathName().toStdString()));

// What this organ was last set to. Has to happen before a byte of audio is
// read: the resident format, streaming and the preload head all decide how
// the samples are read and cannot be changed afterwards.
Expand All @@ -2528,7 +2519,31 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
p->setValueNotifyingHost(p->convertTo0to1(1.0f));

loadGlobalDefaults();
loadSettingsFor(odfFile);
seedSampleLibraries();

// The path the definition is known by. A native file chooser can hand back
// a symlinked OrganDefinitions already resolved, which loses the folder its
// packages sit beside; when the file lies under a known library's linked
// OrganDefinitions, the path is rebuilt through that link. Everything below
// -- the root, the settings, the cache and the organ to reopen -- uses it.
std::vector<std::string> libraryRoots;
libraryRoots.reserve(libraries_.size());
for (const auto& library : libraries_)
libraryRoots.push_back(library.getFullPathName().toStdString());
const juce::File effectiveOdf(mp::restoreLogicalOdfPath(
odfFile.getFullPathName().toStdString(), libraryRoots));

// A Hauptwerk set puts its definitions in <root>/OrganDefinitions and its
// audio in <root>/OrganInstallationPackages, so the root is the definition's
// grandparent. deriveOrganRoot (mp_core, shared with the loader so the two
// never disagree) also copes with a set that has been reorganised with
// symlinks -- OrganDefinitions or OrganInstallationPackages relocated onto
// another drive -- where the plain parent walk can land somewhere that no
// longer has OrganInstallationPackages beside it.
const juce::File root(
mp::deriveOrganRoot(effectiveOdf.getFullPathName().toStdString()));

loadSettingsFor(effectiveOdf);

OdfLoader loader;
OdfLoader::Options opts;
Expand All @@ -2544,7 +2559,7 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
}

OrganModel loaded;
if (!loader.load(odfFile.getFullPathName().toStdString(), opts, loaded,
if (!loader.load(effectiveOdf.getFullPathName().toStdString(), opts, loaded,
result.diagnostics)) {
result.error = result.diagnostics.errors.empty()
? "the organ definition could not be parsed"
Expand Down Expand Up @@ -2576,7 +2591,7 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(

model_ = std::move(loaded);
organRootDir_ = opts.organRootDir;
loadedOdf_ = odfFile;
loadedOdf_ = effectiveOdf;

// Console click -> stop. Without this a drawstop would move on screen and
// the organ would stay silent, which is the worst of both.
Expand Down Expand Up @@ -2888,9 +2903,9 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
samples_.setCacheDir(cacheDirectory().getFullPathName().toStdString());
samples_.setCacheIdentity(
organKey(),
odfFile.getFullPathName().toStdString() + "|" +
std::to_string(odfFile.getSize()) + "|" +
std::to_string(odfFile.getLastModificationTime().toMilliseconds()));
effectiveOdf.getFullPathName().toStdString() + "|" +
std::to_string(effectiveOdf.getSize()) + "|" +
std::to_string(effectiveOdf.getLastModificationTime().toMilliseconds()));

result.samples = samples_.loadAll(model_, opts.organRootDir, head,
LoopSelection::Longest, &loadProgress_,
Expand Down Expand Up @@ -2971,7 +2986,7 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(

// Only now, having got this far: an organ that failed to load is not one
// worth reopening on the next start.
setLastOrgan(odfFile);
setLastOrgan(effectiveOdf);
// And the library it came from, so a definition moved away from its audio
// later can still be matched to it.
if (!graphicsOnly) rememberSampleLibrary(juce::File(organRootDir_));
Expand Down
54 changes: 54 additions & 0 deletions src/mp_core/OdfLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,60 @@ bool hasInstallationPackages(const std::filesystem::path& root) {

} // namespace

std::string restoreLogicalOdfPath(
const std::string& odfPath,
const std::vector<std::string>& libraryRoots) {
namespace fs = std::filesystem;

std::error_code ec;
const fs::path selected =
fs::weakly_canonical(fs::path(odfPath), ec);

if (ec) return odfPath;

for (const auto& root : libraryRoots) {
const fs::path logicalDefinitions =
fs::path(root) / "OrganDefinitions";

ec.clear();
if (!fs::is_directory(logicalDefinitions, ec))
continue;

ec.clear();
const fs::path physicalDefinitions =
fs::weakly_canonical(logicalDefinitions, ec);

if (ec)
continue;

const fs::path relative =
selected.lexically_relative(physicalDefinitions);

if (relative.empty() || relative.is_absolute())
continue;

bool escapes = false;
for (const auto& part : relative) {
if (part == "..") {
escapes = true;
break;
}
}

if (escapes)
continue;

const fs::path candidate =
logicalDefinitions / relative;

ec.clear();
if (fs::is_regular_file(candidate, ec))
return candidate.string();
}

return odfPath;
}

std::string findLibraryHolding(const std::vector<std::string>& roots,
const OrganModel& model) {
std::vector<Id> wanted;
Expand Down
8 changes: 8 additions & 0 deletions src/mp_core/OdfLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ class OdfLoader {
OdfDiagnostics& outDiag);
};

// A native file chooser may resolve a symlinked OrganDefinitions directory
// before returning the selected file. If the physical file belongs underneath
// the OrganDefinitions directory of one of the known library roots, rebuild
// the equivalent path through that logical directory.
std::string restoreLogicalOdfPath(
const std::string& odfPath,
const std::vector<std::string>& libraryRoots);

// Given the path to a *.Organ_Hauptwerk_xml (or *.CustomOrgan_Hauptwerk_xml)
// file, return the directory OrganInstallationPackages and OrganDefinitions
// hang off. Ordinarily that is just the ODF's grandparent — <root>/
Expand Down
7 changes: 7 additions & 0 deletions tests/test_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6164,6 +6164,13 @@ class SymlinkedOrganTest final : public mp::test::Test {

// Through the resolved path it cannot, and must not invent one.
const fs::path resolved = defsElsewhere / "test.Organ_Hauptwerk_xml";
const std::string restored =
mp::restoreLogicalOdfPath(resolved.string(), {setRoot.string()});

MP_CHECK(fs::path(restored).lexically_normal() ==
throughLink.lexically_normal(),
"layout D: a resolved ODF path must be restored through the "
"known OrganDefinitions symlink");
const std::string derived = mp::deriveOrganRoot(resolved.string());
MP_CHECK(!fs::is_directory(fs::path(derived) / "OrganInstallationPackages", ec),
"layout D: a resolved path genuinely has no packages to find");
Expand Down
Loading