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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ jobs:
uses: actions/checkout@v4
with:
repository: ZettaScaleLabs/zenoh-flat-jni
ref: dbb1f8c6509e333f49d1c03ecb84dcf07b7af7b8
ref: 757cc6ad7e346f428ffc5e3252ba01ff4d19ba9b
path: zenoh-flat-jni

- name: Check out zenoh-flat
uses: actions/checkout@v4
with:
repository: ZettaScaleLabs/zenoh-flat
ref: 6d22091d75912be1c14330b0f78fa1d4bf258ea3
ref: 1ece3df5e64db63f980a43e8ae661bdd5ed760c2
path: zenoh-flat

- name: Use prebindgen from GitHub main
Expand Down
3 changes: 2 additions & 1 deletion ZENOH_FLAT_TRANSITION.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ zenoh (Rust)
| --- | --- | --- |
| [#481](https://github.com/eclipse-zenoh/zenoh-java/pull/481) | Use receiver-style zenoh-flat-jni bindings (generated Session/Query/Publisher methods, split key-expr overloads, de-prefixed callback names); +61–83% subscriber throughput | merged |
| [#484](https://github.com/eclipse-zenoh/zenoh-java/pull/484) | `Encoding`/`ZenohId.toString` become pure JVM values (fixes a per-message native leak); the conversion logic lives in the **shared tier** (`EncodingCodec`/`zidString` in zenoh-flat-jni, correspondence-tested against native, reusable by zenoh-kotlin) with only a thin facade here; publisher **default encoding set natively at declare** (plain puts cross no encoding data). Pairs with [zenoh-flat-jni#4](https://github.com/ZettaScaleLabs/zenoh-flat-jni/pull/4), [zenoh-flat#3](https://github.com/ZettaScaleLabs/zenoh-flat/pull/3) (merged), [prebindgen#80](https://github.com/milyin/prebindgen/pull/80) (merged) | in progress |
| [#491](https://github.com/eclipse-zenoh/zenoh-java/pull/491) | `Session.undeclare` detaches the key-expr handle even when the native undeclare fails (the generated wrapper consumes it either way); found on the zenoh-kotlin port ([zenoh-kotlin#668](https://github.com/eclipse-zenoh/zenoh-kotlin/pull/668)) | open |
| [#491](https://github.com/eclipse-zenoh/zenoh-java/pull/491) | `Session.undeclare` detaches the key-expr handle even when the native undeclare fails (the generated wrapper consumes it either way); found on the zenoh-kotlin port ([zenoh-kotlin#668](https://github.com/eclipse-zenoh/zenoh-kotlin/pull/668)) | merged |
| shared-parameters | `Parameters` becomes a thin facade over the shared string-backed implementation in zenoh-flat-jni (Rust `parameters.rs` semantics: no percent-decoding, infallible parse, first-match-wins get) + `ParametersCorrespondenceTest` vs the native oracle. Pairs with [zenoh-flat#4](https://github.com/ZettaScaleLabs/zenoh-flat/pull/4), [zenoh-flat-jni#9](https://github.com/ZettaScaleLabs/zenoh-flat-jni/pull/9) | open |

Companion PRs in the upstream repos are coordinated per constituent PR (e.g.
[ZettaScaleLabs/zenoh-flat-jni#3](https://github.com/ZettaScaleLabs/zenoh-flat-jni/pull/3)
Expand Down
16 changes: 15 additions & 1 deletion zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,21 @@ internal fun queryCallbackOf(
// on a channel by a queue handler) and replied to later. The native query
// is dropped when it is replied to (see [Query.reply]) or when [Query] is
// closed — that drop is what finalizes the querier's get.
f(Query.fromParts(keStr, parameters, payloadH, encId, encSchema, attachH, acceptsReplies, zq))
val query = try {
Query.fromParts(keStr, parameters, payloadH, encId, encSchema, attachH, acceptsReplies, zq)
} catch (t: Throwable) {
// Defense in depth: the leaves carry remote (attacker-controlled)
// data, and fromParts must be total on it — but if decomposition
// ever throws, free the owned native leaves (payload/attachment
// have no GC backstop) and finalize the query so the remote get
// completes, instead of leaking them. The rethrown exception is
// swallowed by the native callback bridge, by design.
payloadH?.close()
attachH?.close()
zq.close()
throw t
}
f(query)
}

internal fun replyCallbackOf(
Expand Down
81 changes: 35 additions & 46 deletions zenoh-java/src/commonMain/kotlin/io/zenoh/query/Parameters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,90 +14,74 @@

package io.zenoh.query

import java.net.URLDecoder
import io.zenoh.jni.query.Parameters as JniParameters

/**
* Parameters of the [Selector].
*
* When in string form, the `parameters` should be encoded like the query section of a URL:
* - parameters are separated by `;`,
* A thin facade over the shared string-backed implementation
* ([io.zenoh.jni.query.Parameters]), which mirrors Rust's
* `zenoh::query::Parameters` exactly. The string form follows the
* `a=b;c=d|e;f=g` format:
* - parameters are separated by `;` (empty chunks are skipped),
* - the parameter name and value are separated by the first `=`,
* - in the absence of `=`, the parameter value is considered to be the empty string,
* - both name and value should use percent-encoding to escape characters,
* - defining a value for the same parameter name twice is considered undefined behavior and an
* error result is returned.
* - the value is taken verbatim (no percent-decoding),
* - a duplicated parameter name is allowed: [get] returns the FIRST
* occurrence, and the duplicates survive round-tripping until an
* [insert]/[remove] normalizes the string.
*
* @see Selector
*/
data class Parameters internal constructor(private val params: MutableMap<String, String>) : IntoParameters {
class Parameters internal constructor(internal val inner: JniParameters) : IntoParameters {

companion object {

private const val LIST_SEPARATOR = ";"
private const val FIELD_SEPARATOR = "="
private const val VALUE_SEPARATOR = "|"

/**
* Creates an empty Parameters.
*/
@JvmStatic
fun empty() = Parameters(mutableMapOf())
fun empty() = Parameters(JniParameters.empty())

/**
* Creates a [Parameters] instance from the provided map.
*/
@JvmStatic
fun from(params: Map<String, String>): Parameters = Parameters(params.toMutableMap())
fun from(params: Map<String, String>): Parameters = Parameters(JniParameters.fromMap(params))

/**
* Attempts to create a [Parameters] from a string.
* Creates a [Parameters] from a string in the `a=b;c=d|e;f=g` format.
*
* When in string form, the `parameters` should be encoded like the query section of a URL:
* - parameters are separated by `;`,
* - the parameter name and value are separated by the first `=`,
* - in the absence of `=`, the parameter value is considered to be the empty string,
* - both name and value should use percent-encoding to escape characters,
* - defining a value for the same parameter name twice is considered undefined behavior and an
* error result is returned.
* Construction is infallible — any string is accepted, exactly as in
* the Rust layer.
*/
@JvmStatic
fun from(params: String): Parameters {
if (params.isBlank()) {
return Parameters(mutableMapOf())
}
return params.split(LIST_SEPARATOR).fold(mutableMapOf<String, String>()) { parametersMap, parameter ->
val (key, value) = parameter.split(FIELD_SEPARATOR).let { it[0] to it.getOrNull(1) }
require(!parametersMap.containsKey(key)) { "Duplicated parameter `$key` detected." }
parametersMap[key] = value?.let { URLDecoder.decode(it, Charsets.UTF_8.name()) } ?: ""
parametersMap
}.let { Parameters(it) }
}
fun from(params: String): Parameters = Parameters(JniParameters.fromString(params))
}

override fun toString(): String =
params.entries.joinToString(LIST_SEPARATOR) { "${it.key}$FIELD_SEPARATOR${it.value}" }
override fun toString(): String = inner.toString()

override fun into(): Parameters = this

/**
* Returns empty if no parameters were provided.
*/
fun isEmpty(): Boolean = params.isEmpty()
fun isEmpty(): Boolean = inner.isEmpty()

/**
* Returns true if the [key] is contained.
*/
fun containsKey(key: String): Boolean = params.containsKey(key)
fun containsKey(key: String): Boolean = inner.containsKey(key)

/**
* Returns the value of the [key] if present.
* Returns the value of the [key] if present (the first occurrence wins).
*/
fun get(key: String): String? = params[key]
fun get(key: String): String? = inner.get(key)

/**
* Returns the value of the [key] if present, or if not, the [default] value provided.
*/
fun getOrDefault(key: String, default: String): String = params.getOrDefault(key, default)
fun getOrDefault(key: String, default: String): String = inner.getOrDefault(key, default)

/**
* Returns the values of the [key] if present.
Expand All @@ -108,39 +92,44 @@ data class Parameters internal constructor(private val params: MutableMap<String
* assertEquals(List.of("1", "2", "3"), parameters.values("c"))
* ```
*/
fun values(key: String): List<String>? = params[key]?.split(VALUE_SEPARATOR)
fun values(key: String): List<String>? = if (inner.containsKey(key)) inner.values(key) else null

/**
* Inserts the key-value pair into the parameters, returning the old value
* in case of it being already present.
*/
fun insert(key: String, value: String): String? = params.put(key, value)
fun insert(key: String, value: String): String? = inner.insert(key, value)

/**
* Removes the [key] parameter, returning its value.
*/
fun remove(key: String): String? = params.remove(key)
fun remove(key: String): String? = inner.remove(key)

/**
* Extends the parameters with the [parameters] provided, overwriting
* any conflicting params.
*/
fun extend(parameters: IntoParameters) {
params.putAll(parameters.into().params)
inner.extend(parameters.into().inner)
}

/**
* Extends the parameters with the [parameters] provided, overwriting
* any conflicting params.
*/
fun extend(parameters: Map<String, String>) {
params.putAll(parameters)
inner.extend(parameters)
}

/**
* Returns a map with the key value pairs of the parameters.
* Returns a map with the key value pairs of the parameters. For a
* duplicated key the last occurrence wins (unlike [get]).
*/
fun toMap(): Map<String, String> = params
fun toMap(): Map<String, String> = inner.toMap()

override fun equals(other: Any?): Boolean = other is Parameters && inner == other.inner

override fun hashCode(): Int = inner.hashCode()
}

interface IntoParameters {
Expand Down
3 changes: 3 additions & 0 deletions zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ class Query internal constructor(
zq: io.zenoh.jni.query.Query,
): Query {
val ke = KeyExpr(keStr)
// The parameters string is ATTACKER-CONTROLLED (the Rust layer
// forwards any selector parameters untouched) — the shared
// string-backed Parameters accepts any input, never throwing.
val selector = if (parameters.isEmpty()) Selector(ke)
else Selector(ke, Parameters.from(parameters))
return Query(
Expand Down
14 changes: 6 additions & 8 deletions zenoh-java/src/commonMain/kotlin/io/zenoh/query/Selector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@ import io.zenoh.keyexpr.KeyExpr
*
* When in string form, selectors look a lot like a URI, with similar semantics:
* - the `key_expr` before the first `?` must be a valid key expression.
* - the `parameters` after the first `?` should be encoded like the query section of a URL:
* - the `parameters` after the first `?` follow the `a=b;c=d|e;f=g` format:
* - parameters are separated by `;`,
* - the parameter name and value are separated by the first `=`,
* - in the absence of `=`, the parameter value is considered to be the empty string,
* - both name and value should use percent-encoding to escape characters,
* - defining a value for the same parameter name twice is considered undefined behavior,
* with the encouraged behaviour being to reject operations when a duplicate parameter is detected.
* - the value is taken verbatim (no percent-decoding is applied),
* - a duplicated parameter name is allowed; lookups return the first occurrence.
*
* Zenoh intends to standardize the usage of a set of parameter names. To avoid conflicting with RPC parameters,
* the Zenoh team has settled on reserving the set of parameter names that start with non-alphanumeric characters.
Expand All @@ -60,13 +59,12 @@ data class Selector(val keyExpr: KeyExpr, val parameters: Parameters? = null) :
*
* When in string form, selectors look a lot like a URI, with similar semantics:
* - the `key_expr` before the first `?` must be a valid key expression.
* - the `parameters` after the first `?` should be encoded like the query section of a URL:
* - the `parameters` after the first `?` follow the `a=b;c=d|e;f=g` format:
* - parameters are separated by `;`,
* - the parameter name and value are separated by the first `=`,
* - in the absence of `=`, the parameter value is considered to be the empty string,
* - both name and value should use percent-encoding to escape characters,
* - defining a value for the same parameter name twice is considered undefined behavior,
* with the encouraged behaviour being to reject operations when a duplicate parameter is detected.
* - the value is taken verbatim (no percent-decoding is applied),
* - a duplicated parameter name is allowed; lookups return the first occurrence.
*
* @param selector The selector expression as a String.
* @return An instance [Selector].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ import org.junit.Test
* The SDK implements the encoding string ↔ `(id, schema)` conversion in pure
* Kotlin (no JNI crossing), on the contract that any JVM-side reimplementation
* of zenoh-flat API must be verified against the native implementation. These
* tests drive both implementations — the pure one and the native oracle (the
* tests drive both implementations — the pure one and the native one (the
* generated `Encoding` handle methods) — over the whole predefined id range
* plus the edge shapes of the parse/render rules, asserting equal results.
*/
class EncodingCorrespondenceTest {

/** Native oracle: the canonical string of `(id, schema)`. */
/** Native implementation: the canonical string of `(id, schema)`. */
private fun nativeRender(id: Int, schema: String?): String {
val h = JniEncoding.newFromId(id, schema, throwZError0)
try {
Expand All @@ -42,7 +42,7 @@ class EncodingCorrespondenceTest {
}
}

/** Native oracle: parse a string into `(id, schema, canonical string)`. */
/** Native implementation: parse a string into `(id, schema, canonical string)`. */
private fun nativeParse(s: String): Triple<Int, String?, String> {
val h = JniEncoding.newFromString(s, throwZError0)
try {
Expand Down
Loading
Loading