"use strict";

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

const gPrefs = Services.prefs;

function symmetricEquality(expect, a, b) {
  /* Use if/else instead of |do_check_eq(expect, a.spec == b.spec)| so
     that we get the specs output on the console if the check fails.
   */
  if (expect) {
    /* Check all the sub-pieces too, since that can help with
       debugging cases when equals() returns something unexpected */
    /* We don't check port in the loop, because it can be defaulted in
       some cases. */
    [
      "spec",
      "prePath",
      "scheme",
      "userPass",
      "username",
      "password",
      "hostPort",
      "host",
      "pathQueryRef",
      "filePath",
      "query",
      "ref",
      "directory",
      "fileName",
      "fileBaseName",
      "fileExtension",
    ].map(function (prop) {
      dump("Testing '" + prop + "'\n");
      Assert.equal(a[prop], b[prop]);
    });
  } else {
    Assert.notEqual(a.spec, b.spec);
  }
  Assert.equal(expect, a.equals(b));
  Assert.equal(expect, b.equals(a));
}

function stringToURL(str) {
  return Cc["@mozilla.org/network/standard-url-mutator;1"]
    .createInstance(Ci.nsIStandardURLMutator)
    .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null)
    .finalize()
    .QueryInterface(Ci.nsIURL);
}

function pairToURLs(pair) {
  Assert.equal(pair.length, 2);
  return pair.map(stringToURL);
}

add_test(function test_setEmptyPath() {
  var pairs = [
    ["http://example.com", "http://example.com/tests/dom/tests"],
    ["http://example.com:80", "http://example.com/tests/dom/tests"],
    ["http://example.com:80/", "http://example.com/tests/dom/test"],
    ["http://example.com/", "http://example.com/tests/dom/tests"],
    ["http://example.com/a", "http://example.com/tests/dom/tests"],
    ["http://example.com:80/a", "http://example.com/tests/dom/tests"],
  ].map(pairToURLs);

  for (var [provided, target] of pairs) {
    symmetricEquality(false, target, provided);

    provided = provided.mutate().setPathQueryRef("").finalize();
    target = target.mutate().setPathQueryRef("").finalize();

    Assert.equal(provided.spec, target.spec);
    symmetricEquality(true, target, provided);
  }
  run_next_test();
});

add_test(function test_setQuery() {
  var pairs = [
    ["http://example.com", "http://example.com/?foo"],
    ["http://example.com/bar", "http://example.com/bar?foo"],
    ["http://example.com#bar", "http://example.com/?foo#bar"],
    ["http://example.com/#bar", "http://example.com/?foo#bar"],
    ["http://example.com/?longerthanfoo#bar", "http://example.com/?foo#bar"],
    ["http://example.com/?longerthanfoo", "http://example.com/?foo"],
    /* And one that's nonempty but shorter than "foo" */
    ["http://example.com/?f#bar", "http://example.com/?foo#bar"],
    ["http://example.com/?f", "http://example.com/?foo"],
  ].map(pairToURLs);

  for (var [provided, target] of pairs) {
    symmetricEquality(false, provided, target);

    provided = provided
      .mutate()
      .setQuery("foo")
      .finalize()
      .QueryInterface(Ci.nsIURL);

    Assert.equal(provided.spec, target.spec);
    symmetricEquality(true, provided, target);
  }

  [provided, target] = [
    "http://example.com/#",
    "http://example.com/?foo#bar",
  ].map(stringToURL);
  symmetricEquality(false, provided, target);
  provided = provided
    .mutate()
    .setQuery("foo")
    .finalize()
    .QueryInterface(Ci.nsIURL);
  symmetricEquality(false, provided, target);

  var newProvided = Services.io
    .newURI("#bar", null, provided)
    .QueryInterface(Ci.nsIURL);

  Assert.equal(newProvided.spec, target.spec);
  symmetricEquality(true, newProvided, target);
  run_next_test();
});

add_test(function test_setRef() {
  var tests = [
    ["http://example.com", "", "http://example.com/"],
    ["http://example.com:80", "", "http://example.com:80/"],
    ["http://example.com:80/", "", "http://example.com:80/"],
    ["http://example.com/", "", "http://example.com/"],
    ["http://example.com/a", "", "http://example.com/a"],
    ["http://example.com:80/a", "", "http://example.com:80/a"],

    ["http://example.com", "x", "http://example.com/#x"],
    ["http://example.com:80", "x", "http://example.com:80/#x"],
    ["http://example.com:80/", "x", "http://example.com:80/#x"],
    ["http://example.com/", "x", "http://example.com/#x"],
    ["http://example.com/a", "x", "http://example.com/a#x"],
    ["http://example.com:80/a", "x", "http://example.com:80/a#x"],

    ["http://example.com", "xx", "http://example.com/#xx"],
    ["http://example.com:80", "xx", "http://example.com:80/#xx"],
    ["http://example.com:80/", "xx", "http://example.com:80/#xx"],
    ["http://example.com/", "xx", "http://example.com/#xx"],
    ["http://example.com/a", "xx", "http://example.com/a#xx"],
    ["http://example.com:80/a", "xx", "http://example.com:80/a#xx"],

    [
      "http://example.com",
      "xxxxxxxxxxxxxx",
      "http://example.com/#xxxxxxxxxxxxxx",
    ],
    [
      "http://example.com:80",
      "xxxxxxxxxxxxxx",
      "http://example.com:80/#xxxxxxxxxxxxxx",
    ],
    [
      "http://example.com:80/",
      "xxxxxxxxxxxxxx",
      "http://example.com:80/#xxxxxxxxxxxxxx",
    ],
    [
      "http://example.com/",
      "xxxxxxxxxxxxxx",
      "http://example.com/#xxxxxxxxxxxxxx",
    ],
    [
      "http://example.com/a",
      "xxxxxxxxxxxxxx",
      "http://example.com/a#xxxxxxxxxxxxxx",
    ],
    [
      "http://example.com:80/a",
      "xxxxxxxxxxxxxx",
      "http://example.com:80/a#xxxxxxxxxxxxxx",
    ],
  ];

  for (var [before, ref, result] of tests) {
    /* Test1: starting with empty ref */
    var a = stringToURL(before);
    a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL);
    var b = stringToURL(result);

    Assert.equal(a.spec, b.spec);
    Assert.equal(ref, b.ref);
    symmetricEquality(true, a, b);

    /* Test2: starting with non-empty */
    a = a.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL);
    var c = stringToURL(before);
    c = c.mutate().setRef("yyyy").finalize().QueryInterface(Ci.nsIURL);
    symmetricEquality(true, a, c);

    /* Test3: reset the ref */
    a = a.mutate().setRef("").finalize().QueryInterface(Ci.nsIURL);
    symmetricEquality(true, a, stringToURL(before));

    /* Test4: verify again after reset */
    a = a.mutate().setRef(ref).finalize().QueryInterface(Ci.nsIURL);
    symmetricEquality(true, a, b);
  }
  run_next_test();
});

// Bug 960014 - Make nsStandardURL::SetHost less magical around IPv6
add_test(function test_ipv6() {
  var url = stringToURL("http://example.com");
  url = url.mutate().setHost("[2001::1]").finalize();
  Assert.equal(url.host, "2001::1");

  url = stringToURL("http://example.com");
  url = url.mutate().setHostPort("[2001::1]:30").finalize();
  Assert.equal(url.host, "2001::1");
  Assert.equal(url.port, 30);
  Assert.equal(url.hostPort, "[2001::1]:30");

  url = stringToURL("http://example.com");
  url = url.mutate().setHostPort("2001:1").finalize();
  Assert.equal(url.host, "0.0.7.209");
  Assert.equal(url.port, 1);
  Assert.equal(url.hostPort, "0.0.7.209:1");
  run_next_test();
});

add_test(function test_ipv6_fail() {
  var url = stringToURL("http://example.com");

  Assert.throws(
    () => {
      url = url.mutate().setHost("2001::1").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "missing brackets"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("[2001::1]:20").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "url.host with port"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("[2001::1").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "missing last bracket"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("2001::1]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "missing first bracket"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("2001[::1]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad bracket position"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("[]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "empty IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("[hello]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHost("[192.168.1.1]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("2001::1").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "missing brackets"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001::1]30").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "missing : after IP"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001:1]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001:1]10").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001:1]10:20").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001:1]:10:20").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("[2001:1").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("2001]:1").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("2001:1]").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "bad IPv6 address"
  );
  Assert.throws(
    () => {
      url = url.mutate().setHostPort("").finalize();
    },
    /NS_ERROR_UNEXPECTED/,
    "Empty hostPort should fail"
  );

  // These checks used to fail, but now don't (see bug 1433958 comment 57)
  url = url.mutate().setHostPort("[2001::1]:").finalize();
  Assert.equal(url.spec, "http://[2001::1]/");
  url = url.mutate().setHostPort("[2002::1]:bad").finalize();
  Assert.equal(url.spec, "http://[2002::1]/");

  run_next_test();
});

add_test(function test_clearedSpec() {
  var url = stringToURL("http://example.com/path");
  Assert.throws(
    () => {
      url = url.mutate().setSpec("http: example").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "set bad spec"
  );
  Assert.throws(
    () => {
      url = url.mutate().setSpec("").finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "set empty spec"
  );
  Assert.equal(url.spec, "http://example.com/path");
  url = url
    .mutate()
    .setHost("allizom.org")
    .finalize()
    .QueryInterface(Ci.nsIURL);

  var ref = stringToURL("http://allizom.org/path");
  symmetricEquality(true, url, ref);
  run_next_test();
});

add_test(function test_escapeBrackets() {
  // Query
  var url = stringToURL("http://example.com/?a[x]=1");
  Assert.equal(url.spec, "http://example.com/?a[x]=1");

  url = stringToURL("http://example.com/?a%5Bx%5D=1");
  Assert.equal(url.spec, "http://example.com/?a%5Bx%5D=1");

  url = stringToURL("http://[2001::1]/?a[x]=1");
  Assert.equal(url.spec, "http://[2001::1]/?a[x]=1");

  url = stringToURL("http://[2001::1]/?a%5Bx%5D=1");
  Assert.equal(url.spec, "http://[2001::1]/?a%5Bx%5D=1");

  // Path
  url = stringToURL("http://example.com/brackets[x]/test");
  Assert.equal(url.spec, "http://example.com/brackets[x]/test");

  url = stringToURL("http://example.com/a%5Bx%5D/test");
  Assert.equal(url.spec, "http://example.com/a%5Bx%5D/test");
  run_next_test();
});

add_test(function test_escapeQuote() {
  var url = stringToURL("http://example.com/#'");
  Assert.equal(url.spec, "http://example.com/#'");
  Assert.equal(url.ref, "'");
  url = url.mutate().setRef("test'test").finalize();
  Assert.equal(url.spec, "http://example.com/#test'test");
  Assert.equal(url.ref, "test'test");
  run_next_test();
});

add_test(function test_apostropheEncoding() {
  // For now, single quote is escaped everywhere _except_ the path.
  // This policy is controlled by the bitmask in nsEscape.cpp::EscapeChars[]
  var url = stringToURL("http://example.com/dir'/file'.ext'");
  Assert.equal(url.spec, "http://example.com/dir'/file'.ext'");
  run_next_test();
});

add_test(function test_accentEncoding() {
  var url = stringToURL("http://example.com/?hello=`");
  Assert.equal(url.spec, "http://example.com/?hello=`");
  Assert.equal(url.query, "hello=`");

  url = stringToURL("http://example.com/?hello=%2C");
  Assert.equal(url.spec, "http://example.com/?hello=%2C");
  Assert.equal(url.query, "hello=%2C");
  run_next_test();
});

add_test(
  { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
  function test_percentDecoding() {
    var url = stringToURL("http://%70%61%73%74%65%62%69%6E.com");
    Assert.equal(url.spec, "http://pastebin.com/");

    // Disallowed hostname characters are rejected even when percent encoded
    Assert.throws(
      () => {
        url = stringToURL("http://example.com%0a%23.google.com/");
      },
      /NS_ERROR_MALFORMED_URI/,
      "invalid characters are not allowed"
    );
    run_next_test();
  }
);

add_test(function test_hugeStringThrows() {
  let prefs = Services.prefs;
  let maxLen = prefs.getIntPref("network.standard-url.max-length");
  let url = stringToURL("http://test:test@example.com");

  let hugeString = new Array(maxLen + 1).fill("a").join("");
  let setters = [
    { method: "setSpec", qi: Ci.nsIURIMutator },
    { method: "setUsername", qi: Ci.nsIURIMutator },
    { method: "setPassword", qi: Ci.nsIURIMutator },
    { method: "setFilePath", qi: Ci.nsIURIMutator },
    { method: "setHostPort", qi: Ci.nsIURIMutator },
    { method: "setHost", qi: Ci.nsIURIMutator },
    { method: "setUserPass", qi: Ci.nsIURIMutator },
    { method: "setPathQueryRef", qi: Ci.nsIURIMutator },
    { method: "setQuery", qi: Ci.nsIURIMutator },
    { method: "setRef", qi: Ci.nsIURIMutator },
    { method: "setScheme", qi: Ci.nsIURIMutator },
    { method: "setFileName", qi: Ci.nsIURLMutator },
    { method: "setFileExtension", qi: Ci.nsIURLMutator },
    { method: "setFileBaseName", qi: Ci.nsIURLMutator },
  ];

  for (let prop of setters) {
    Assert.throws(
      () =>
        (url = url
          .mutate()
          .QueryInterface(prop.qi)
          [prop.method](hugeString)
          .finalize()),
      /NS_ERROR_MALFORMED_URI/,
      `Passing a huge string to "${prop.method}" should throw`
    );
  }

  run_next_test();
});

add_test(function test_verticalBar() {
  var url = Services.io.newURI("file:///w|m");
  Assert.equal(url.spec, "file:///w|m");

  url = Services.io.newURI("file:///w||m");
  Assert.equal(url.spec, "file:///w||m");

  url = Services.io.newURI("file:///w|/m");
  Assert.equal(url.spec, "file:///w:/m");

  url = Services.io.newURI("file:C|/m/");
  Assert.equal(url.spec, "file:///C:/m/");

  url = Services.io.newURI("file:C||/m/");
  Assert.equal(url.spec, "file:///C||/m/");

  run_next_test();
});

add_test(function test_pathPercentEncodedDot() {
  var url = stringToURL("http://example.com/hello/%2e%2E/%2e");
  Assert.equal(url.spec, "http://example.com/");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e%2E/%");
  Assert.equal(url.spec, "http://example.com/%");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%");
  Assert.equal(url.fileBaseName, "%");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e%2E/%2");
  Assert.equal(url.spec, "http://example.com/%2");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%2");
  Assert.equal(url.fileBaseName, "%2");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e%2E/%#");
  Assert.equal(url.spec, "http://example.com/%#");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%");
  Assert.equal(url.fileBaseName, "%");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e%2E/%2?");
  Assert.equal(url.spec, "http://example.com/%2?");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%2");
  Assert.equal(url.fileBaseName, "%2");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e/");
  Assert.equal(url.spec, "http://example.com/hello/");
  Assert.equal(url.directory, "/hello/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/%2e");
  Assert.equal(url.spec, "http://example.com/");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/.%2e");
  Assert.equal(url.spec, "http://example.com/");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/%2e.");
  Assert.equal(url.spec, "http://example.com/");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/%2e%2e");
  Assert.equal(url.spec, "http://example.com/");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "");
  Assert.equal(url.fileBaseName, "");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/%2e%2e%2e");
  Assert.equal(url.spec, "http://example.com/%2e%2e%2e");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%2e%2e%2e");
  Assert.equal(url.fileBaseName, "%2e%2e%2e");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/%2e%2e%2e%2e");
  Assert.equal(url.spec, "http://example.com/%2e%2e%2e%2e");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%2e%2e%2e%2e");
  Assert.equal(url.fileBaseName, "%2e%2e%2e%2e");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e%2");
  Assert.equal(url.spec, "http://example.com/hello/%2e%2");
  Assert.equal(url.directory, "/hello/");
  Assert.equal(url.fileName, "%2e%2");
  Assert.equal(url.fileBaseName, "%2e%2");
  Assert.equal(url.fileExtension, "");

  url = stringToURL("http://example.com/hello/%2e./%2e%2e/.%2e/%2e.bar");
  Assert.equal(url.spec, "http://example.com/%2e.bar");
  Assert.equal(url.directory, "/");
  Assert.equal(url.fileName, "%2e.bar");
  Assert.equal(url.fileBaseName, "%2e");
  Assert.equal(url.fileExtension, "bar");

  url = stringToURL("http://example.com/%2eX/X%2e/%2eX");
  Assert.equal(url.spec, "http://example.com/%2eX/X%2e/%2eX");
  Assert.equal(url.directory, "/%2eX/X%2e/");
  Assert.equal(url.fileName, "%2eX");
  Assert.equal(url.fileBaseName, "%2eX");
  Assert.equal(url.fileExtension, "");

  run_next_test();
});

add_test(function test_filterWhitespace() {
  let url = stringToURL(
    " \r\n\th\nt\rt\tp://ex\r\n\tample.com/path\r\n\t/\r\n\tto the/fil\r\n\te.e\r\n\txt?que\r\n\try#ha\r\n\tsh \r\n\t "
  );
  Assert.equal(
    url.spec,
    "http://example.com/path/to%20the/file.ext?query#hash"
  );

  // These setters should filter \r\n\t.
  url = stringToURL("http://test.com/path?query#hash");
  url = url.mutate().setFilePath("pa\r\n\tth").finalize();
  Assert.equal(url.spec, "http://test.com/path?query#hash");
  url = url.mutate().setQuery("que\r\n\try").finalize();
  Assert.equal(url.spec, "http://test.com/path?query#hash");
  url = url.mutate().setRef("ha\r\n\tsh").finalize();
  Assert.equal(url.spec, "http://test.com/path?query#hash");
  url = url
    .mutate()
    .QueryInterface(Ci.nsIURLMutator)
    .setFileName("fi\r\n\tle.name")
    .finalize();
  Assert.equal(url.spec, "http://test.com/fi%0D%0A%09le.name?query#hash");

  run_next_test();
});

add_test(function test_backslashReplacement() {
  var url = stringToURL(
    "http:\\\\test.com\\path/to\\file?query\\backslash#hash\\"
  );
  Assert.equal(
    url.spec,
    "http://test.com/path/to/file?query\\backslash#hash\\"
  );

  url = stringToURL("http:\\\\test.com\\example.org/path\\to/file");
  Assert.equal(url.spec, "http://test.com/example.org/path/to/file");
  Assert.equal(url.host, "test.com");
  Assert.equal(url.pathQueryRef, "/example.org/path/to/file");

  run_next_test();
});

add_test(function test_authority_host() {
  Assert.throws(
    () => {
      stringToURL("http:");
    },
    /NS_ERROR_MALFORMED_URI/,
    "TYPE_AUTHORITY should have host"
  );
  Assert.throws(
    () => {
      stringToURL("http:///");
    },
    /NS_ERROR_MALFORMED_URI/,
    "TYPE_AUTHORITY should have host"
  );

  run_next_test();
});

add_test(function test_trim_C0_and_space() {
  var url = stringToURL(
    "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://example.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
  );
  Assert.equal(url.spec, "http://example.com/");
  url = url
    .mutate()
    .setSpec(
      "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f http://test.com/ \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
    )
    .finalize();
  Assert.equal(url.spec, "http://test.com/");
  Assert.throws(
    () => {
      url = url
        .mutate()
        .setSpec(
          "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19 "
        )
        .finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "set empty spec"
  );
  run_next_test();
});

// This tests that C0-and-space characters in the path, query and ref are
// percent encoded.
add_test(function test_encode_C0_and_space() {
  function toHex(d) {
    var hex = d.toString(16);
    if (hex.length == 1) {
      hex = "0" + hex;
    }
    return hex.toUpperCase();
  }

  for (var i = 0x0; i <= 0x20; i++) {
    // These characters get filtered - they are not encoded.
    if (
      String.fromCharCode(i) == "\r" ||
      String.fromCharCode(i) == "\n" ||
      String.fromCharCode(i) == "\t"
    ) {
      continue;
    }
    let url = stringToURL(
      "http://example.com/pa" +
        String.fromCharCode(i) +
        "th?qu" +
        String.fromCharCode(i) +
        "ery#ha" +
        String.fromCharCode(i) +
        "sh"
    );
    Assert.equal(
      url.spec,
      "http://example.com/pa%" +
        toHex(i) +
        "th?qu%" +
        toHex(i) +
        "ery#ha%" +
        toHex(i) +
        "sh"
    );
  }

  // Additionally, we need to check the setters.
  let url = stringToURL("http://example.com/path?query#hash");
  url = url.mutate().setFilePath("pa\0th").finalize();
  Assert.equal(url.spec, "http://example.com/pa%00th?query#hash");
  url = url.mutate().setQuery("qu\0ery").finalize();
  Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#hash");
  url = url.mutate().setRef("ha\0sh").finalize();
  Assert.equal(url.spec, "http://example.com/pa%00th?qu%00ery#ha%00sh");
  url = url
    .mutate()
    .QueryInterface(Ci.nsIURLMutator)
    .setFileName("fi\0le.name")
    .finalize();
  Assert.equal(url.spec, "http://example.com/fi%00le.name?qu%00ery#ha%00sh");

  run_next_test();
});

add_test(function test_ipv4Normalize() {
  var localIPv4s = [
    "http://127.0.0.1",
    "http://127.0.1",
    "http://127.1",
    "http://2130706433",
    "http://0177.00.00.01",
    "http://0177.00.01",
    "http://0177.01",
    "http://00000000000000000000000000177.0000000.0000000.0001",
    "http://000000177.0000001",
    "http://017700000001",
    "http://0x7f.0x00.0x00.0x01",
    "http://0x7f.0x01",
    "http://0x7f000001",
    "http://0x007f.0x0000.0x0000.0x0001",
    "http://000177.0.00000.0x0001",
    "http://127.0.0.1.",
  ].map(stringToURL);

  let url;
  for (url of localIPv4s) {
    Assert.equal(url.spec, "http://127.0.0.1/");
  }

  // These should treated as a domain instead of an IPv4.
  var nonIPv4s = [
    "http://2+3/",
    "http://0.0.0.-1/",
    "http://1.2.3.4../",
    "resource://123/",
    "resource://4294967296/",
  ];
  var spec;
  for (spec of nonIPv4s) {
    url = stringToURL(spec);
    Assert.equal(url.spec, spec);
  }

  url = stringToURL("resource://path/to/resource/");
  url = url.mutate().setHost("123").finalize();
  Assert.equal(url.host, "123");

  run_next_test();
});

add_test(function test_invalidHostChars() {
  var url = stringToURL("http://example.org/");
  for (let i = 0; i <= 0x20; i++) {
    // These characters get filtered.
    if (
      String.fromCharCode(i) == "\r" ||
      String.fromCharCode(i) == "\n" ||
      String.fromCharCode(i) == "\t"
    ) {
      continue;
    }
    Assert.throws(
      () => {
        url = url
          .mutate()
          .setHost("a" + String.fromCharCode(i) + "b")
          .finalize();
      },
      /NS_ERROR_MALFORMED_URI/,
      "Trying to set hostname containing char code: " + i
    );
  }
  for (let c of "@[]<>|:") {
    Assert.throws(
      () => {
        url = url
          .mutate()
          .setHost("a" + c)
          .finalize();
      },
      /NS_ERROR_MALFORMED_URI/,
      "Trying to set hostname containing char: " + c
    );
  }

  // It also can't contain /, \, #, ?, but we treat these characters as
  // hostname separators, so there is no way to set them and fail.
  run_next_test();
});

add_test(function test_normalize_ipv6() {
  var url = stringToURL("http://example.com");
  url = url.mutate().setHost("[::192.9.5.5]").finalize();
  Assert.equal(url.spec, "http://[::c009:505]/");

  run_next_test();
});

add_test(function test_emptyPassword() {
  var url = stringToURL("http://a:@example.com");
  Assert.equal(url.spec, "http://a@example.com/");
  url = url.mutate().setPassword("pp").finalize();
  Assert.equal(url.spec, "http://a:pp@example.com/");
  url = url.mutate().setPassword("").finalize();
  Assert.equal(url.spec, "http://a@example.com/");
  url = url.mutate().setUserPass("xxx:").finalize();
  Assert.equal(url.spec, "http://xxx@example.com/");
  url = url.mutate().setPassword("zzzz").finalize();
  Assert.equal(url.spec, "http://xxx:zzzz@example.com/");
  url = url.mutate().setUserPass("xxxxx:yyyyyy").finalize();
  Assert.equal(url.spec, "http://xxxxx:yyyyyy@example.com/");
  url = url.mutate().setUserPass("z:").finalize();
  Assert.equal(url.spec, "http://z@example.com/");
  url = url.mutate().setPassword("ppppppppppp").finalize();
  Assert.equal(url.spec, "http://z:ppppppppppp@example.com/");

  url = stringToURL("http://example.com");
  url = url.mutate().setPassword("").finalize(); // Still empty. Should work.
  Assert.equal(url.spec, "http://example.com/");

  run_next_test();
});

add_test(function test_emptyUser() {
  let url = stringToURL("http://:a@example.com/path/to/something?query#hash");
  Assert.equal(url.spec, "http://:a@example.com/path/to/something?query#hash");
  url = stringToURL("http://:@example.com/path/to/something?query#hash");
  Assert.equal(url.spec, "http://example.com/path/to/something?query#hash");

  const kurl = stringToURL(
    "http://user:pass@example.com:8888/path/to/something?query#hash"
  );
  url = kurl.mutate().setUsername("").finalize();
  Assert.equal(
    url.spec,
    "http://:pass@example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  Assert.equal(url.hostPort, "example.com:8888");
  Assert.equal(url.filePath, "/path/to/something");
  Assert.equal(url.query, "query");
  Assert.equal(url.ref, "hash");
  url = kurl.mutate().setUserPass(":pass1").finalize();
  Assert.equal(
    url.spec,
    "http://:pass1@example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  Assert.equal(url.hostPort, "example.com:8888");
  Assert.equal(url.filePath, "/path/to/something");
  Assert.equal(url.query, "query");
  Assert.equal(url.ref, "hash");
  url = url.mutate().setUsername("user2").finalize();
  Assert.equal(
    url.spec,
    "http://user2:pass1@example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass(":pass234").finalize();
  Assert.equal(
    url.spec,
    "http://:pass234@example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass("").finalize();
  Assert.equal(
    url.spec,
    "http://example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  url = url.mutate().setPassword("pa").finalize();
  Assert.equal(
    url.spec,
    "http://:pa@example.com:8888/path/to/something?query#hash"
  );
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass("user:pass").finalize();
  symmetricEquality(true, url.QueryInterface(Ci.nsIURL), kurl);

  url = stringToURL("http://example.com:8888/path/to/something?query#hash");
  url = url.mutate().setPassword("pass").finalize();
  Assert.equal(
    url.spec,
    "http://:pass@example.com:8888/path/to/something?query#hash"
  );
  url = url.mutate().setUsername("").finalize();
  Assert.equal(
    url.spec,
    "http://:pass@example.com:8888/path/to/something?query#hash"
  );

  url = stringToURL("http://example.com:8888");
  url = url.mutate().setUsername("user").finalize();
  url = url.mutate().setUsername("").finalize();
  Assert.equal(url.spec, "http://example.com:8888/");

  url = stringToURL("http://:pass@example.com");
  Assert.equal(url.spec, "http://:pass@example.com/");
  url = url.mutate().setPassword("").finalize();
  Assert.equal(url.spec, "http://example.com/");
  url = url.mutate().setUserPass("user:pass").finalize();
  Assert.equal(url.spec, "http://user:pass@example.com/");
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass("u:p").finalize();
  Assert.equal(url.spec, "http://u:p@example.com/");
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass("u1:p23").finalize();
  Assert.equal(url.spec, "http://u1:p23@example.com/");
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUsername("u").finalize();
  Assert.equal(url.spec, "http://u:p23@example.com/");
  Assert.equal(url.host, "example.com");
  url = url.mutate().setPassword("p").finalize();
  Assert.equal(url.spec, "http://u:p@example.com/");
  Assert.equal(url.host, "example.com");

  url = url.mutate().setUserPass("u2:p2").finalize();
  Assert.equal(url.spec, "http://u2:p2@example.com/");
  Assert.equal(url.host, "example.com");
  url = url.mutate().setUserPass("u23:p23").finalize();
  Assert.equal(url.spec, "http://u23:p23@example.com/");
  Assert.equal(url.host, "example.com");

  run_next_test();
});

registerCleanupFunction(function () {
  gPrefs.clearUserPref("network.standard-url.punycode-host");
});

add_test(function test_idna_host() {
  // See bug 945240 - this test makes sure that URLs return a punycode hostname
  let url = stringToURL(
    "http://user:password@ält.example.org:8080/path?query#etc"
  );
  equal(url.host, "xn--lt-uia.example.org");
  equal(url.hostPort, "xn--lt-uia.example.org:8080");
  equal(url.prePath, "http://user:password@xn--lt-uia.example.org:8080");
  equal(
    url.spec,
    "http://user:password@xn--lt-uia.example.org:8080/path?query#etc"
  );
  equal(
    url.specIgnoringRef,
    "http://user:password@xn--lt-uia.example.org:8080/path?query"
  );
  equal(
    url
      .QueryInterface(Ci.nsISensitiveInfoHiddenURI)
      .getSensitiveInfoHiddenSpec(),
    "http://user:****@xn--lt-uia.example.org:8080/path?query#etc"
  );

  equal(url.displayHost, "ält.example.org");
  equal(url.displayHostPort, "ält.example.org:8080");
  equal(
    url.displaySpec,
    "http://user:password@ält.example.org:8080/path?query#etc"
  );

  equal(url.asciiHost, "xn--lt-uia.example.org");
  equal(url.asciiHostPort, "xn--lt-uia.example.org:8080");
  equal(
    url.asciiSpec,
    "http://user:password@xn--lt-uia.example.org:8080/path?query#etc"
  );

  url = url.mutate().setRef("").finalize(); // SetRef calls InvalidateCache()
  equal(
    url.spec,
    "http://user:password@xn--lt-uia.example.org:8080/path?query"
  );
  equal(
    url.displaySpec,
    "http://user:password@ält.example.org:8080/path?query"
  );
  equal(
    url.asciiSpec,
    "http://user:password@xn--lt-uia.example.org:8080/path?query"
  );

  url = stringToURL("http://user:password@www.ält.com:8080/path?query#etc");
  url = url.mutate().setRef("").finalize();
  equal(url.spec, "http://user:password@www.xn--lt-uia.com:8080/path?query");

  run_next_test();
});

add_test(
  { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
  function test_bug1517025() {
    Assert.throws(
      () => {
        stringToURL("https://b%9a/");
      },
      /NS_ERROR_MALFORMED_URI/,
      "bad URI"
    );

    Assert.throws(
      () => {
        stringToURL("https://b%9ª/");
      },
      /NS_ERROR_MALFORMED_URI/,
      "bad URI"
    );

    let base = stringToURL(
      "https://bug1517025.bmoattachments.org/attachment.cgi?id=9033787"
    );
    Assert.throws(
      () => {
        Services.io.newURI("/\\b%9ª", "windows-1252", base);
      },
      /NS_ERROR_MALFORMED_URI/,
      "bad URI"
    );

    run_next_test();
  }
);

add_task(async function test_emptyHostWithURLType() {
  let makeURL = (str, type) => {
    return Cc["@mozilla.org/network/standard-url-mutator;1"]
      .createInstance(Ci.nsIStandardURLMutator)
      .init(type, 80, str, "UTF-8", null)
      .finalize()
      .QueryInterface(Ci.nsIURL);
  };

  let url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_AUTHORITY);
  Assert.throws(
    () => url.mutate().setHost("").finalize().spec,
    /NS_ERROR_UNEXPECTED/,
    "Empty host is not allowed for URLTYPE_AUTHORITY"
  );

  url = makeURL("http://user@foo.com/bar/", Ci.nsIStandardURL.URLTYPE_STANDARD);
  Assert.throws(
    () => url.mutate().setHost("").finalize().spec,
    /NS_ERROR_MALFORMED_URI/,
    "Setting an empty host should throw if there is a username present"
  );

  url = makeURL(
    "http://:password@foo.com/bar/",
    Ci.nsIStandardURL.URLTYPE_STANDARD
  );
  Assert.throws(
    () => url.mutate().setHost("").finalize().spec,
    /NS_ERROR_MALFORMED_URI/,
    "Setting an empty host should throw if there is a password present"
  );

  url = makeURL("http://foo.com:123/bar/", Ci.nsIStandardURL.URLTYPE_STANDARD);
  Assert.throws(
    () => url.mutate().setHost("").finalize().spec,
    /NS_ERROR_MALFORMED_URI/,
    "Setting an empty host should throw if there is a port present"
  );

  url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_STANDARD);
  Assert.equal(url.mutate().setHost("").finalize().spec, "http:///bar/");

  url = makeURL("http://foo.com/bar/", Ci.nsIStandardURL.URLTYPE_NO_AUTHORITY);
  equal(
    url.spec,
    "http:///bar/",
    "Host is removed when parsing URLTYPE_NO_AUTHORITY"
  );
  equal(
    url.mutate().setHost("").finalize().spec,
    "http:///bar/",
    "Setting an empty host does nothing for URLTYPE_NO_AUTHORITY"
  );
  Assert.throws(
    () => url.mutate().setHost("something").finalize().spec,
    /NS_ERROR_UNEXPECTED/,
    "Setting a non-empty host is not allowed for URLTYPE_NO_AUTHORITY"
  );
  equal(
    url.mutate().setHost("#j").finalize().spec,
    "http:///bar/",
    "Setting a pseudo-empty host does nothing for URLTYPE_NO_AUTHORITY"
  );

  url = makeURL(
    "http://example.org:123/foo?bar#baz",
    Ci.nsIStandardURL.URLTYPE_AUTHORITY
  );
  Assert.throws(
    () => url.mutate().setHost("#j").finalize().spec,
    /NS_ERROR_UNEXPECTED/,
    "A pseudo-empty host is not allowed for URLTYPE_AUTHORITY"
  );
});

add_task(async function test_fuzz() {
  let makeURL = str => {
    return (
      Cc["@mozilla.org/network/standard-url-mutator;1"]
        .createInstance(Ci.nsIStandardURLMutator)
        .QueryInterface(Ci.nsIURIMutator)
        // .init(type, 80, str, "UTF-8", null)
        .setSpec(str)
        .finalize()
        .QueryInterface(Ci.nsIURL)
    );
  };

  Assert.throws(() => {
    let url = makeURL("/");
    url.mutate().setHost("(").finalize();
  }, /NS_ERROR_MALFORMED_URI/);
});

add_task(async function test_bug1648493() {
  let url = stringToURL("https://example.com/");
  url = url.mutate().setScheme("file").finalize();
  url = url.mutate().setScheme("resource").finalize();
  url = url.mutate().setPassword("Ãª").finalize();
  url = url.mutate().setUsername("Ã§").finalize();
  url = url.mutate().setScheme("t").finalize();
  equal(url.spec, "t://%C3%83%C2%A7:%C3%83%C2%AA@example.com/");
  equal(url.username, "%C3%83%C2%A7");
});

add_task(async function test_bug1873976() {
  let url = Services.io.newURI("file:.");
  equal(url.spec, "file:///");
});

add_task(async function test_bug1890346() {
  let url = Services.io.newURI("file:..?/..");
  equal(url.spec, "file:///?/..");
});

add_task(async function test_bug1914141() {
  equal(Services.io.isValidHostname("example.com"), true);
  equal(Services.io.isValidHostname("example.0"), false);

  equal(Services.io.isValidHostname("192.168.0.1"), true);
  equal(Services.io.isValidHostname("192.168.0"), true);
  equal(Services.io.isValidHostname("1.192.168.0.1"), false);
  equal(Services.io.isValidHostname("invalid.192.168.0.1"), false);

  equal(Services.io.isValidHostname("::1"), true);
  equal(Services.io.isValidHostname("abcd::zz::00"), false);
  equal(Services.io.isValidHostname("zzzz::1.2.3.4"), false);

  equal(Services.io.isValidHostname("::1.2.3.4"), true);
});

add_task(async function test_bug1998992() {
  const maxLength = Services.prefs.getIntPref(
    "network.standard-url.max-length"
  );

  let uri = stringToURL("http://example.com/");

  const baseLength = uri.spec.length;
  const targetEncodedLength = maxLength - baseLength + 100;

  const numSpaces = Math.ceil(targetEncodedLength / 3);
  const longRef = " ".repeat(numSpaces);

  Assert.throws(
    () => {
      uri.mutate().setRef(longRef).finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "SetRef should reject refs that exceed max length after encoding"
  );

  Assert.throws(
    () => {
      uri.mutate().setQuery(longRef).finalize();
    },
    /NS_ERROR_MALFORMED_URI/,
    "SetQuery should reject queries that exceed max length after encoding"
  );
});
