comphelper/source/misc/string.cxx | 31 ++++++++++++++++++++ include/comphelper/string.hxx | 9 +++++ sd/qa/unit/data/pptx/tdf148478_transitionMusic.pptx |binary sd/qa/unit/export-tests-ooxml3.cxx | 28 ++++++++++++++++++ sd/source/filter/eppt/pptx-epptooxml.cxx | 30 +++++++++++++++++-- 5 files changed, 95 insertions(+), 3 deletions(-)
New commits: commit e9f5d6ce38830c6911f788aae0d99407d9fee467 Author: Justin Luth <[email protected]> AuthorDate: Tue Nov 4 16:35:57 2025 -0500 Commit: Justin Luth <[email protected]> CommitDate: Thu Nov 6 19:47:51 2025 +0100 tdf#148478 pptx export: only export ASCII .wav filenames MS PowerPoint reports a corrupt presentation if one of the ppt/media files contains non-ascii characters. make CppunitTest_sd_export_tests-ooxml3 \ CPPUNIT_TEST_NAME=testTdf148478_transitionMusic Change-Id: Ide5c5d38b4517e7a933fb8d414d901b49b15faca Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193435 Reviewed-by: Justin Luth <[email protected]> Tested-by: Jenkins diff --git a/comphelper/source/misc/string.cxx b/comphelper/source/misc/string.cxx index a472cf57d1e8..2da3eb444c39 100644 --- a/comphelper/source/misc/string.cxx +++ b/comphelper/source/misc/string.cxx @@ -513,6 +513,37 @@ bool isdigitAsciiString(std::u16string_view rString) [](sal_Unicode c){ return rtl::isAsciiDigit(c); }); } +bool isValidAsciiFilename(std::u16string_view rString) +{ + if (rString.empty() || rString[0] == ' ' || rString[rString.size() - 1] == ' ') + return false; + + bool bRet = std::all_of( + rString.data(), rString.data() + rString.size(), + [](sal_Unicode c) + { + if (!rtl::isAscii(c)) + return false; + switch (c) + { + case '<': + case '>': + case ':': + case '"': + case '\': + case '/': + case '?': + case '%': + case '*': + case '|': + return false; + default: + return true; + } + }); + return bRet; +} + OUString reverseString(std::u16string_view rStr) { if (rStr.empty()) diff --git a/include/comphelper/string.hxx b/include/comphelper/string.hxx index 4cc5bf2ee64b..f71d6e52fea1 100644 --- a/include/comphelper/string.hxx +++ b/include/comphelper/string.hxx @@ -375,6 +375,15 @@ COMPHELPER_DLLPUBLIC bool isdigitAsciiString(std::string_view rString); */ COMPHELPER_DLLPUBLIC bool isdigitAsciiString(std::u16string_view rString); +/** Determine if an OUString contains only valid ASCII filename characters + + @param rString An OUString + + @return false if empty string, or string contains any characters that are not allowed + in any target operating system or target file system + */ +COMPHELPER_DLLPUBLIC bool isValidAsciiFilename(std::u16string_view rString); + /** Sanitize an OUString to not have invalid surrogates @param rString An OUString diff --git a/sd/qa/unit/data/pptx/tdf148478_transitionMusic.pptx b/sd/qa/unit/data/pptx/tdf148478_transitionMusic.pptx new file mode 100644 index 000000000000..3906fda9d56d Binary files /dev/null and b/sd/qa/unit/data/pptx/tdf148478_transitionMusic.pptx differ diff --git a/sd/qa/unit/export-tests-ooxml3.cxx b/sd/qa/unit/export-tests-ooxml3.cxx index da333327fb76..c804362c9ee5 100644 --- a/sd/qa/unit/export-tests-ooxml3.cxx +++ b/sd/qa/unit/export-tests-ooxml3.cxx @@ -927,6 +927,34 @@ CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testTdf120573) "ContentType", u"audio/x-wav"); } +CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testTdf148478_transitionMusic) +{ + // This original file does not open cleanly in MS PowerPoint + // because the file name has a Unicode character: ../media/Cortázar.wav. + createSdImpressDoc("pptx/tdf148478_transitionMusic.pptx"); + save(u"Impress Office Open XML"_ustr); + + // an MD5-based ascii replacement was substituted for the unicode name. This now opens in MS PP. + uno::Reference<packages::zip::XZipFileAccess2> xNameAccess + = packages::zip::ZipFileAccess::createWithURL(comphelper::getComponentContext(m_xSFactory), + maTempFile.GetURL()); + CPPUNIT_ASSERT( + xNameAccess->hasByName(u"ppt/media/audio_ac976207e31e8b69f8b3c3d981097f3d.wav"_ustr)); + + xmlDocUniquePtr pXmlDocRels = parseExport(u"ppt/slides/_rels/slide1.xml.rels"_ustr); + assertXPath(pXmlDocRels, + "(/rels:Relationships/rels:Relationship[@Target='../media/" + "audio_ac976207e31e8b69f8b3c3d981097f3d.wav'])", + "Type", + u"http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"); + + xmlDocUniquePtr pXmlContentType = parseExport(u"[Content_Types].xml"_ustr); + assertXPath(pXmlContentType, + "/ContentType:Types/ContentType:Override[@PartName='/ppt/media/" + "audio_ac976207e31e8b69f8b3c3d981097f3d.wav']", + "ContentType", u"audio/x-wav"); +} + CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testTdf119118) { createSdImpressDoc("pptx/tdf119118.pptx"); diff --git a/sd/source/filter/eppt/pptx-epptooxml.cxx b/sd/source/filter/eppt/pptx-epptooxml.cxx index 6ae52a0092b3..15e81bb0de40 100644 --- a/sd/source/filter/eppt/pptx-epptooxml.cxx +++ b/sd/source/filter/eppt/pptx-epptooxml.cxx @@ -38,12 +38,14 @@ #include <comphelper/sequenceashashmap.hxx> #include <comphelper/storagehelper.hxx> +#include <comphelper/string.hxx> #include <comphelper/xmltools.hxx> #include <sax/fshelper.hxx> #include <sax/fastattribs.hxx> #include <rtl/ustrbuf.hxx> #include <sal/log.hxx> #include <tools/UnitConversion.hxx> +#include <tools/urlobj.hxx> #include <tools/datetime.hxx> #include <unotools/securityoptions.hxx> #include <com/sun/star/animations/TransitionType.hpp> @@ -2804,10 +2806,32 @@ void PowerPointExport::embedEffectAudio(const FSHelperPtr& pFS, const OUString& if (!xAudioStream.is()) return; - int nLastSlash = sUrl.lastIndexOf('/'); - sName = sUrl.copy(nLastSlash >= 0 ? nLastSlash + 1 : 0); + sName = INetURLObject::decode(sUrl, INetURLObject::DecodeMechanism::ToIUri, RTL_TEXTENCODING_UTF8); + int nLastSlash = sName.lastIndexOf('/'); + sName = sName.copy(nLastSlash >= 0 ? nLastSlash + 1 : 0); - OUString sPath = "../media/" + sName; + // MS PowerPoint reports a corrupt file if the media name contains non-ascii characters + OUString sAsciiName; + if (comphelper::string::isValidAsciiFilename(sName)) + sAsciiName = sName; + else + { + // create an ASCII name - using a hash to try and keep it unique and yet non-random + comphelper::Hash aHash(comphelper::HashType::MD5); + sal_Int32 nBytesToRead = std::clamp<sal_Int32>(xAudioStream->available(), 0, 32000); + uno::Sequence<sal_Int8> aTempBuf(nBytesToRead); + if ((nBytesToRead = xAudioStream->readBytes(aTempBuf, nBytesToRead))) + aHash.update(aTempBuf.getConstArray(), nBytesToRead); + else // safety fallback: use the name to create a hash: should never happen + { + const OString sUtf(OUStringToOString(sName, RTL_TEXTENCODING_UTF8)); + aHash.update(sUtf.getStr(), sUtf.getLength()); + } + sAsciiName + = "audio_" + OUString::fromUtf8(comphelper::hashToString(aHash.finalize())) + ".wav"; + } + + OUString sPath = "../media/" + sAsciiName; sRelId = addRelation(pFS->getOutputStream(), oox::getRelationship(Relationship::AUDIO), sPath);
