From 159f0d5e600b6527661023fa9374bddf003000d3 Mon Sep 17 00:00:00 2001
From: Michael Andres <ma@suse.de>
Date: Fri, 14 Mar 2025 16:30:02 +0100
Subject: [PATCH] Fix lost double slash when appending to an absolute FTP url
 (bsc#1238315)

Ftp actually differs between absolute and relative URL paths. Absolute
path names begin with a double slash encoded as '/%2F'. This must be
preserved when manipulating the path.
---
 tests/zypp/MediaSetAccess_test.cc       | 12 +++++++
 tests/zypp/Url_test.cc                  | 47 +++++++++++++++++++++++++
 zypp-core/Url.cc                        | 24 ++++++++++++-
 zypp/media/MediaNetworkCommonHandler.cc | 12 ++-----
 4 files changed, 85 insertions(+), 10 deletions(-)

diff --git a/tests/zypp/MediaSetAccess_test.cc b/tests/zypp/MediaSetAccess_test.cc
index f91fd4b8f..7fc30968a 100644
--- a/tests/zypp/MediaSetAccess_test.cc
+++ b/tests/zypp/MediaSetAccess_test.cc
@@ -328,5 +328,17 @@ BOOST_AUTO_TEST_CASE(msa_remote_tests_ftp)
   msa_remote_tests_impl<FtpServer>();
 }
 
+BOOST_AUTO_TEST_CASE(ftp_absolute_path)
+{
+  FtpServer srv( DATADIR, 10002 );
+  BOOST_REQUIRE( srv.start() );
+
+  zypp::Url base = srv.url();
+  base.setPathName ( zypp::str::Str() << "//" << DATADIR );
+
+  MediaSetAccess setaccess( base, "/" );
+  OnMediaLocation loc( "/src1/cd1/test.txt" );
+  BOOST_CHECK_NO_THROW( setaccess.provideFile ( loc ) );
+}
 
 // vim: set ts=2 sts=2 sw=2 ai et:
diff --git a/tests/zypp/Url_test.cc b/tests/zypp/Url_test.cc
index a63b86618..dca01a40e 100644
--- a/tests/zypp/Url_test.cc
+++ b/tests/zypp/Url_test.cc
@@ -342,4 +342,51 @@ BOOST_AUTO_TEST_CASE(bsc1234304)
 
 }
 
+BOOST_AUTO_TEST_CASE(bsc1238315)
+{
+  // Fix lost leading double slash when appending path to URLs.
+  // A leading "//" in ordinary URLs is quite meaningless but would be preserved.
+  // Important however with FTP whether a leading "//" is encoded as "/%2f".
+  Url u;
+  auto appendPathName = []( const Url & url_r, const Pathname & path_r, Url::EEncoding eflag_r = url::E_DECODED ) {
+    Url ret { url_r };
+    ret.appendPathName( path_r, eflag_r );
+    return ret.asString();
+  };
+
+  // No difference with singe "/" URLs:
+  u = Url("http://HOST/fo%20o");
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba%20a", url::E_ENCODED ),    "http://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba a" ),                      "http://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "%2fba%20a", url::E_ENCODED ), "http://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "/ba a" ),                     "http://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba%20a", url::E_ENCODED ), "http://host/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba a" ),                   "http://host/ba%20a" );
+
+  u = Url("ftp://HOST/fo%20o");
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba%20a", url::E_ENCODED ),    "ftp://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba a" ),                      "ftp://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "%2fba%20a", url::E_ENCODED ), "ftp://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "/ba a" ),                     "ftp://host/fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba%20a", url::E_ENCODED ), "ftp://host/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba a" ),                   "ftp://host/ba%20a" );
+
+  // FTP differs with double "//" URLs:
+  u = Url("http://HOST//fo%20o");
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba%20a", url::E_ENCODED ),    "http://host//fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba a" ),                      "http://host//fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "%2fba%20a", url::E_ENCODED ), "http://host//fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "/ba a" ),                     "http://host//fo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba%20a", url::E_ENCODED ), "http://host//ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba a" ),                   "http://host//ba%20a" );
+
+  u = Url("ftp://HOST//fo%20o");
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba%20a", url::E_ENCODED ),    "ftp://host/%2Ffo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "ba a" ),                      "ftp://host/%2Ffo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "%2fba%20a", url::E_ENCODED ), "ftp://host/%2Ffo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "/ba a" ),                     "ftp://host/%2Ffo%20o/ba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba%20a", url::E_ENCODED ), "ftp://host/%2Fba%20a" );
+  BOOST_CHECK_EQUAL( appendPathName( u, "../ba a" ),                   "ftp://host/%2Fba%20a" );
+}
+
 // vim: set ts=2 sts=2 sw=2 ai et:
diff --git a/zypp-core/Url.cc b/zypp-core/Url.cc
index 0fac690fb..1a5256306 100644
--- a/zypp-core/Url.cc
+++ b/zypp-core/Url.cc
@@ -802,7 +802,29 @@ namespace zypp
   // -----------------------------------------------------------------
 
   void Url::appendPathName( const Pathname & path_r, EEncoding eflag_r )
-  { if ( ! path_r.emptyOrRoot() ) setPathName( Pathname(getPathName( eflag_r )) / path_r, eflag_r ); }
+  {
+    if ( ! path_r.emptyOrRoot() ) {
+      // Check on string level to restore leading double slashes "//" (E_DECODED)
+      // Most important for ftp: where "/%2f" (E_ENCODED) denotes an absolute path.
+      std::string upath { getPathName( url::E_DECODED ) };
+      if ( upath.empty() ) {
+        setPathName( path_r.absolutename(), eflag_r );
+      } else {
+        bool doubleslhash = str::startsWith( upath, "//" );
+        Pathname npath { upath };   // now let Pathname handle the correct concatenation
+        if ( eflag_r == url::E_DECODED ) {
+          npath /= path_r;
+        } else {
+          npath /= url::decode( path_r.asString() );
+        }
+        if ( doubleslhash ) {
+          setPathName( "/" + npath.asString(), url::E_DECODED );
+        } else {
+          setPathName( npath.asString(), url::E_DECODED );
+        }
+      }
+    }
+  }
 
   // -----------------------------------------------------------------
   void
diff --git a/zypp/media/MediaNetworkCommonHandler.cc b/zypp/media/MediaNetworkCommonHandler.cc
index b1416648f..ad1f9ce69 100644
--- a/zypp/media/MediaNetworkCommonHandler.cc
+++ b/zypp/media/MediaNetworkCommonHandler.cc
@@ -112,15 +112,9 @@ namespace zypp::media
     if ( canRedir )
       MIL << "Redirecting " << filename_r << " request to geoip location." << std::endl;
 
-    // Simply extend the URLs pathname. An 'absolute' URL path
-    // is achieved by encoding the leading '/' in an URL path:2
-    //   URL: ftp://user@server		-> ~user
-    //   URL: ftp://user@server/		-> ~user
-    //   URL: ftp://user@server//		-> ~user
-    //   URL: ftp://user@server/%2F	-> /
-    //                         ^- this '/' is just a separator
-    Url newurl( baseUrl );
-    newurl.setPathName( ( Pathname("./"+baseUrl.getPathName()) / filename_r ).asString().substr(1) );
+    // Simply extend the URLs pathname:
+    Url newurl { baseUrl };
+    newurl.appendPathName( filename_r );
     return newurl;
   }
 
