Skip to main content

takanawa_ffi/
lib.rs

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            // SAFETY: config was checked for null and is only read for the duration of this call.
117            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        // SAFETY: pointers were checked for null and are only read/written during this call.
173        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        // SAFETY: out_download is valid for writes by the function contract.
211        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        // SAFETY: pointers were checked for null and are only accessed during this call.
256        let download = unsafe { &*download };
257        // SAFETY: snapshot was checked for null and points to caller-owned writable memory.
258        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        // SAFETY: download and written were checked for null and are only accessed during this call.
293        let download = unsafe { &*download };
294        let bitmap = download.inner.bitmap();
295        // SAFETY: written points to caller-owned writable memory.
296        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            // SAFETY: buffer is non-null and buffer_len is at least bitmap.len().
307            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        // SAFETY: download was checked for null and is only read during this call.
330        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        // SAFETY: written points to caller-owned writable memory.
346        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        // SAFETY: buffer is non-null and large enough for message plus null terminator.
356        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        // SAFETY: download was checked for null and points to caller-owned handle storage.
371        let handle = unsafe { *download };
372        if handle.is_null() {
373            return Err(TakanawaError::NullPointer("*download"));
374        }
375        // SAFETY: handle was created by Box::into_raw in tknw_download_create and is released once here.
376        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    // SAFETY: ptr is non-null and the caller must provide a valid null-terminated string.
435    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            // SAFETY: expected_sha256 is non-null and expected_sha256_len was validated as 32.
457            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        // SAFETY: download was checked for null and is borrowed only for this call.
488        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}