1#![allow(unsafe_code)]
2#![allow(clippy::missing_panics_doc)]
3#![allow(clippy::not_unsafe_ptr_arg_deref)]
4
5use std::ffi::CStr;
6use std::os::raw::{c_char, c_uchar};
7use std::panic::{AssertUnwindSafe, catch_unwind};
8use std::path::PathBuf;
9use std::ptr;
10use std::sync::{Arc, LazyLock, Mutex};
11use std::time::Duration;
12
13use takanawa_core::{HashConfig, TakanawaError};
14use takanawa_http::{
15 DEFAULT_MAX_IO, DownloadConfig, DownloadEngine, DownloadHandle, DownloadPhase, RetryConfig,
16 TimeoutConfig,
17};
18use tokio::runtime::{Builder, Runtime};
19
20pub const TKNW_ABI_VERSION: u32 = 1;
21
22#[repr(i32)]
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum TknwStatus {
25 Ok = 0,
26 BufferTooSmall = 1,
27 NullPointer = -1,
28 AbiMismatch = -2,
29 InvalidConfig = -3,
30 RuntimeNotInitialized = -4,
31 TargetExists = -10,
32 PartBusy = -11,
33 PartSizeMismatch = -12,
34 PartCorrupt = -13,
35 RemoteChanged = -14,
36 HttpProtocol = -20,
37 Network = -21,
38 Io = -30,
39 HashMismatch = -40,
40 Cancelled = -50,
41 AlreadyStarted = -51,
42 Panic = -100,
43 Internal = -101,
44}
45
46#[repr(u32)]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TknwHashKind {
49 None = 0,
50 Sha256 = 1,
51}
52
53#[repr(C)]
54#[derive(Debug, Clone, Copy)]
55pub struct TknwGlobalConfig {
56 pub abi_version: u32,
57 pub struct_size: usize,
58 pub max_io: usize,
59}
60
61#[repr(C)]
62#[derive(Debug, Clone, Copy)]
63pub struct TknwDownloadConfig {
64 pub abi_version: u32,
65 pub struct_size: usize,
66 pub url: *const c_char,
67 pub target_path: *const c_char,
68 pub chunk_size: u64,
69 pub parallelism: usize,
70 pub max_parallel_chunks: usize,
71 pub max_retries: u32,
72 pub backoff_initial_millis: u64,
73 pub backoff_max_millis: u64,
74 pub connect_timeout_millis: u64,
75 pub read_timeout_millis: u64,
76 pub total_timeout_millis: u64,
77 pub bytes_per_second_limit: u64,
78 pub hash_kind: u32,
79 pub expected_sha256: *const c_uchar,
80 pub expected_sha256_len: usize,
81}
82
83#[repr(C)]
84#[derive(Debug, Clone, Copy)]
85pub struct TknwDownloadSnapshot {
86 pub abi_version: u32,
87 pub struct_size: usize,
88 pub phase: u32,
89 pub content_len: u64,
90 pub downloaded_bytes: u64,
91 pub chunk_size: u64,
92 pub chunk_count: u64,
93 pub completed_chunks: u64,
94 pub active_io: usize,
95}
96
97pub struct TknwDownload {
98 global: Arc<GlobalRuntime>,
99 inner: DownloadHandle,
100 last_error: Mutex<Option<String>>,
101}
102
103struct GlobalRuntime {
104 runtime: Runtime,
105 engine: DownloadEngine,
106}
107
108static GLOBAL: LazyLock<Mutex<Option<Arc<GlobalRuntime>>>> = LazyLock::new(|| Mutex::new(None));
109
110#[unsafe(no_mangle)]
111pub extern "C" fn tknw_global_init(config: *const TknwGlobalConfig) -> TknwStatus {
112 ffi_boundary(|| {
113 let max_io = if config.is_null() {
114 DEFAULT_MAX_IO
115 } else {
116 let config = unsafe { &*config };
118 validate_abi(
119 "TknwGlobalConfig",
120 config.abi_version,
121 config.struct_size,
122 size_of::<TknwGlobalConfig>(),
123 )?;
124 if config.max_io == 0 {
125 DEFAULT_MAX_IO
126 } else {
127 config.max_io
128 }
129 };
130 let mut global = GLOBAL.lock().expect("global runtime mutex poisoned");
131 if let Some(existing) = global.as_ref() {
132 existing.engine.set_max_io(max_io);
133 return Ok(TknwStatus::Ok);
134 }
135
136 *global = Some(Arc::new(GlobalRuntime::new(max_io)?));
137 Ok(TknwStatus::Ok)
138 })
139}
140
141#[unsafe(no_mangle)]
142pub extern "C" fn tknw_global_shutdown() -> TknwStatus {
143 ffi_boundary(|| {
144 let mut global = GLOBAL.lock().expect("global runtime mutex poisoned");
145 let _ = global.take();
146 Ok(TknwStatus::Ok)
147 })
148}
149
150#[unsafe(no_mangle)]
151pub extern "C" fn tknw_global_set_max_io(max_io: usize) -> TknwStatus {
152 ffi_boundary(|| {
153 let global = current_global()?;
154 global.engine.set_max_io(max_io);
155 Ok(TknwStatus::Ok)
156 })
157}
158
159#[unsafe(no_mangle)]
160pub extern "C" fn tknw_download_create(
161 config: *const TknwDownloadConfig,
162 out_download: *mut *mut TknwDownload,
163) -> TknwStatus {
164 ffi_boundary(|| {
165 if config.is_null() {
166 return Err(TakanawaError::NullPointer("config"));
167 }
168 if out_download.is_null() {
169 return Err(TakanawaError::NullPointer("out_download"));
170 }
171
172 let config = unsafe { &*config };
174 validate_abi(
175 "TknwDownloadConfig",
176 config.abi_version,
177 config.struct_size,
178 size_of::<TknwDownloadConfig>(),
179 )?;
180
181 let global = current_global()?;
182 let url = read_c_string(config.url, "url")?;
183 let target_path = read_c_string(config.target_path, "target_path")?;
184 let hash = read_hash_config(config)?;
185 let download_config = DownloadConfig {
186 url,
187 target_path: PathBuf::from(target_path),
188 chunk_size: config.chunk_size,
189 parallelism: config.parallelism,
190 max_parallel_chunks: config.max_parallel_chunks,
191 retry: RetryConfig {
192 max_retries: config.max_retries,
193 backoff_initial: millis(config.backoff_initial_millis),
194 backoff_max: millis(config.backoff_max_millis),
195 },
196 timeout: TimeoutConfig {
197 connect: millis(config.connect_timeout_millis),
198 read: millis(config.read_timeout_millis),
199 total: millis(config.total_timeout_millis),
200 },
201 bytes_per_second_limit: config.bytes_per_second_limit,
202 hash,
203 };
204 let download = Box::new(TknwDownload {
205 inner: DownloadHandle::new(global.engine.clone(), download_config),
206 global,
207 last_error: Mutex::new(None),
208 });
209
210 unsafe {
212 *out_download = Box::into_raw(download);
213 }
214 Ok(TknwStatus::Ok)
215 })
216}
217
218#[unsafe(no_mangle)]
219pub extern "C" fn tknw_download_start(download: *mut TknwDownload) -> TknwStatus {
220 ffi_download_boundary(download, |download| {
221 download.inner.start_on(&download.global.runtime)?;
222 Ok(TknwStatus::Ok)
223 })
224}
225
226#[unsafe(no_mangle)]
227pub extern "C" fn tknw_download_pause(download: *mut TknwDownload) -> TknwStatus {
228 ffi_download_boundary(download, |download| {
229 download.inner.pause()?;
230 Ok(TknwStatus::Ok)
231 })
232}
233
234#[unsafe(no_mangle)]
235pub extern "C" fn tknw_download_cancel(download: *mut TknwDownload) -> TknwStatus {
236 ffi_download_boundary(download, |download| {
237 download.inner.cancel()?;
238 Ok(TknwStatus::Ok)
239 })
240}
241
242#[unsafe(no_mangle)]
243pub extern "C" fn tknw_download_snapshot(
244 download: *const TknwDownload,
245 snapshot: *mut TknwDownloadSnapshot,
246) -> TknwStatus {
247 ffi_boundary(|| {
248 if download.is_null() {
249 return Err(TakanawaError::NullPointer("download"));
250 }
251 if snapshot.is_null() {
252 return Err(TakanawaError::NullPointer("snapshot"));
253 }
254
255 let download = unsafe { &*download };
257 let snapshot_ref = unsafe { &mut *snapshot };
259 validate_abi(
260 "TknwDownloadSnapshot",
261 snapshot_ref.abi_version,
262 snapshot_ref.struct_size,
263 size_of::<TknwDownloadSnapshot>(),
264 )?;
265
266 let current = download.inner.snapshot();
267 snapshot_ref.phase = phase_to_u32(current.phase);
268 snapshot_ref.content_len = current.content_len;
269 snapshot_ref.downloaded_bytes = current.downloaded_bytes;
270 snapshot_ref.chunk_size = current.chunk_size;
271 snapshot_ref.chunk_count = current.chunk_count;
272 snapshot_ref.completed_chunks = current.completed_chunks;
273 snapshot_ref.active_io = current.active_io;
274 Ok(TknwStatus::Ok)
275 })
276}
277
278#[unsafe(no_mangle)]
279pub extern "C" fn tknw_download_copy_bitmap(
280 download: *const TknwDownload,
281 buffer: *mut c_uchar,
282 buffer_len: usize,
283 written: *mut usize,
284) -> TknwStatus {
285 ffi_boundary(|| {
286 if download.is_null() {
287 return Err(TakanawaError::NullPointer("download"));
288 }
289 if written.is_null() {
290 return Err(TakanawaError::NullPointer("written"));
291 }
292 let download = unsafe { &*download };
294 let bitmap = download.inner.bitmap();
295 unsafe {
297 *written = bitmap.len();
298 }
299 if bitmap.len() > buffer_len {
300 return Ok(TknwStatus::BufferTooSmall);
301 }
302 if !bitmap.is_empty() {
303 if buffer.is_null() {
304 return Err(TakanawaError::NullPointer("buffer"));
305 }
306 unsafe {
308 ptr::copy_nonoverlapping(bitmap.as_ptr(), buffer, bitmap.len());
309 }
310 }
311 Ok(TknwStatus::Ok)
312 })
313}
314
315#[unsafe(no_mangle)]
316pub extern "C" fn tknw_download_last_error(
317 download: *const TknwDownload,
318 buffer: *mut c_char,
319 buffer_len: usize,
320 written: *mut usize,
321) -> TknwStatus {
322 ffi_boundary(|| {
323 if download.is_null() {
324 return Err(TakanawaError::NullPointer("download"));
325 }
326 if written.is_null() {
327 return Err(TakanawaError::NullPointer("written"));
328 }
329 let download = unsafe { &*download };
331 let message = download
332 .inner
333 .snapshot()
334 .last_error
335 .or_else(|| {
336 download
337 .last_error
338 .lock()
339 .expect("last error mutex poisoned")
340 .clone()
341 })
342 .unwrap_or_default();
343 let bytes = message.as_bytes();
344 let required = bytes.len() + 1;
345 unsafe {
347 *written = required;
348 }
349 if required > buffer_len {
350 return Ok(TknwStatus::BufferTooSmall);
351 }
352 if buffer.is_null() {
353 return Err(TakanawaError::NullPointer("buffer"));
354 }
355 unsafe {
357 ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), buffer, bytes.len());
358 *buffer.add(bytes.len()) = 0;
359 }
360 Ok(TknwStatus::Ok)
361 })
362}
363
364#[unsafe(no_mangle)]
365pub extern "C" fn tknw_download_release(download: *mut *mut TknwDownload) -> TknwStatus {
366 ffi_boundary(|| {
367 if download.is_null() {
368 return Err(TakanawaError::NullPointer("download"));
369 }
370 let handle = unsafe { *download };
372 if handle.is_null() {
373 return Err(TakanawaError::NullPointer("*download"));
374 }
375 unsafe {
377 drop(Box::from_raw(handle));
378 *download = ptr::null_mut();
379 }
380 Ok(TknwStatus::Ok)
381 })
382}
383
384impl GlobalRuntime {
385 fn new(max_io: usize) -> Result<Self, TakanawaError> {
386 let runtime = Builder::new_multi_thread()
387 .enable_all()
388 .thread_name("takanawa")
389 .build()
390 .map_err(TakanawaError::Io)?;
391 let engine = DownloadEngine::new(max_io)?;
392 Ok(Self { runtime, engine })
393 }
394}
395
396fn current_global() -> Result<Arc<GlobalRuntime>, TakanawaError> {
397 GLOBAL
398 .lock()
399 .expect("global runtime mutex poisoned")
400 .as_ref()
401 .cloned()
402 .ok_or(TakanawaError::RuntimeNotInitialized)
403}
404
405const fn millis(value: u64) -> Duration {
406 Duration::from_millis(value)
407}
408
409fn validate_abi(
410 name: &'static str,
411 abi_version: u32,
412 actual_size: usize,
413 expected_size: usize,
414) -> Result<(), TakanawaError> {
415 if abi_version != TKNW_ABI_VERSION {
416 return Err(TakanawaError::AbiMismatch(format!(
417 "{name} ABI version mismatch: expected {TKNW_ABI_VERSION}, got {abi_version}"
418 )));
419 }
420 if actual_size < expected_size {
421 return Err(TakanawaError::StructSizeMismatch {
422 name,
423 expected: expected_size,
424 actual: actual_size,
425 });
426 }
427 Ok(())
428}
429
430fn read_c_string(ptr: *const c_char, name: &'static str) -> Result<String, TakanawaError> {
431 if ptr.is_null() {
432 return Err(TakanawaError::NullPointer(name));
433 }
434 let value = unsafe { CStr::from_ptr(ptr) };
436 value
437 .to_str()
438 .map(str::to_owned)
439 .map_err(|err| TakanawaError::Utf8(format!("{name}: {err}")))
440}
441
442fn read_hash_config(config: &TknwDownloadConfig) -> Result<HashConfig, TakanawaError> {
443 match config.hash_kind {
444 0 => Ok(HashConfig::None),
445 1 => {
446 if config.expected_sha256.is_null() {
447 return Err(TakanawaError::NullPointer("expected_sha256"));
448 }
449 if config.expected_sha256_len != 32 {
450 return Err(TakanawaError::InvalidConfig(format!(
451 "SHA-256 expected hash length must be 32, got {}",
452 config.expected_sha256_len
453 )));
454 }
455 let mut hash = [0; 32];
456 unsafe {
458 ptr::copy_nonoverlapping(config.expected_sha256, hash.as_mut_ptr(), 32);
459 }
460 Ok(HashConfig::Sha256(hash))
461 }
462 other => Err(TakanawaError::InvalidConfig(format!(
463 "unsupported hash kind {other}"
464 ))),
465 }
466}
467
468fn ffi_boundary<F>(f: F) -> TknwStatus
469where
470 F: FnOnce() -> Result<TknwStatus, TakanawaError>,
471{
472 match catch_unwind(AssertUnwindSafe(f)) {
473 Ok(Ok(status)) => status,
474 Ok(Err(err)) => status_from_error(&err),
475 Err(_) => TknwStatus::Panic,
476 }
477}
478
479fn ffi_download_boundary<F>(download: *mut TknwDownload, f: F) -> TknwStatus
480where
481 F: FnOnce(&mut TknwDownload) -> Result<TknwStatus, TakanawaError>,
482{
483 match catch_unwind(AssertUnwindSafe(|| {
484 if download.is_null() {
485 return Err(TakanawaError::NullPointer("download"));
486 }
487 let download = unsafe { &mut *download };
489 f(download).inspect_err(|err| {
490 *download
491 .last_error
492 .lock()
493 .expect("last error mutex poisoned") = Some(err.to_string());
494 })
495 })) {
496 Ok(Ok(status)) => status,
497 Ok(Err(err)) => status_from_error(&err),
498 Err(_) => TknwStatus::Panic,
499 }
500}
501
502fn status_from_error(err: &TakanawaError) -> TknwStatus {
503 match err {
504 TakanawaError::NullPointer(_) => TknwStatus::NullPointer,
505 TakanawaError::StructSizeMismatch { .. } | TakanawaError::AbiMismatch(_) => {
506 TknwStatus::AbiMismatch
507 }
508 TakanawaError::InvalidConfig(_) | TakanawaError::NotRunning | TakanawaError::Utf8(_) => {
509 TknwStatus::InvalidConfig
510 }
511 TakanawaError::RuntimeNotInitialized => TknwStatus::RuntimeNotInitialized,
512 TakanawaError::TargetExists(_) => TknwStatus::TargetExists,
513 TakanawaError::PartBusy(_) => TknwStatus::PartBusy,
514 TakanawaError::PartSizeMismatch { .. } => TknwStatus::PartSizeMismatch,
515 TakanawaError::PartCorrupt(_) => TknwStatus::PartCorrupt,
516 TakanawaError::RemoteChanged(_) => TknwStatus::RemoteChanged,
517 TakanawaError::HttpProtocol(_) | TakanawaError::RetryableHttpStatus(_) => {
518 TknwStatus::HttpProtocol
519 }
520 TakanawaError::Network(_) => TknwStatus::Network,
521 TakanawaError::Io(_) => TknwStatus::Io,
522 TakanawaError::HashMismatch => TknwStatus::HashMismatch,
523 TakanawaError::Cancelled => TknwStatus::Cancelled,
524 TakanawaError::AlreadyStarted => TknwStatus::AlreadyStarted,
525 TakanawaError::Ffi(_) => TknwStatus::Internal,
526 }
527}
528
529const fn phase_to_u32(phase: DownloadPhase) -> u32 {
530 phase as u32
531}
532
533#[cfg(feature = "jni")]
534mod android_jni {
535 use std::ffi::CString;
536 use std::panic::{AssertUnwindSafe, catch_unwind};
537 use std::ptr;
538
539 use jni::JNIEnv;
540 use jni::errors::Error as JniError;
541 use jni::objects::{JByteArray, JClass, JLongArray, JString};
542 use jni::sys::{jbyte, jint, jlong, jstring};
543
544 use super::{
545 TKNW_ABI_VERSION, TknwDownload, TknwDownloadConfig, TknwDownloadSnapshot, TknwGlobalConfig,
546 TknwHashKind, TknwStatus, tknw_download_cancel, tknw_download_copy_bitmap,
547 tknw_download_create, tknw_download_last_error, tknw_download_pause, tknw_download_release,
548 tknw_download_snapshot, tknw_download_start, tknw_global_init, tknw_global_set_max_io,
549 tknw_global_shutdown,
550 };
551
552 #[unsafe(no_mangle)]
553 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_globalInit<'local>(
554 _env: JNIEnv<'local>,
555 _class: JClass<'local>,
556 max_io: jint,
557 ) -> jint {
558 let Ok(max_io) = usize::try_from(max_io) else {
559 return status_code(TknwStatus::InvalidConfig);
560 };
561 let config = TknwGlobalConfig {
562 abi_version: TKNW_ABI_VERSION,
563 struct_size: size_of::<TknwGlobalConfig>(),
564 max_io,
565 };
566 status_code(tknw_global_init(&raw const config))
567 }
568
569 #[unsafe(no_mangle)]
570 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_globalShutdown<'local>(
571 _env: JNIEnv<'local>,
572 _class: JClass<'local>,
573 ) -> jint {
574 status_code(tknw_global_shutdown())
575 }
576
577 #[unsafe(no_mangle)]
578 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_globalSetMaxIo<'local>(
579 _env: JNIEnv<'local>,
580 _class: JClass<'local>,
581 max_io: jint,
582 ) -> jint {
583 let Ok(max_io) = usize::try_from(max_io) else {
584 return status_code(TknwStatus::InvalidConfig);
585 };
586 status_code(tknw_global_set_max_io(max_io))
587 }
588
589 #[unsafe(no_mangle)]
590 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadCreate<'local>(
591 mut env: JNIEnv<'local>,
592 _class: JClass<'local>,
593 url: JString<'local>,
594 target_path: JString<'local>,
595 chunk_size: jlong,
596 parallelism: jint,
597 max_parallel_chunks: jint,
598 max_retries: jint,
599 backoff_initial_millis: jlong,
600 backoff_max_millis: jlong,
601 connect_timeout_millis: jlong,
602 read_timeout_millis: jlong,
603 total_timeout_millis: jlong,
604 bytes_per_second_limit: jlong,
605 expected_sha256: JByteArray<'local>,
606 out_handle: JLongArray<'local>,
607 ) -> jint {
608 jni_status(|| {
609 let Ok(chunk_size) = u64::try_from(chunk_size) else {
610 return Ok(status_code(TknwStatus::InvalidConfig));
611 };
612 let Ok(parallelism) = usize::try_from(parallelism) else {
613 return Ok(status_code(TknwStatus::InvalidConfig));
614 };
615 let Ok(max_parallel_chunks) = usize::try_from(max_parallel_chunks) else {
616 return Ok(status_code(TknwStatus::InvalidConfig));
617 };
618 let Ok(max_retries) = u32::try_from(max_retries) else {
619 return Ok(status_code(TknwStatus::InvalidConfig));
620 };
621 let Ok(backoff_initial_millis) = u64::try_from(backoff_initial_millis) else {
622 return Ok(status_code(TknwStatus::InvalidConfig));
623 };
624 let Ok(backoff_max_millis) = u64::try_from(backoff_max_millis) else {
625 return Ok(status_code(TknwStatus::InvalidConfig));
626 };
627 let Ok(connect_timeout_millis) = u64::try_from(connect_timeout_millis) else {
628 return Ok(status_code(TknwStatus::InvalidConfig));
629 };
630 let Ok(read_timeout_millis) = u64::try_from(read_timeout_millis) else {
631 return Ok(status_code(TknwStatus::InvalidConfig));
632 };
633 let Ok(total_timeout_millis) = u64::try_from(total_timeout_millis) else {
634 return Ok(status_code(TknwStatus::InvalidConfig));
635 };
636 let Ok(bytes_per_second_limit) = u64::try_from(bytes_per_second_limit) else {
637 return Ok(status_code(TknwStatus::InvalidConfig));
638 };
639
640 let url = match read_java_string(&mut env, &url) {
641 Ok(url) => url,
642 Err(status) => return Ok(status_code(status)),
643 };
644 let target_path = match read_java_string(&mut env, &target_path) {
645 Ok(target_path) => target_path,
646 Err(status) => return Ok(status_code(status)),
647 };
648 let expected_hash = match read_optional_hash(&mut env, &expected_sha256) {
649 Ok(expected_hash) => expected_hash,
650 Err(status) => return Ok(status_code(status)),
651 };
652 let hash_ptr = expected_hash.as_ref().map_or(ptr::null(), Vec::as_ptr);
653 let hash_len = expected_hash.as_ref().map_or(0, Vec::len);
654 let config = TknwDownloadConfig {
655 abi_version: TKNW_ABI_VERSION,
656 struct_size: size_of::<TknwDownloadConfig>(),
657 url: url.as_ptr(),
658 target_path: target_path.as_ptr(),
659 chunk_size,
660 parallelism,
661 max_parallel_chunks,
662 max_retries,
663 backoff_initial_millis,
664 backoff_max_millis,
665 connect_timeout_millis,
666 read_timeout_millis,
667 total_timeout_millis,
668 bytes_per_second_limit,
669 hash_kind: if expected_hash.is_some() {
670 TknwHashKind::Sha256 as u32
671 } else {
672 TknwHashKind::None as u32
673 },
674 expected_sha256: hash_ptr,
675 expected_sha256_len: hash_len,
676 };
677 let mut download = ptr::null_mut();
678 let status = tknw_download_create(&raw const config, &raw mut download);
679 if status != TknwStatus::Ok {
680 return Ok(status_code(status));
681 }
682
683 Ok(write_long_array(
684 &mut env,
685 &out_handle,
686 &[download as jlong],
687 ))
688 })
689 }
690
691 #[unsafe(no_mangle)]
692 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadStart<'local>(
693 _env: JNIEnv<'local>,
694 _class: JClass<'local>,
695 handle: jlong,
696 ) -> jint {
697 status_code(tknw_download_start(download_mut(handle)))
698 }
699
700 #[unsafe(no_mangle)]
701 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadPause<'local>(
702 _env: JNIEnv<'local>,
703 _class: JClass<'local>,
704 handle: jlong,
705 ) -> jint {
706 status_code(tknw_download_pause(download_mut(handle)))
707 }
708
709 #[unsafe(no_mangle)]
710 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadCancel<'local>(
711 _env: JNIEnv<'local>,
712 _class: JClass<'local>,
713 handle: jlong,
714 ) -> jint {
715 status_code(tknw_download_cancel(download_mut(handle)))
716 }
717
718 #[unsafe(no_mangle)]
719 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadSnapshot<'local>(
720 mut env: JNIEnv<'local>,
721 _class: JClass<'local>,
722 handle: jlong,
723 out_snapshot: JLongArray<'local>,
724 ) -> jint {
725 let mut snapshot = TknwDownloadSnapshot {
726 abi_version: TKNW_ABI_VERSION,
727 struct_size: size_of::<TknwDownloadSnapshot>(),
728 phase: 0,
729 content_len: 0,
730 downloaded_bytes: 0,
731 chunk_size: 0,
732 chunk_count: 0,
733 completed_chunks: 0,
734 active_io: 0,
735 };
736 let status = tknw_download_snapshot(download_const(handle), &raw mut snapshot);
737 if status != TknwStatus::Ok {
738 return status_code(status);
739 }
740
741 let values = match snapshot_values(&snapshot) {
742 Ok(values) => values,
743 Err(status) => return status_code(status),
744 };
745 jni_status(|| Ok(write_long_array(&mut env, &out_snapshot, &values)))
746 }
747
748 #[unsafe(no_mangle)]
749 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadBitmapSize<'local>(
750 mut env: JNIEnv<'local>,
751 _class: JClass<'local>,
752 handle: jlong,
753 out_size: JLongArray<'local>,
754 ) -> jint {
755 let mut written = 0;
756 let status =
757 tknw_download_copy_bitmap(download_const(handle), ptr::null_mut(), 0, &raw mut written);
758 if !matches!(status, TknwStatus::Ok | TknwStatus::BufferTooSmall) {
759 return status_code(status);
760 }
761 let Ok(written) = jlong::try_from(written) else {
762 return status_code(TknwStatus::Internal);
763 };
764 jni_status(|| Ok(write_long_array(&mut env, &out_size, &[written])))
765 }
766
767 #[unsafe(no_mangle)]
768 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadCopyBitmap<'local>(
769 env: JNIEnv<'local>,
770 _class: JClass<'local>,
771 handle: jlong,
772 out_bitmap: JByteArray<'local>,
773 ) -> jint {
774 jni_status(|| {
775 if out_bitmap.as_raw().is_null() {
776 return Ok(status_code(TknwStatus::NullPointer));
777 }
778 let Ok(len) = env.get_array_length(&out_bitmap) else {
779 return Ok(status_code(TknwStatus::InvalidConfig));
780 };
781 let Ok(len) = usize::try_from(len) else {
782 return Ok(status_code(TknwStatus::InvalidConfig));
783 };
784 let mut buffer = vec![0; len];
785 let mut written = 0;
786 let status = tknw_download_copy_bitmap(
787 download_const(handle),
788 buffer.as_mut_ptr(),
789 buffer.len(),
790 &raw mut written,
791 );
792 if status != TknwStatus::Ok {
793 return Ok(status_code(status));
794 }
795 let signed = buffer
796 .into_iter()
797 .take(written)
798 .map(|byte| jbyte::from_ne_bytes([byte]))
799 .collect::<Vec<_>>();
800 Ok(match env.set_byte_array_region(&out_bitmap, 0, &signed) {
801 Ok(()) => status_code(TknwStatus::Ok),
802 Err(_) => status_code(TknwStatus::Internal),
803 })
804 })
805 }
806
807 #[unsafe(no_mangle)]
808 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadLastError<'local>(
809 env: JNIEnv<'local>,
810 _class: JClass<'local>,
811 handle: jlong,
812 ) -> jstring {
813 match catch_unwind(AssertUnwindSafe(|| {
814 let message = last_error(download_const(handle));
815 Ok::<jstring, JniError>(
816 env.new_string(message)
817 .map_or_else(|_| ptr::null_mut(), JString::into_raw),
818 )
819 })) {
820 Ok(Ok(value)) => value,
821 Ok(Err(_)) | Err(_) => ptr::null_mut(),
822 }
823 }
824
825 #[unsafe(no_mangle)]
826 pub extern "C" fn Java_ai_yetanother_takanawa_NativeBridge_downloadRelease<'local>(
827 _env: JNIEnv<'local>,
828 _class: JClass<'local>,
829 handle: jlong,
830 ) -> jint {
831 let mut download = download_mut(handle);
832 status_code(tknw_download_release(&raw mut download))
833 }
834
835 fn read_java_string(env: &mut JNIEnv<'_>, value: &JString<'_>) -> Result<CString, TknwStatus> {
836 if value.as_raw().is_null() {
837 return Err(TknwStatus::NullPointer);
838 }
839 let value: String = env
840 .get_string(value)
841 .map_err(|_| TknwStatus::InvalidConfig)?
842 .into();
843 CString::new(value).map_err(|_| TknwStatus::InvalidConfig)
844 }
845
846 fn read_optional_hash(
847 env: &mut JNIEnv<'_>,
848 value: &JByteArray<'_>,
849 ) -> Result<Option<Vec<u8>>, TknwStatus> {
850 if value.as_raw().is_null() {
851 return Ok(None);
852 }
853 let hash = env
854 .convert_byte_array(value)
855 .map_err(|_| TknwStatus::InvalidConfig)?;
856 Ok(Some(hash))
857 }
858
859 fn write_long_array(env: &mut JNIEnv<'_>, array: &JLongArray<'_>, values: &[jlong]) -> jint {
860 if array.as_raw().is_null() {
861 return status_code(TknwStatus::NullPointer);
862 }
863 let Ok(len) = env.get_array_length(array) else {
864 return status_code(TknwStatus::InvalidConfig);
865 };
866 let Ok(len) = usize::try_from(len) else {
867 return status_code(TknwStatus::InvalidConfig);
868 };
869 if len < values.len() {
870 return status_code(TknwStatus::BufferTooSmall);
871 }
872 match env.set_long_array_region(array, 0, values) {
873 Ok(()) => status_code(TknwStatus::Ok),
874 Err(_) => status_code(TknwStatus::Internal),
875 }
876 }
877
878 fn jni_status(action: impl FnOnce() -> Result<jint, JniError>) -> jint {
879 match catch_unwind(AssertUnwindSafe(action)) {
880 Ok(Ok(status)) => status,
881 Ok(Err(_)) | Err(_) => status_code(TknwStatus::Internal),
882 }
883 }
884
885 fn last_error(download: *const TknwDownload) -> String {
886 if download.is_null() {
887 return String::new();
888 }
889 let mut written = 0;
890 let status = tknw_download_last_error(download, ptr::null_mut(), 0, &raw mut written);
891 if !matches!(status, TknwStatus::Ok | TknwStatus::BufferTooSmall) || written == 0 {
892 return String::new();
893 }
894
895 let mut buffer = vec![0; written];
896 let status = tknw_download_last_error(
897 download,
898 buffer.as_mut_ptr().cast(),
899 buffer.len(),
900 &raw mut written,
901 );
902 if status != TknwStatus::Ok {
903 return String::new();
904 }
905 let len = buffer
906 .iter()
907 .position(|byte| *byte == 0)
908 .unwrap_or(buffer.len());
909 String::from_utf8_lossy(&buffer[..len]).into_owned()
910 }
911
912 const fn download_mut(handle: jlong) -> *mut TknwDownload {
913 handle as *mut TknwDownload
914 }
915
916 const fn download_const(handle: jlong) -> *const TknwDownload {
917 handle as *const TknwDownload
918 }
919
920 fn snapshot_values(snapshot: &TknwDownloadSnapshot) -> Result<[jlong; 7], TknwStatus> {
921 Ok([
922 jlong::from(snapshot.phase),
923 jlong::try_from(snapshot.content_len).map_err(|_| TknwStatus::Internal)?,
924 jlong::try_from(snapshot.downloaded_bytes).map_err(|_| TknwStatus::Internal)?,
925 jlong::try_from(snapshot.chunk_size).map_err(|_| TknwStatus::Internal)?,
926 jlong::try_from(snapshot.chunk_count).map_err(|_| TknwStatus::Internal)?,
927 jlong::try_from(snapshot.completed_chunks).map_err(|_| TknwStatus::Internal)?,
928 jlong::try_from(snapshot.active_io).map_err(|_| TknwStatus::Internal)?,
929 ])
930 }
931
932 const fn status_code(status: TknwStatus) -> jint {
933 status as jint
934 }
935}
936
937#[cfg(test)]
938mod tests {
939 use std::ffi::CString;
940 use std::sync::{LazyLock, Mutex};
941
942 use tempfile::TempDir;
943
944 use super::*;
945
946 static TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
947
948 #[test]
949 fn creates_snapshots_and_releases_handle() {
950 let _guard = TEST_LOCK.lock().unwrap();
951 let global_config = TknwGlobalConfig {
952 abi_version: TKNW_ABI_VERSION,
953 struct_size: size_of::<TknwGlobalConfig>(),
954 max_io: 2,
955 };
956 assert_eq!(tknw_global_init(&raw const global_config), TknwStatus::Ok);
957
958 let dir = TempDir::new().unwrap();
959 let url = CString::new("https://example.test/file").unwrap();
960 let target =
961 CString::new(dir.path().join("file.bin").to_string_lossy().as_bytes()).unwrap();
962 let config = TknwDownloadConfig {
963 abi_version: TKNW_ABI_VERSION,
964 struct_size: size_of::<TknwDownloadConfig>(),
965 url: url.as_ptr(),
966 target_path: target.as_ptr(),
967 chunk_size: 0,
968 parallelism: 0,
969 max_parallel_chunks: 0,
970 max_retries: 4,
971 backoff_initial_millis: 100,
972 backoff_max_millis: 3_000,
973 connect_timeout_millis: 30_000,
974 read_timeout_millis: 0,
975 total_timeout_millis: 0,
976 bytes_per_second_limit: 0,
977 hash_kind: TknwHashKind::None as u32,
978 expected_sha256: ptr::null(),
979 expected_sha256_len: 0,
980 };
981 let mut handle = ptr::null_mut();
982 assert_eq!(
983 tknw_download_create(&raw const config, &raw mut handle),
984 TknwStatus::Ok
985 );
986 assert!(!handle.is_null());
987
988 let mut snapshot = TknwDownloadSnapshot {
989 abi_version: TKNW_ABI_VERSION,
990 struct_size: size_of::<TknwDownloadSnapshot>(),
991 phase: 0,
992 content_len: 0,
993 downloaded_bytes: 0,
994 chunk_size: 0,
995 chunk_count: 0,
996 completed_chunks: 0,
997 active_io: 0,
998 };
999 assert_eq!(
1000 tknw_download_snapshot(handle, &raw mut snapshot),
1001 TknwStatus::Ok
1002 );
1003 assert_eq!(snapshot.phase, DownloadPhase::Created as u32);
1004
1005 assert_eq!(tknw_download_release(&raw mut handle), TknwStatus::Ok);
1006 assert!(handle.is_null());
1007 assert_eq!(
1008 tknw_download_release(&raw mut handle),
1009 TknwStatus::NullPointer
1010 );
1011 assert_eq!(tknw_global_shutdown(), TknwStatus::Ok);
1012 }
1013
1014 #[test]
1015 fn rejects_bad_struct_size() {
1016 let _guard = TEST_LOCK.lock().unwrap();
1017 let global_config = TknwGlobalConfig {
1018 abi_version: TKNW_ABI_VERSION,
1019 struct_size: size_of::<TknwGlobalConfig>() - 1,
1020 max_io: 2,
1021 };
1022
1023 assert_eq!(
1024 tknw_global_init(&raw const global_config),
1025 TknwStatus::AbiMismatch
1026 );
1027 }
1028}