Files
dht/jni/types.rs
T

404 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::{DHTOptions, FileInfo, MetadataOptions, TorrentInfo, types::NetMode};
use jni::JNIEnv;
use jni::objects::{JObject, JString, JValue};
use jni::sys::jlong;
#[derive(Debug, thiserror::Error)]
pub enum DhtOptionsConversionError {
#[error(transparent)]
Jni(#[from] jni::errors::Error),
#[error("invalid DHTOptions.{field}: expected {expected}, got {value}")]
InvalidValue {
field: &'static str,
expected: &'static str,
value: i64,
},
}
type DhtOptionsResult<T> = Result<T, DhtOptionsConversionError>;
fn invalid_value(
field: &'static str,
expected: &'static str,
value: impl Into<i64>,
) -> DhtOptionsConversionError {
DhtOptionsConversionError::InvalidValue {
field,
expected,
value: value.into(),
}
}
fn checked_port(value: i32) -> DhtOptionsResult<u16> {
u16::try_from(value).map_err(|_| invalid_value("port", "an integer in 0..=65535", value))
}
fn checked_non_negative_u32(field: &'static str, value: i32) -> DhtOptionsResult<u32> {
u32::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_non_negative_u64(field: &'static str, value: i64) -> DhtOptionsResult<u64> {
u64::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_non_negative_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
usize::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
}
fn checked_positive_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
let value = checked_non_negative_usize(field, value)?;
if value == 0 {
return Err(invalid_value(
field,
"an integer greater than or equal to 1",
0,
));
}
Ok(value)
}
fn checked_percentage(field: &'static str, value: i32) -> DhtOptionsResult<u8> {
if !(0..=100).contains(&value) {
return Err(invalid_value(field, "an integer in 0..=100", value));
}
Ok(u8::try_from(value).expect("0..=100 always fits in u8"))
}
fn checked_netmode(value: i32) -> DhtOptionsResult<NetMode> {
match value {
0 => Ok(NetMode::Ipv4Only),
1 => Ok(NetMode::Ipv6Only),
2 => Ok(NetMode::DualStack),
_ => Err(invalid_value("netMode", "one of 0, 1, or 2", value)),
}
}
// ──────────────────────────────────────────────────────────────────────────────
// 常量:Java 类全限定名
// ──────────────────────────────────────────────────────────────────────────────
const CLASS_TORRENT_INFO: &str = "cn/lmcw/dht/model/TorrentInfo";
const CLASS_FILE_INFO: &str = "cn/lmcw/dht/model/FileInfo";
const CLASS_ARRAY_LIST: &str = "java/util/ArrayList";
// ──────────────────────────────────────────────────────────────────────────────
// 基础类型转换
// ──────────────────────────────────────────────────────────────────────────────
/// Rust String → Java Stringjstring
pub fn rust_str_to_jstring<'local>(
env: &mut JNIEnv<'local>,
s: &str,
) -> jni::errors::Result<JObject<'local>> {
let js: JString<'local> = env.new_string(s)?;
Ok(js.into())
}
// ──────────────────────────────────────────────────────────────────────────────
// FileInfo: Rust → Java
// ──────────────────────────────────────────────────────────────────────────────
/// 将 Rust `FileInfo` 构造为 Java `cn.lmcw.dht.model.FileInfo` 对象。
pub fn file_info_to_java<'local>(
env: &mut JNIEnv<'local>,
fi: &FileInfo,
) -> jni::errors::Result<JObject<'local>> {
env.with_local_frame_returning_local(4, |env| {
let cls = env.find_class(CLASS_FILE_INFO)?;
let path = rust_str_to_jstring(env, &fi.path)?;
env.new_object(
&cls,
"(Ljava/lang/String;J)V",
&[JValue::Object(&path), JValue::Long(fi.size as jlong)],
)
})
}
// ──────────────────────────────────────────────────────────────────────────────
// TorrentInfo: Rust → Java
// ──────────────────────────────────────────────────────────────────────────────
/// 将 Rust `TorrentInfo` 构造为 Java `cn.lmcw.dht.model.TorrentInfo` 对象。
/// files 字段被构造为 `java.util.ArrayList<FileInfo>`。
pub fn torrent_info_to_java<'local>(
env: &mut JNIEnv<'local>,
ti: &TorrentInfo,
) -> jni::errors::Result<JObject<'local>> {
let cls = env.find_class(CLASS_TORRENT_INFO)?;
// 构造 files list
let list = build_file_list(env, &ti.files)?;
// 构造 peers listList<String>
let peers_list = build_string_list(env, &ti.peers)?;
let info_hash = rust_str_to_jstring(env, &ti.info_hash)?;
let magnet = rust_str_to_jstring(env, &ti.magnet_link)?;
let name = rust_str_to_jstring(env, &ti.name)?;
let obj = env.new_object(
&cls,
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/util/List;JLjava/util/List;J)V",
&[
JValue::Object(&info_hash),
JValue::Object(&magnet),
JValue::Object(&name),
JValue::Long(ti.total_size as jlong),
JValue::Object(&list),
JValue::Long(ti.piece_length as jlong),
JValue::Object(&peers_list),
JValue::Long(ti.timestamp as jlong),
],
)?;
Ok(obj)
}
/// 构造 `java.util.ArrayList` 并填入 FileInfo 对象列表。
fn build_file_list<'local>(
env: &mut JNIEnv<'local>,
files: &[FileInfo],
) -> jni::errors::Result<JObject<'local>> {
let list_cls = env.find_class(CLASS_ARRAY_LIST)?;
let list = env.new_object(&list_cls, "()V", &[])?;
for fi in files {
let jfi = file_info_to_java(env, fi)?;
env.call_method(
&list,
"add",
"(Ljava/lang/Object;)Z",
&[JValue::Object(&jfi)],
)?;
env.delete_local_ref(jfi)?;
}
Ok(list)
}
/// 构造 `java.util.ArrayList` 并填入字符串列表。
fn build_string_list<'local>(
env: &mut JNIEnv<'local>,
strs: &[String],
) -> jni::errors::Result<JObject<'local>> {
let list_cls = env.find_class(CLASS_ARRAY_LIST)?;
let list = env.new_object(&list_cls, "()V", &[])?;
for s in strs {
let js = rust_str_to_jstring(env, s)?;
env.call_method(
&list,
"add",
"(Ljava/lang/Object;)Z",
&[JValue::Object(&js)],
)?;
env.delete_local_ref(js)?;
}
Ok(list)
}
// ──────────────────────────────────────────────────────────────────────────────
// DHTOptions: Java → Rust
// ──────────────────────────────────────────────────────────────────────────────
/// 从 JVM bindings 的 options DTO 读取字段,构造 Rust `DHTOptions`。
pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> DhtOptionsResult<DHTOptions> {
let port = checked_port(env.get_field(obj, "port", "I")?.i()?)?;
let metadata_timeout_secs = checked_non_negative_u64(
"metadataTimeout",
env.get_field(obj, "metadataTimeout", "J")?.j()?,
)?;
let metadata_max_queue_size = checked_positive_usize(
"maxMetadataQueueSize",
env.get_field(obj, "maxMetadataQueueSize", "I")?.i()?,
)?;
let metadata_max_worker_count = checked_positive_usize(
"maxMetadataWorkerCount",
env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()?,
)?;
let pool_capacity = checked_positive_usize(
"poolCapacity",
env.get_field(obj, "poolCapacity", "I")?.i()?,
)?;
let find_node_rate = checked_non_negative_u32(
"findNodeRatePerSecond",
env.get_field(obj, "findNodeRatePerSecond", "I")?.i()?,
)?;
let find_node_burst = checked_non_negative_u32(
"findNodeBurst",
env.get_field(obj, "findNodeBurst", "I")?.i()?,
)?;
let max_find_node_in_flight = checked_positive_usize(
"maxFindNodeInFlight",
env.get_field(obj, "maxFindNodeInFlight", "I")?.i()?,
)?;
let max_new_destinations = checked_non_negative_u32(
"maxNewDestinationsPerMinute",
env.get_field(obj, "maxNewDestinationsPerMinute", "I")?
.i()?,
)?;
let max_replacements = checked_non_negative_u32(
"maxReplacementsPerMinute",
env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()?,
)?;
let request_timeout_secs = checked_non_negative_u64(
"requestTimeoutSeconds",
env.get_field(obj, "requestTimeoutSeconds", "J")?.j()?,
)?;
let max_response_rate = checked_non_negative_u32(
"maxResponseRatePerSecond",
env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()?,
)?;
let max_response_bytes = checked_non_negative_u64(
"maxResponseBytesPerSecond",
env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()?,
)?;
let max_response_per_source = checked_non_negative_u32(
"maxResponseRatePerSource",
env.get_field(obj, "maxResponseRatePerSource", "I")?.i()?,
)?;
let pressure_floor = checked_percentage(
"metadataPressureFloorPercent",
env.get_field(obj, "metadataPressureFloorPercent", "I")?
.i()?,
)?;
let recent_probe_ttl = checked_non_negative_u64(
"recentProbeTtlSeconds",
env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()?,
)?;
let responsive_capacity = checked_positive_usize(
"responsiveCapacity",
env.get_field(obj, "responsiveCapacity", "I")?.i()?,
)?;
let responsive_ttl = checked_non_negative_u64(
"responsiveTtlSeconds",
env.get_field(obj, "responsiveTtlSeconds", "J")?.j()?,
)?;
let low_watermark = checked_non_negative_usize(
"poolLowWatermark",
env.get_field(obj, "poolLowWatermark", "I")?.i()?,
)?;
if low_watermark > pool_capacity {
return Err(invalid_value(
"poolLowWatermark",
"an integer no greater than poolCapacity",
i64::try_from(low_watermark).expect("Java int always fits in i64"),
));
}
let subnet_in_flight = checked_positive_usize(
"maxInFlightPerSubnet",
env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()?,
)?;
let hash_queue_capacity = checked_positive_usize(
"hashQueueCapacity",
env.get_field(obj, "hashQueueCapacity", "I")?.i()?,
)?;
let netmode = checked_netmode(env.get_field(obj, "netMode", "I")?.i()?)?;
let mut options = DHTOptions {
port,
netmode,
hash_queue_capacity,
metadata: MetadataOptions {
timeout_secs: metadata_timeout_secs,
max_queue_size: metadata_max_queue_size,
max_worker_count: metadata_max_worker_count,
..MetadataOptions::default()
},
..DHTOptions::default()
};
options.crawl.pool.capacity = pool_capacity;
options.crawl.pool.recent_probe_ttl_secs = recent_probe_ttl;
options.crawl.pool.responsive_capacity = responsive_capacity;
options.crawl.pool.responsive_ttl_secs = responsive_ttl;
options.crawl.pool.low_watermark = low_watermark;
options.crawl.rate_limit.max_find_node_rate_per_sec = find_node_rate;
options.crawl.rate_limit.burst = find_node_burst;
options.crawl.rate_limit.max_in_flight = max_find_node_in_flight;
options.crawl.rate_limit.max_new_destinations_per_minute = max_new_destinations;
options.crawl.rate_limit.request_timeout_secs = request_timeout_secs;
options.crawl.rate_limit.max_response_rate_per_sec = max_response_rate;
options.crawl.rate_limit.max_response_bytes_per_sec = max_response_bytes;
options.crawl.rate_limit.max_response_rate_per_source = max_response_per_source;
options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor;
options.crawl.rate_limit.max_replacements_per_minute = max_replacements;
options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight;
Ok(options)
}
/// 从 JVM bindings 的 options DTO 读取,或若为 null 则返回默认选项。
pub fn java_to_dht_options_or_default(
env: &mut JNIEnv,
obj: &JObject,
) -> DhtOptionsResult<DHTOptions> {
if obj.is_null() {
Ok(DHTOptions::default())
} else {
java_to_dht_options(env, obj)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_invalid<T>(result: DhtOptionsResult<T>, field: &'static str, value: i64) {
match result {
Err(DhtOptionsConversionError::InvalidValue {
field: actual_field,
value: actual_value,
..
}) => {
assert_eq!(actual_field, field);
assert_eq!(actual_value, value);
}
_ => panic!("expected an invalid-value error"),
}
}
#[test]
fn port_accepts_java_boundaries_and_rejects_out_of_range_values() {
assert_eq!(checked_port(0).unwrap(), 0);
assert_eq!(checked_port(i32::from(u16::MAX)).unwrap(), u16::MAX);
assert_invalid(checked_port(-1), "port", -1);
assert_invalid(checked_port(i32::from(u16::MAX) + 1), "port", 65_536);
}
#[test]
fn signed_values_are_checked_before_unsigned_conversion() {
assert_eq!(checked_non_negative_u32("rate", 0).unwrap(), 0);
assert_eq!(
checked_non_negative_u32("rate", i32::MAX).unwrap(),
i32::MAX as u32
);
assert_invalid(checked_non_negative_u32("rate", -1), "rate", -1);
assert_eq!(checked_non_negative_u64("timeout", 0).unwrap(), 0);
assert_eq!(
checked_non_negative_u64("timeout", i64::MAX).unwrap(),
i64::MAX as u64
);
assert_invalid(checked_non_negative_u64("timeout", -1), "timeout", -1);
}
#[test]
fn capacities_and_in_flight_limits_must_be_positive() {
assert_eq!(checked_positive_usize("capacity", 1).unwrap(), 1);
assert_eq!(
checked_positive_usize("capacity", i32::MAX).unwrap(),
i32::MAX as usize
);
assert_invalid(checked_positive_usize("capacity", 0), "capacity", 0);
assert_invalid(checked_positive_usize("capacity", -1), "capacity", -1);
}
#[test]
fn percentage_and_netmode_only_accept_documented_values() {
assert_eq!(checked_percentage("percent", 0).unwrap(), 0);
assert_eq!(checked_percentage("percent", 100).unwrap(), 100);
assert_invalid(checked_percentage("percent", -1), "percent", -1);
assert_invalid(checked_percentage("percent", 101), "percent", 101);
assert_eq!(checked_netmode(0).unwrap(), NetMode::Ipv4Only);
assert_eq!(checked_netmode(1).unwrap(), NetMode::Ipv6Only);
assert_eq!(checked_netmode(2).unwrap(), NetMode::DualStack);
assert_invalid(checked_netmode(-1), "netMode", -1);
assert_invalid(checked_netmode(3), "netMode", 3);
}
}