Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions kuri/api/android/kuri.api
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ public final class org/dexpace/kuri/Uri {
public final fun fileExtension ()Ljava/lang/String;
public final fun fileName ()Ljava/lang/String;
public final fun fragment ()Ljava/lang/String;
public final fun hasTrailingSlash ()Z
public fun hashCode ()I
public final fun host ()Lorg/dexpace/kuri/host/Host;
public final fun hostName ()Ljava/lang/String;
public final fun isAbsolute ()Z
public final fun isDirectory ()Z
public final fun isOpaquePath ()Z
public final fun newBuilder ()Lorg/dexpace/kuri/Uri$Builder;
public final fun normalized ()Lorg/dexpace/kuri/Uri;
Expand All @@ -60,6 +62,7 @@ public final class org/dexpace/kuri/Uri {
public final fun port ()Ljava/lang/Integer;
public final fun query ()Ljava/lang/String;
public final fun queryParameters ()Lorg/dexpace/kuri/query/QueryParameters;
public final fun redact ()Lorg/dexpace/kuri/Uri;
public final fun relativize (Lorg/dexpace/kuri/Uri;)Lorg/dexpace/kuri/Uri;
public final fun resolve (Ljava/lang/String;)Lorg/dexpace/kuri/error/ParseResult;
public final fun resolve (Ljava/lang/String;Lorg/dexpace/kuri/ParseOptions;)Lorg/dexpace/kuri/error/ParseResult;
Expand Down Expand Up @@ -138,10 +141,12 @@ public final class org/dexpace/kuri/Url {
public final fun fileName ()Ljava/lang/String;
public final fun fragment ()Ljava/lang/String;
public final fun hasOpaqueOrigin ()Z
public final fun hasTrailingSlash ()Z
public fun hashCode ()I
public final fun host ()Lorg/dexpace/kuri/host/Host;
public final fun hostName ()Ljava/lang/String;
public final fun href ()Ljava/lang/String;
public final fun isDirectory ()Z
public final fun isSpecial ()Z
public final fun newBuilder ()Lorg/dexpace/kuri/Url$Builder;
public final fun origin ()Ljava/lang/String;
Expand All @@ -156,6 +161,7 @@ public final class org/dexpace/kuri/Url {
public final fun port ()Ljava/lang/Integer;
public final fun query ()Ljava/lang/String;
public final fun queryParameters ()Lorg/dexpace/kuri/query/QueryParameters;
public final fun redact ()Lorg/dexpace/kuri/Url;
public final fun relativize (Lorg/dexpace/kuri/Url;)Ljava/lang/String;
public final fun resolve (Ljava/lang/String;)Lorg/dexpace/kuri/error/ParseResult;
public final fun resolveOrNull (Ljava/lang/String;)Lorg/dexpace/kuri/Url;
Expand Down
6 changes: 6 additions & 0 deletions kuri/api/jvm/kuri.api
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ public final class org/dexpace/kuri/Uri {
public final fun fileExtension ()Ljava/lang/String;
public final fun fileName ()Ljava/lang/String;
public final fun fragment ()Ljava/lang/String;
public final fun hasTrailingSlash ()Z
public fun hashCode ()I
public final fun host ()Lorg/dexpace/kuri/host/Host;
public final fun hostName ()Ljava/lang/String;
public final fun isAbsolute ()Z
public final fun isDirectory ()Z
public final fun isOpaquePath ()Z
public final fun newBuilder ()Lorg/dexpace/kuri/Uri$Builder;
public final fun normalized ()Lorg/dexpace/kuri/Uri;
Expand All @@ -71,6 +73,7 @@ public final class org/dexpace/kuri/Uri {
public final fun port ()Ljava/lang/Integer;
public final fun query ()Ljava/lang/String;
public final fun queryParameters ()Lorg/dexpace/kuri/query/QueryParameters;
public final fun redact ()Lorg/dexpace/kuri/Uri;
public final fun relativize (Lorg/dexpace/kuri/Uri;)Lorg/dexpace/kuri/Uri;
public final fun resolve (Ljava/lang/String;)Lorg/dexpace/kuri/error/ParseResult;
public final fun resolve (Ljava/lang/String;Lorg/dexpace/kuri/ParseOptions;)Lorg/dexpace/kuri/error/ParseResult;
Expand Down Expand Up @@ -149,10 +152,12 @@ public final class org/dexpace/kuri/Url {
public final fun fileName ()Ljava/lang/String;
public final fun fragment ()Ljava/lang/String;
public final fun hasOpaqueOrigin ()Z
public final fun hasTrailingSlash ()Z
public fun hashCode ()I
public final fun host ()Lorg/dexpace/kuri/host/Host;
public final fun hostName ()Ljava/lang/String;
public final fun href ()Ljava/lang/String;
public final fun isDirectory ()Z
public final fun isSpecial ()Z
public final fun newBuilder ()Lorg/dexpace/kuri/Url$Builder;
public final fun origin ()Ljava/lang/String;
Expand All @@ -167,6 +172,7 @@ public final class org/dexpace/kuri/Url {
public final fun port ()Ljava/lang/Integer;
public final fun query ()Ljava/lang/String;
public final fun queryParameters ()Lorg/dexpace/kuri/query/QueryParameters;
public final fun redact ()Lorg/dexpace/kuri/Url;
public final fun relativize (Lorg/dexpace/kuri/Url;)Ljava/lang/String;
public final fun resolve (Ljava/lang/String;)Lorg/dexpace/kuri/error/ParseResult;
public final fun resolveOrNull (Ljava/lang/String;)Lorg/dexpace/kuri/Url;
Expand Down
6 changes: 6 additions & 0 deletions kuri/api/kuri.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,16 @@ final class org.dexpace.kuri/Uri { // org.dexpace.kuri/Uri|null[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.dexpace.kuri/Uri.equals|equals(kotlin.Any?){}[0]
final fun fileExtension(): kotlin/String // org.dexpace.kuri/Uri.fileExtension|fileExtension(){}[0]
final fun fileName(): kotlin/String // org.dexpace.kuri/Uri.fileName|fileName(){}[0]
final fun hasTrailingSlash(): kotlin/Boolean // org.dexpace.kuri/Uri.hasTrailingSlash|hasTrailingSlash(){}[0]
final fun hashCode(): kotlin/Int // org.dexpace.kuri/Uri.hashCode|hashCode(){}[0]
final fun isAbsolute(): kotlin/Boolean // org.dexpace.kuri/Uri.isAbsolute|isAbsolute(){}[0]
final fun isDirectory(): kotlin/Boolean // org.dexpace.kuri/Uri.isDirectory|isDirectory(){}[0]
final fun isOpaquePath(): kotlin/Boolean // org.dexpace.kuri/Uri.isOpaquePath|isOpaquePath(){}[0]
final fun newBuilder(): org.dexpace.kuri/Uri.Builder // org.dexpace.kuri/Uri.newBuilder|newBuilder(){}[0]
final fun normalized(): org.dexpace.kuri/Uri // org.dexpace.kuri/Uri.normalized|normalized(){}[0]
final fun normalizedEquals(org.dexpace.kuri/Uri): kotlin/Boolean // org.dexpace.kuri/Uri.normalizedEquals|normalizedEquals(org.dexpace.kuri.Uri){}[0]
final fun queryParameters(): org.dexpace.kuri.query/QueryParameters // org.dexpace.kuri/Uri.queryParameters|queryParameters(){}[0]
final fun redact(): org.dexpace.kuri/Uri // org.dexpace.kuri/Uri.redact|redact(){}[0]
final fun relativize(org.dexpace.kuri/Uri): org.dexpace.kuri/Uri? // org.dexpace.kuri/Uri.relativize|relativize(org.dexpace.kuri.Uri){}[0]
final fun resolve(kotlin/String, org.dexpace.kuri/ParseOptions = ...): org.dexpace.kuri.error/ParseResult<org.dexpace.kuri/Uri> // org.dexpace.kuri/Uri.resolve|resolve(kotlin.String;org.dexpace.kuri.ParseOptions){}[0]
final fun resolveOrNull(kotlin/String, org.dexpace.kuri/ParseOptions = ...): org.dexpace.kuri/Uri? // org.dexpace.kuri/Uri.resolveOrNull|resolveOrNull(kotlin.String;org.dexpace.kuri.ParseOptions){}[0]
Expand Down Expand Up @@ -524,9 +527,12 @@ final class org.dexpace.kuri/Url { // org.dexpace.kuri/Url|null[0]
final fun fileExtension(): kotlin/String // org.dexpace.kuri/Url.fileExtension|fileExtension(){}[0]
final fun fileName(): kotlin/String // org.dexpace.kuri/Url.fileName|fileName(){}[0]
final fun hasOpaqueOrigin(): kotlin/Boolean // org.dexpace.kuri/Url.hasOpaqueOrigin|hasOpaqueOrigin(){}[0]
final fun hasTrailingSlash(): kotlin/Boolean // org.dexpace.kuri/Url.hasTrailingSlash|hasTrailingSlash(){}[0]
final fun hashCode(): kotlin/Int // org.dexpace.kuri/Url.hashCode|hashCode(){}[0]
final fun isDirectory(): kotlin/Boolean // org.dexpace.kuri/Url.isDirectory|isDirectory(){}[0]
final fun isSpecial(): kotlin/Boolean // org.dexpace.kuri/Url.isSpecial|isSpecial(){}[0]
final fun newBuilder(): org.dexpace.kuri/Url.Builder // org.dexpace.kuri/Url.newBuilder|newBuilder(){}[0]
final fun redact(): org.dexpace.kuri/Url // org.dexpace.kuri/Url.redact|redact(){}[0]
final fun relativize(org.dexpace.kuri/Url): kotlin/String? // org.dexpace.kuri/Url.relativize|relativize(org.dexpace.kuri.Url){}[0]
final fun resolve(kotlin/String): org.dexpace.kuri.error/ParseResult<org.dexpace.kuri/Url> // org.dexpace.kuri/Url.resolve|resolve(kotlin.String){}[0]
final fun resolveOrNull(kotlin/String): org.dexpace.kuri/Url? // org.dexpace.kuri/Url.resolveOrNull|resolveOrNull(kotlin.String){}[0]
Expand Down
41 changes: 41 additions & 0 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.dexpace.kuri.parser.UriParser
import org.dexpace.kuri.parser.decodedSegments
import org.dexpace.kuri.parser.fileExtensionOf
import org.dexpace.kuri.parser.fileNameOf
import org.dexpace.kuri.parser.isDirectoryPath
import org.dexpace.kuri.parser.toUriPathString
import org.dexpace.kuri.percent.PercentCodec
import org.dexpace.kuri.percent.PercentEncodeSet
Expand Down Expand Up @@ -499,6 +500,46 @@ public class Uri internal constructor(
*/
public fun withoutFragment(): Uri = withFragment(null)

/**
* Returns a copy of this URI with its userinfo, query, and fragment removed, leaving the
* [scheme], [host], [port], and [path] intact (SPEC [CONF-120]).
*
* A convenience for logging or telemetry: it strips exactly the three components RFC 3986
* treats as sensitive or context-dependent (credentials, the query string, and the fragment)
* while every other component — including a userinfo-less authority — is preserved verbatim.
* A URI that already carries none of the three is returned equal in value (though [newBuilder]
* always rebuilds, so the result is not necessarily the same reference).
*
* @return a new `Uri` with no userinfo, query, or fragment.
*/
public fun redact(): Uri =
newBuilder()
.userInfo(null)
.query(null)
.fragment(null)
.build()

/**
* Reports whether this URI's path denotes a directory — its [encodedPath] ends in `/` (SPEC
* [PATH-3], [CONF-85]).
*
* True for a trailing-slash path such as `/a/` and for the root path `/` (a single empty
* segment); `false` for a path with content after its last segment (`/a`) and for a wholly
* empty path, which has no trailing slash to report. [hasTrailingSlash] is an exact alias, for
* a call site that prefers the WHATWG "trailing slash" phrasing over the filesystem-style
* "directory" term.
*
* @return `true` iff [encodedPath] ends in `/`.
*/
public fun isDirectory(): Boolean = components.path.isDirectoryPath()

/**
* Alias of [isDirectory] (SPEC [PATH-3], [CONF-85]); both accessors report the same condition.
*
* @return `true` iff [encodedPath] ends in `/`.
*/
public fun hasTrailingSlash(): Boolean = isDirectory()

/** The canonical [uriString]; a parsed `Uri` round-trips through `toString` then [parse]. */
override fun toString(): String = uriString

Expand Down
45 changes: 45 additions & 0 deletions kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.dexpace.kuri.parser.UrlParser
import org.dexpace.kuri.parser.decodedSegments
import org.dexpace.kuri.parser.fileExtensionOf
import org.dexpace.kuri.parser.fileNameOf
import org.dexpace.kuri.parser.isDirectoryPath
import org.dexpace.kuri.percent.PercentCodec
import org.dexpace.kuri.percent.PercentEncodeSet
import org.dexpace.kuri.percent.PercentEncodeSets
Expand Down Expand Up @@ -517,6 +518,50 @@ public class Url internal constructor(
*/
public fun withoutFragment(): Url = withFragment(null)

/**
* Returns a copy of this URL with its userinfo (username and password), query, and fragment
* removed, leaving the [scheme], [host], [port], and [encodedPath] intact (SPEC [CONF-120]).
*
* A convenience for logging or telemetry: it strips exactly the components WHATWG/RFC 3986
* treat as sensitive or context-dependent (credentials, the query string, and the fragment)
* while every other component — including a credential-less authority — is preserved
* verbatim. For an opaque-path URL such as `mailto:user@example.com`, the `user@` text is part
* of the opaque path rather than real userinfo (there is no authority at all) and so is left
* untouched; only the actual query and fragment are stripped. A URL that already carries none
* of the three is returned equal in value (though [newBuilder] always rebuilds, so the result
* is not necessarily the same reference).
*
* @return a new `Url` with no username, password, query, or fragment.
*/
public fun redact(): Url =
newBuilder()
.username("")
.password("")
.query(null)
.fragment(null)
.build()

/**
* Reports whether this URL's path denotes a directory — its [encodedPath] ends in `/` (SPEC
* [PATH-3], [CONF-85]).
*
* True for a trailing-slash path such as `/a/` and for the root path `/` (a single empty
* segment) — which every special-scheme URL with an otherwise-empty path canonicalizes to, so
* e.g. `https://h` and `https://h/` both report `true`. `false` for a path with content after
* its last segment (`/a`). [hasTrailingSlash] is an exact alias, for a call site that prefers
* the WHATWG "trailing slash" phrasing over the filesystem-style "directory" term.
*
* @return `true` iff [encodedPath] ends in `/`.
*/
public fun isDirectory(): Boolean = components.path.isDirectoryPath()

/**
* Alias of [isDirectory] (SPEC [PATH-3], [CONF-85]); both accessors report the same condition.
*
* @return `true` iff [encodedPath] ends in `/`.
*/
public fun hasTrailingSlash(): Boolean = isDirectory()

/**
* The WHATWG `protocol` setter (URL §5): returns a copy with [value]'s scheme, or this URL
* unchanged when the change is not permitted (special↔non-special, or an invalid `file`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ internal fun splitUriPath(path: String): ComponentPath.Segments =
else -> ComponentPath.Segments(path.split('/'), rooted = false)
}

/**
* Whether [path] denotes a directory-style path — one whose serialization ends in `/` (SPEC
* [PATH-3], [CONF-85]). Shared by the `Uri`/`Url` `isDirectory`/`hasTrailingSlash` projections.
*
* For a [Segments][ComponentPath.Segments] path this is exactly "the last segment is the empty
* string": a non-empty list whose final element is `""`, which covers both the root path `/`
* (`[""]`) and any explicitly trailing-slashed path (`/a/`, `["a", ""]`). A wholly empty path
* (`emptyList()`, serialized `""`) has no trailing slash to report. For an
* [Opaque][ComponentPath.Opaque] path — which has no segment structure — the equivalent
* serialization-ends-in-`/` test is applied directly to its verbatim text.
*/
internal fun ComponentPath.isDirectoryPath(): Boolean =
when (this) {
is ComponentPath.Segments -> segments.isNotEmpty() && segments.last().isEmpty()
is ComponentPath.Opaque -> path.endsWith(URI_PATH_SEPARATOR)
}

/**
* The "file name" of decoded path [segments]: the last non-empty segment, or `""` when there is
* none (SPEC §3.3). Shared by the `Uri`/`Url` `fileName()` projections.
Expand Down
65 changes: 65 additions & 0 deletions kuri/src/commonTest/kotlin/org/dexpace/kuri/UriDxTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,71 @@ class UriDxTest {
assertNull(parseOk("http://h/p#x").withoutFragment().fragment)
}

// --- redact ---

@Test
fun `redact strips userinfo query and fragment but keeps scheme host port and path`() {
val uri = parseOk("http://user:pass@h:8080/p/a?q=1#frag")

val redacted = uri.redact()

assertEquals("http://h:8080/p/a", redacted.uriString)
assertNull(redacted.userInfo)
assertNull(redacted.query)
assertNull(redacted.fragment)
assertEquals("h", redacted.hostName)
assertEquals(8080, redacted.port)
assertEquals("/p/a", redacted.encodedPath)
}

@Test
fun `redact is a no-op in value when there is no userinfo query or fragment already`() {
val uri = parseOk("http://h:8080/p")

assertEquals(uri, uri.redact())
}

@Test
fun `redact strips only the credential-bearing components on a relative reference`() {
val uri = parseOk("/a/b?q#f")

val redacted = uri.redact()

assertEquals("/a/b", redacted.uriString)
assertNull(redacted.query)
assertNull(redacted.fragment)
}

// --- isDirectory / hasTrailingSlash ---

@Test
fun `isDirectory and hasTrailingSlash agree on a trailing empty segment`() {
val trailingSlash = parseOk("http://h/a/")

assertTrue(trailingSlash.isDirectory())
assertTrue(trailingSlash.hasTrailingSlash())

val noTrailingSlash = parseOk("http://h/a")

assertFalse(noTrailingSlash.isDirectory())
assertFalse(noTrailingSlash.hasTrailingSlash())
}

@Test
fun `isDirectory is true for the root path`() {
assertTrue(parseOk("http://h/").isDirectory())
}

@Test
fun `isDirectory is false for a wholly empty path`() {
assertFalse(parseOk("http://h").isDirectory())
}

@Test
fun `isDirectory is false for an opaque path with no trailing slash`() {
assertFalse(parseOk("mailto:a@example.com").isDirectory())
}

@Test
fun `relativize returns null across different schemes`() {
// The authorities match, but a differing scheme fails the shared-hierarchy check, so there is no
Expand Down
Loading
Loading