Bug 1394906 - Implement immutable, threadsafe MozURL r=mayhemer draft
authorValentin Gosu <valentin.gosu@gmail.com>
Sat, 07 Oct 2017 01:15:59 +0200
changeset 676296 550aafbfb1d57257db49897a7506ebb9f038021f
parent 674765 294f332a35538940469b1a2576615ff5ffe1e016
child 734913 6332e9ef803c50132ef7a6f1f477c9022943bde4
push id83466
push uservalentin.gosu@gmail.com
push dateSat, 07 Oct 2017 08:51:19 +0000
reviewersmayhemer
bugs1394906
milestone58.0a1
Bug 1394906 - Implement immutable, threadsafe MozURL r=mayhemer Also adds the necessary methods to rust-url-capi and fixes a bug in rusturl_set_username MozReview-Commit-ID: 9rsPIQAbWBJ
netwerk/base/MozURL.cpp
netwerk/base/MozURL.h
netwerk/base/moz.build
netwerk/base/rust-url-capi/src/lib.rs
netwerk/base/rust-url-capi/src/rust-url-capi.h
netwerk/test/gtest/TestMozURL.cpp
netwerk/test/gtest/moz.build
new file mode 100644
--- /dev/null
+++ b/netwerk/base/MozURL.cpp
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MozURL.h"
+
+namespace mozilla {
+namespace net {
+
+NS_IMPL_ADDREF(MozURL)
+NS_IMPL_RELEASE(MozURL)
+
+/* static */ nsresult
+MozURL::Init(const nsACString& aSpec, MozURL** aURL)
+{
+  rusturl* ptr = rusturl_new(&aSpec);
+  if (!ptr) {
+    return NS_ERROR_FAILURE;
+  }
+  RefPtr<MozURL> url = new MozURL(ptr);
+  url.forget(aURL);
+  return NS_OK;
+}
+
+nsresult
+MozURL::GetScheme(nsACString& aScheme)
+{
+  return rusturl_get_scheme(mURL.get(), &aScheme);
+}
+
+nsresult
+MozURL::GetSpec(nsACString& aSpec)
+{
+  return rusturl_get_spec(mURL.get(), &aSpec);
+}
+
+nsresult
+MozURL::GetUsername(nsACString& aUser)
+{
+  return rusturl_get_username(mURL.get(), &aUser);
+}
+
+nsresult
+MozURL::GetPassword(nsACString& aPassword)
+{
+  return rusturl_get_password(mURL.get(), &aPassword);
+}
+
+nsresult
+MozURL::GetHostname(nsACString& aHost)
+{
+  return rusturl_get_host(mURL.get(), &aHost);
+}
+
+nsresult
+MozURL::GetPort(int32_t* aPort)
+{
+  return rusturl_get_port(mURL.get(), aPort);
+}
+
+nsresult
+MozURL::GetFilePath(nsACString& aPath)
+{
+  return rusturl_get_filepath(mURL.get(), &aPath);
+}
+
+nsresult
+MozURL::GetQuery(nsACString& aQuery)
+{
+  return rusturl_get_query(mURL.get(), &aQuery);
+}
+
+nsresult
+MozURL::GetRef(nsACString& aRef)
+{
+  return rusturl_get_fragment(mURL.get(), &aRef);
+}
+
+// MozURL::Mutator
+
+// This macro ensures that the mutator is still valid, meaning it hasn't been
+// finalized, and none of the setters have returned an error code.
+#define ENSURE_VALID()                          \
+  PR_BEGIN_MACRO                                \
+    if (mFinalized) {                           \
+      mStatus = NS_ERROR_NOT_AVAILABLE;         \
+    }                                           \
+    if (NS_FAILED(mStatus)) {                   \
+      return *this;                             \
+    }                                           \
+  PR_END_MACRO
+
+nsresult
+MozURL::Mutator::Finalize(MozURL** aURL)
+{
+  if (mFinalized) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  mFinalized = true;
+  if (NS_FAILED(mStatus)) {
+    return mStatus;
+  }
+  RefPtr<MozURL> result = new MozURL(mURL.release());
+  result.forget(aURL);
+  return NS_OK;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetScheme(const nsACString& aScheme)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_scheme(mURL.get(), &aScheme);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetUsername(const nsACString& aUser)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_username(mURL.get(), &aUser);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetPassword(const nsACString& aPassword)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_password(mURL.get(), &aPassword);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetHostname(const nsACString& aHost)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_host(mURL.get(), &aHost);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetFilePath(const nsACString& aPath)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_path(mURL.get(), &aPath);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetQuery(const nsACString& aQuery)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_query(mURL.get(), &aQuery);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetRef(const nsACString& aRef)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_fragment(mURL.get(), &aRef);
+  return *this;
+}
+
+MozURL::Mutator&
+MozURL::Mutator::SetPort(int32_t aPort)
+{
+  ENSURE_VALID();
+  mStatus = rusturl_set_port_no(mURL.get(), aPort);
+  return *this;
+}
+
+} // namespace net
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/base/MozURL.h
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozURL_h__
+#define mozURL_h__
+
+#include "rust-url-capi/src/rust-url-capi.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace net {
+
+// This class provides a thread-safe, immutable URL parser.
+// As long as there is RefPtr to the object, you may use it on any thread.
+// The constructor is private. One can instantiate the object by
+// calling the Init() method as such:
+//
+// RefPtr<MozURL> url;
+// nsAutoCString href("http://example.com/path?query#ref");
+// nsresult rv = MozURL::Init(href, getter_AddRefs(url));
+// if (NS_SUCCEEDED(rv)) { /* use url */ }
+//
+// When changing the URL is needed, you need to call the Mutate() method.
+// This gives you a Mutator object, on which you can perform setter operations.
+// Calling Finalize() on the Mutator will result in a new MozURL and a status
+// code. If any of the setter operations failed, it will be reflected in the
+// status code, and a null MozURL.
+//
+// Note: In the case of a domain name containing non-ascii characters,
+// GetSpec and GetHostname will return the IDNA(punycode) version of the host.
+// Also note that for now, MozURL only supports the UTF-8 charset.
+class MozURL final
+{
+public:
+  static nsresult Init(const nsACString& aSpec, MozURL** aURL);
+
+  nsresult GetScheme(nsACString& aScheme);
+  nsresult GetSpec(nsACString& aSpec);
+  nsresult GetUsername(nsACString& aUser);
+  nsresult GetPassword(nsACString& aPassword);
+  // Will return the hostname of URL. If the hostname is an IPv6 address,
+  // it will be enclosed in square brackets, such as `[::1]`
+  nsresult GetHostname(nsACString& aHost);
+  // Will return the port number, if specified, or -1
+  nsresult GetPort(int32_t* aPort);
+  nsresult GetFilePath(nsACString& aPath);
+  nsresult GetQuery(nsACString& aQuery);
+  nsresult GetRef(nsACString& aRef);
+
+private:
+  MozURL(rusturl* rawPtr)
+    : mURL(rawPtr)
+  {
+  }
+  virtual ~MozURL() {}
+  struct FreeRustURL
+  {
+    void operator()(rusturl* aPtr) { rusturl_free(aPtr); }
+  };
+  mozilla::UniquePtr<rusturl, FreeRustURL> mURL;
+
+public:
+  class MOZ_STACK_CLASS Mutator
+  {
+  public:
+    // Calling this method will result in the creation of a new MozURL that
+    // adopts the mutator's mURL.
+    // If any of the setters failed with an error code, that error code will be
+    // returned here. It will also return an error code if Finalize is called
+    // more than once on the Mutator.
+    nsresult Finalize(MozURL** aURL);
+
+    // These setter methods will return a reference to `this` so that you may
+    // chain setter operations as such:
+    //
+    // RefPtr<MozURL> url2;
+    // nsresult rv = url->Mutate().SetHostname(NS_LITERAL_CSTRING("newhost"))
+    //                            .SetFilePath(NS_LITERAL_CSTRING("new/file/path"))
+    //                            .Finalize(getter_AddRefs(url2));
+    // if (NS_SUCCEEDED(rv)) { /* use url2 */ }
+    Mutator& SetScheme(const nsACString& aScheme);
+    Mutator& SetUsername(const nsACString& aUser);
+    Mutator& SetPassword(const nsACString& aPassword);
+    Mutator& SetHostname(const nsACString& aHost);
+    Mutator& SetFilePath(const nsACString& aPath);
+    Mutator& SetQuery(const nsACString& aQuery);
+    Mutator& SetRef(const nsACString& aRef);
+    Mutator& SetPort(int32_t aPort);
+
+    // This method returns the status code of the setter operations.
+    // If any of the setters failed, it will return the code of the first error
+    // that occured. If none of the setters failed, it will return NS_OK.
+    // This method is useful to avoid doing expensive operations when the result
+    // would not be used because an error occurred. For example:
+    //
+    // RefPtr<MozURL> url2;
+    // MozURL::Mutator mut = url->Mutate();
+    // mut.SetScheme("!@#$"); // this would fail
+    // if (NS_SUCCEDED(mut.GetStatus())) {
+    //   nsAutoCString host(ExpensiveComputing());
+    //   rv = mut.SetHostname(host).Finalize(getter_AddRefs(url2));
+    // }
+    // if (NS_SUCCEEDED(rv)) { /* use url2 */ }
+    nsresult GetStatus() { return mStatus; }
+  private:
+    Mutator(MozURL* url)
+      : mURL(rusturl_clone(url->mURL.get()))
+      , mFinalized(false)
+      , mStatus(NS_OK)
+    {
+    }
+    mozilla::UniquePtr<rusturl, FreeRustURL> mURL;
+    bool mFinalized;
+    nsresult mStatus;
+    friend class MozURL;
+  };
+
+  Mutator Mutate() { return Mutator(this); }
+
+// These are used to avoid inheriting from nsISupports
+public:
+  NS_IMETHOD_(MozExternalRefCountType) AddRef(void);
+  NS_IMETHOD_(MozExternalRefCountType) Release(void);
+  typedef mozilla::TrueType HasThreadSafeRefCnt;
+protected:
+  ::mozilla::ThreadSafeAutoRefCnt mRefCnt;
+  NS_DECL_OWNINGTHREAD
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozURL_h__
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -170,16 +170,17 @@ EXPORTS.mozilla += [
 
 EXPORTS.mozilla.net += [
     'CaptivePortalService.h',
     'ChannelDiverterChild.h',
     'ChannelDiverterParent.h',
     'Dashboard.h',
     'DashboardTypes.h',
     'MemoryDownloader.h',
+    'MozURL.h',
     'PartiallySeekableInputStream.h',
     'Predictor.h',
     'ReferrerPolicy.h',
     'SimpleChannelParent.h',
     'TCPFastOpen.h',
 ]
 
 UNIFIED_SOURCES += [
@@ -188,16 +189,17 @@ UNIFIED_SOURCES += [
     'CaptivePortalService.cpp',
     'ChannelDiverterChild.cpp',
     'ChannelDiverterParent.cpp',
     'Dashboard.cpp',
     'EventTokenBucket.cpp',
     'LoadContextInfo.cpp',
     'LoadInfo.cpp',
     'MemoryDownloader.cpp',
+    'MozURL.cpp',
     'NetworkActivityMonitor.cpp',
     'nsAsyncRedirectVerifyHelper.cpp',
     'nsAsyncStreamCopier.cpp',
     'nsAuthInformationHolder.cpp',
     'nsBase64Encoder.cpp',
     'nsBaseChannel.cpp',
     'nsBaseContentStream.cpp',
     'nsBufferedStreams.cpp',
--- a/netwerk/base/rust-url-capi/src/lib.rs
+++ b/netwerk/base/rust-url-capi/src/lib.rs
@@ -48,16 +48,27 @@ pub extern "C" fn rusturl_new(spec: &nsA
 
   match parser().parse(url_spec) {
     Ok(url) => Box::into_raw(Box::new(url)),
     Err(_) => return ptr::null_mut(),
   }
 }
 
 #[no_mangle]
+pub extern "C" fn rusturl_clone(urlptr: Option<&Url>) -> *mut Url {
+  let url = if let Some(url) = urlptr {
+    url
+  } else {
+    return ptr::null_mut();
+  };
+
+  return Box::into_raw(Box::new(url.clone()));
+}
+
+#[no_mangle]
 pub unsafe extern "C" fn rusturl_free(urlptr: *mut Url) {
   if urlptr.is_null() {
     return;
   }
   Box::from_raw(urlptr);
 }
 
 #[no_mangle]
@@ -140,16 +151,32 @@ pub extern "C" fn rusturl_get_port(urlpt
       // NOTE: Gecko uses -1 to represent the default port
       *port = -1;
     }
   }
   NS_OK
 }
 
 #[no_mangle]
+pub extern "C" fn rusturl_get_filepath(urlptr: Option<&Url>, cont: &mut nsACString) -> nsresult {
+  let url = if let Some(url) = urlptr {
+    url
+  } else {
+    return NS_ERROR_INVALID_ARG;
+  };
+
+  if url.cannot_be_a_base() {
+      cont.assign("");
+  } else {
+      cont.assign(&url[Position::BeforePath..Position::AfterPath]);
+  }
+  NS_OK
+}
+
+#[no_mangle]
 pub extern "C" fn rusturl_get_path(urlptr: Option<&Url>, cont: &mut nsACString) -> nsresult {
   let url = if let Some(url) = urlptr {
     url
   } else {
     return NS_ERROR_INVALID_ARG;
   };
 
   if url.cannot_be_a_base() {
@@ -224,17 +251,17 @@ pub extern "C" fn rusturl_set_username(u
     return NS_ERROR_INVALID_ARG;
   };
 
   let username_ = match str::from_utf8(username) {
     Ok(p) => p,
     Err(_) => return NS_ERROR_MALFORMED_URI, // utf-8 failed
   };
 
-  match quirks::set_protocol(url, username_) {
+  match quirks::set_username(url, username_) {
     Ok(()) => NS_OK,
     Err(()) => NS_ERROR_MALFORMED_URI,
   }
 }
 
 #[no_mangle]
 pub extern "C" fn rusturl_set_password(urlptr: Option<&mut Url>, password: &nsACString) -> nsresult {
   let url = if let Some(url) = urlptr {
--- a/netwerk/base/rust-url-capi/src/rust-url-capi.h
+++ b/netwerk/base/rust-url-capi/src/rust-url-capi.h
@@ -15,24 +15,26 @@ extern "C" {
 //   be non-null.
 // * All rusturl* pointers must refer to pointers which are returned
 //   by rusturl_new, and must be freed with rusturl_free.
 
 // The `rusturl` opaque type is equivalent to the rust type `::url::Url`
 struct rusturl;
 
 rusturl* rusturl_new(const nsACString* spec);
+rusturl* rusturl_clone(const rusturl* url);
 /* unsafe */ void rusturl_free(rusturl* url);
 
 nsresult rusturl_get_spec(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_scheme(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_username(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_password(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_host(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_port(const rusturl* url, int32_t* port);
+nsresult rusturl_get_filepath(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_path(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_query(const rusturl* url, nsACString* cont);
 nsresult rusturl_get_fragment(const rusturl* url, nsACString* cont);
 nsresult rusturl_has_fragment(const rusturl* url, bool* has_fragment);
 
 nsresult rusturl_set_scheme(rusturl* url, const nsACString* scheme);
 nsresult rusturl_set_username(rusturl* url, const nsACString* user);
 nsresult rusturl_set_password(rusturl* url, const nsACString* password);
new file mode 100644
--- /dev/null
+++ b/netwerk/test/gtest/TestMozURL.cpp
@@ -0,0 +1,103 @@
+#include "gtest/gtest.h"
+#include "gtest/MozGTestBench.h" // For MOZ_GTEST_BENCH
+
+#include "nsCOMPtr.h"
+#include "../../base/MozURL.h"
+
+using namespace mozilla::net;
+
+TEST(TestMozURL, Getters)
+{
+  nsAutoCString href("http://user:pass@example.com/path?query#ref");
+  RefPtr<MozURL> url;
+  ASSERT_EQ(MozURL::Init(href, getter_AddRefs(url)), NS_OK);
+
+  nsAutoCString out;
+
+  ASSERT_EQ(url->GetScheme(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("http"));
+
+  ASSERT_EQ(url->GetSpec(out), NS_OK);
+  ASSERT_TRUE(out == href);
+
+  ASSERT_EQ(url->GetUsername(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("user"));
+
+  ASSERT_EQ(url->GetPassword(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("pass"));
+
+  ASSERT_EQ(url->GetHostname(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("example.com"));
+
+  ASSERT_EQ(url->GetFilePath(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("/path"));
+
+  ASSERT_EQ(url->GetQuery(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("query"));
+
+  ASSERT_EQ(url->GetRef(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("ref"));
+
+  url = nullptr;
+  ASSERT_EQ(MozURL::Init(NS_LITERAL_CSTRING(""), getter_AddRefs(url)),
+            NS_ERROR_FAILURE);
+  ASSERT_EQ(url, nullptr);
+}
+
+TEST(TestMozURL, MutatorChain)
+{
+  nsAutoCString href("http://user:pass@example.com/path?query#ref");
+  RefPtr<MozURL> url;
+  ASSERT_EQ(MozURL::Init(href, getter_AddRefs(url)), NS_OK);
+  nsAutoCString out;
+
+  RefPtr<MozURL> url2;
+  ASSERT_EQ(url->Mutate().SetScheme(NS_LITERAL_CSTRING("https"))
+                         .SetUsername(NS_LITERAL_CSTRING("newuser"))
+                         .SetPassword(NS_LITERAL_CSTRING("newpass"))
+                         .SetHostname(NS_LITERAL_CSTRING("test"))
+                         .SetFilePath(NS_LITERAL_CSTRING("new/file/path"))
+                         .SetQuery(NS_LITERAL_CSTRING("bla"))
+                         .SetRef(NS_LITERAL_CSTRING("huh"))
+                         .Finalize(getter_AddRefs(url2)), NS_OK);
+
+  ASSERT_EQ(url2->GetSpec(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("https://newuser:newpass@test/new/file/path?bla#huh"));
+}
+
+TEST(TestMozURL, MutatorFinalizeTwice)
+{
+  nsAutoCString href("http://user:pass@example.com/path?query#ref");
+  RefPtr<MozURL> url;
+  ASSERT_EQ(MozURL::Init(href, getter_AddRefs(url)), NS_OK);
+  nsAutoCString out;
+
+  RefPtr<MozURL> url2;
+  MozURL::Mutator mut = url->Mutate();
+  mut.SetScheme(NS_LITERAL_CSTRING("https")); // Change the scheme to https
+  ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_OK);
+  ASSERT_EQ(url2->GetSpec(out), NS_OK);
+  ASSERT_TRUE(out.EqualsLiteral("https://user:pass@example.com/path?query#ref"));
+
+  // Test that a second call to Finalize will result in an error code
+  url2 = nullptr;
+  ASSERT_EQ(mut.Finalize(getter_AddRefs(url2)), NS_ERROR_NOT_AVAILABLE);
+  ASSERT_EQ(url2, nullptr);
+}
+
+TEST(TestMozURL, MutatorErrorStatus)
+{
+  nsAutoCString href("http://user:pass@example.com/path?query#ref");
+  RefPtr<MozURL> url;
+  ASSERT_EQ(MozURL::Init(href, getter_AddRefs(url)), NS_OK);
+  nsAutoCString out;
+
+  // Test that trying to set the scheme to a bad value will get you an error
+  MozURL::Mutator mut = url->Mutate();
+  mut.SetScheme(NS_LITERAL_CSTRING("!@#$%^&*("));
+  ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI);
+
+  // Test that the mutator will not work after one faulty operation
+  mut.SetScheme(NS_LITERAL_CSTRING("test"));
+  ASSERT_EQ(mut.GetStatus(), NS_ERROR_MALFORMED_URI);
+}
--- a/netwerk/test/gtest/moz.build
+++ b/netwerk/test/gtest/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 UNIFIED_SOURCES += [
     'TestHeaders.cpp',
     'TestHttpAuthUtils.cpp',
+    'TestMozURL.cpp',
     'TestPartiallySeekableInputStream.cpp',
     'TestProtocolProxyService.cpp',
     'TestStandardURL.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul-gtest'