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);
 

Reply via email to