create-sia-app 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +179 -0
  3. package/package.json +29 -0
  4. package/template/CLAUDE.md +160 -0
  5. package/template/README.md +102 -0
  6. package/template/_gitignore +5 -0
  7. package/template/biome.json +40 -0
  8. package/template/index.html +13 -0
  9. package/template/package.json +30 -0
  10. package/template/rust/README.md +16 -0
  11. package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
  12. package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
  13. package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
  14. package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
  15. package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
  16. package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
  17. package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
  18. package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
  19. package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
  20. package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
  21. package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
  22. package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
  23. package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
  24. package/template/rust/sia-sdk-rs/LICENSE +21 -0
  25. package/template/rust/sia-sdk-rs/README.md +30 -0
  26. package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
  27. package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
  28. package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
  29. package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
  30. package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
  31. package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
  32. package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
  33. package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
  34. package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
  35. package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
  36. package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
  37. package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
  38. package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
  39. package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
  40. package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
  41. package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
  42. package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
  43. package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
  44. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
  45. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
  46. package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
  47. package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
  48. package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
  49. package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
  50. package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
  51. package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
  52. package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
  53. package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
  54. package/template/rust/sia-sdk-rs/knope.toml +54 -0
  55. package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
  56. package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
  57. package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
  58. package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
  59. package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
  60. package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
  61. package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
  62. package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
  63. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
  64. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
  65. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
  66. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
  67. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
  68. package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
  69. package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
  70. package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
  71. package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
  72. package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
  73. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
  74. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
  75. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
  76. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
  77. package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
  78. package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
  79. package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
  80. package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
  81. package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
  82. package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
  83. package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
  84. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
  85. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
  86. package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
  87. package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
  88. package/template/scripts/setup-rust.js +29 -0
  89. package/template/src/App.tsx +13 -0
  90. package/template/src/components/DevNote.tsx +21 -0
  91. package/template/src/components/auth/ApproveScreen.tsx +84 -0
  92. package/template/src/components/auth/AuthFlow.tsx +77 -0
  93. package/template/src/components/auth/ConnectScreen.tsx +214 -0
  94. package/template/src/components/auth/LoadingScreen.tsx +8 -0
  95. package/template/src/components/auth/RecoveryScreen.tsx +182 -0
  96. package/template/src/components/upload/UploadZone.tsx +314 -0
  97. package/template/src/index.css +9 -0
  98. package/template/src/lib/constants.ts +8 -0
  99. package/template/src/lib/format.ts +35 -0
  100. package/template/src/lib/hex.ts +13 -0
  101. package/template/src/lib/sdk.ts +25 -0
  102. package/template/src/lib/wasm-env.ts +5 -0
  103. package/template/src/main.tsx +12 -0
  104. package/template/src/stores/auth.ts +86 -0
  105. package/template/tsconfig.app.json +31 -0
  106. package/template/tsconfig.json +7 -0
  107. package/template/tsconfig.node.json +26 -0
  108. package/template/vite.config.ts +18 -0
  109. package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
  110. package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
  111. package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
  112. package/template/wasm/indexd_wasm/package.json +31 -0
@@ -0,0 +1,377 @@
1
+ use std::sync::{Arc, Mutex};
2
+
3
+ use indexd::app_client::RegisterAppRequest;
4
+ use indexd::{self, Url};
5
+ use sia::seed::{self, Seed};
6
+ use sia::signing::{PrivateKey, Signature};
7
+ use thiserror::Error;
8
+
9
+ use crate::{AppMeta, SDK, spawn, tls};
10
+
11
+ #[derive(Debug, Error, uniffi::Error)]
12
+ #[uniffi(flat_error)]
13
+ pub enum AppKeyError {
14
+ #[error("app keys must be 32 bytes")]
15
+ InvalidLength,
16
+
17
+ #[error("signatures must be 64 bytes")]
18
+ SignatureLength,
19
+ }
20
+
21
+ /// An AppKey is used to sign requests to the indexer.
22
+ ///
23
+ /// AppKeys can be registered with an indexer during
24
+ /// onboarding with a [Builder]. They are derived from
25
+ /// a BIP-32 recovery phrase, which can be generated
26
+ /// using [generate_recovery_phrase].
27
+ ///
28
+ /// It must be stored securely by the application and
29
+ /// never shared publicly. If exposed, a user's data
30
+ /// is compromised.
31
+ ///
32
+ /// Mishandling the app key will lead to data loss
33
+ /// and inability to access stored objects.
34
+
35
+ #[derive(uniffi::Object)]
36
+ pub struct AppKey(PrivateKey);
37
+
38
+ impl AppKey {
39
+ pub(crate) fn private_key(&self) -> &PrivateKey {
40
+ &self.0
41
+ }
42
+ }
43
+
44
+ #[uniffi::export]
45
+ impl AppKey {
46
+ /// Imports an AppKey from the provided byte array.
47
+ ///
48
+ /// # Arguments
49
+ /// * `key` - A 32-byte array representing the app key.
50
+ #[uniffi::constructor]
51
+ pub fn new(key: Vec<u8>) -> Result<Self, AppKeyError> {
52
+ if key.len() != 32 {
53
+ return Err(AppKeyError::InvalidLength);
54
+ }
55
+ let mut seed = [0u8; 32];
56
+ seed.copy_from_slice(&key);
57
+ Ok(AppKey(PrivateKey::from_seed(&seed)))
58
+ }
59
+
60
+ /// Exports the AppKey. The app key can be re-imported later
61
+ /// using [AppKey::new].
62
+ ///
63
+ /// AppKeys should be stored securely by the application in lieu of the
64
+ /// recovery phrase.
65
+ pub fn export(&self) -> Vec<u8> {
66
+ self.0.as_ref()[..32].to_vec()
67
+ }
68
+
69
+ /// Signs a message using the AppKey.
70
+ pub fn sign(&self, message: Vec<u8>) -> Vec<u8> {
71
+ let signature = self.0.sign(&message);
72
+ signature.as_ref().to_vec()
73
+ }
74
+
75
+ /// Returns the public key corresponding to the AppKey.
76
+ ///
77
+ /// This can be safely shared with others.
78
+ pub fn public_key(&self) -> String {
79
+ self.0.public_key().to_string()
80
+ }
81
+
82
+ /// Verifies a signature for a given message using the AppKey.
83
+ pub fn verify_signature(
84
+ &self,
85
+ message: Vec<u8>,
86
+ signature: Vec<u8>,
87
+ ) -> Result<bool, AppKeyError> {
88
+ if signature.len() != 64 {
89
+ return Err(AppKeyError::SignatureLength);
90
+ }
91
+ let mut sig_bytes = [0u8; 64];
92
+ sig_bytes.copy_from_slice(&signature);
93
+ Ok(self
94
+ .0
95
+ .public_key()
96
+ .verify(&message, &Signature::from(sig_bytes)))
97
+ }
98
+ }
99
+
100
+ impl From<PrivateKey> for AppKey {
101
+ fn from(pk: PrivateKey) -> Self {
102
+ AppKey(pk)
103
+ }
104
+ }
105
+
106
+ #[derive(uniffi::Error, Debug, Error)]
107
+ #[uniffi(flat_error)]
108
+ pub enum SeedError {
109
+ #[error(transparent)]
110
+ InvalidMnemonic(#[from] seed::SeedError),
111
+ }
112
+
113
+ /// Generates a new BIP-32 12-word recovery phrase.
114
+ #[uniffi::export]
115
+ pub fn generate_recovery_phrase() -> String {
116
+ let seed: [u8; 16] = rand::random();
117
+ Seed::from_seed(seed).to_string()
118
+ }
119
+
120
+ /// Validates a BIP-32 recovery phrase.
121
+ #[uniffi::export]
122
+ pub fn validate_recovery_phrase(phrase: &str) -> Result<(), SeedError> {
123
+ Seed::new(phrase)?;
124
+ Ok(())
125
+ }
126
+
127
+ enum BuilderState {
128
+ Disconnected(indexd::Builder<indexd::DisconnectedState>),
129
+ RequestingApproval(indexd::Builder<indexd::RequestingApprovalState>),
130
+ Approved(indexd::Builder<indexd::ApprovedState>),
131
+ Finalized,
132
+ }
133
+
134
+ #[derive(uniffi::Object)]
135
+ pub struct Builder {
136
+ state: Arc<Mutex<Option<BuilderState>>>,
137
+ }
138
+
139
+ #[derive(Debug, Error, uniffi::Error)]
140
+ #[uniffi(flat_error)]
141
+ pub enum BuilderError {
142
+ #[error(transparent)]
143
+ Error(#[from] indexd::BuilderError),
144
+
145
+ #[error("invalid state for this operation")]
146
+ InvalidState,
147
+
148
+ #[error("crypto error: {0}")]
149
+ Crypto(String),
150
+
151
+ #[error("join error: {0}")]
152
+ JoinError(#[from] tokio::task::JoinError),
153
+
154
+ #[error("{0}")]
155
+ Custom(String),
156
+ }
157
+
158
+ impl Builder {
159
+ async fn with_state_transition<F, Fut, R>(&self, f: F) -> Result<R, BuilderError>
160
+ where
161
+ R: Send + 'static,
162
+ F: FnOnce(BuilderState) -> Fut + Send + 'static,
163
+ Fut: Future<Output = Result<(BuilderState, R), BuilderError>> + Send + 'static,
164
+ {
165
+ let state = {
166
+ self.state
167
+ .lock()
168
+ .map_err(|_| BuilderError::Custom("mutex poisoned".into()))?
169
+ .take()
170
+ };
171
+ match state {
172
+ Some(state) => {
173
+ let (next, result) = spawn(async move { f(state).await }).await??;
174
+ *self
175
+ .state
176
+ .lock()
177
+ .map_err(|_| BuilderError::Custom("mutex poisoned".into()))? = Some(next);
178
+ Ok(result)
179
+ }
180
+ _ => Err(BuilderError::InvalidState),
181
+ }
182
+ }
183
+
184
+ fn with_state<F, R>(&self, f: F) -> Result<R, BuilderError>
185
+ where
186
+ F: FnOnce(&BuilderState) -> Result<R, BuilderError>,
187
+ {
188
+ let state = self
189
+ .state
190
+ .lock()
191
+ .map_err(|_| BuilderError::Custom("mutex poisoned".into()))?;
192
+ match state.as_ref() {
193
+ Some(state) => f(state),
194
+ None => Err(BuilderError::InvalidState),
195
+ }
196
+ }
197
+ }
198
+
199
+ #[uniffi::export]
200
+ impl Builder {
201
+ /// Creates a new SDK builder with the provided indexer URL.
202
+ ///
203
+ /// After creating the builder, call [Builder::connected] to attempt
204
+ /// to connect using an existing app key, or [Builder::request_connection]
205
+ /// to request a new connection.
206
+ #[uniffi::constructor]
207
+ pub fn new(indexer_url: String) -> Result<Self, BuilderError> {
208
+ let builder = indexd::Builder::new(indexer_url)?;
209
+ Ok(Builder {
210
+ state: Arc::new(Mutex::new(Some(BuilderState::Disconnected(builder)))),
211
+ })
212
+ }
213
+
214
+ /// Attempts to connect using the provided app key and TLS configuration.
215
+ /// If the app key is valid, returns Some([SDK]), otherwise returns None.
216
+ ///
217
+ /// If you receive None, call [Builder::request_connection] to request a new connection.
218
+ ///
219
+ /// # Arguments
220
+ /// * `app_key` - The application key used for authentication.
221
+ pub async fn connected(&self, app_key: Arc<AppKey>) -> Result<Option<Arc<SDK>>, BuilderError> {
222
+ self.with_state_transition(|state| async move {
223
+ match state {
224
+ BuilderState::Disconnected(builder) => {
225
+ // install crypto provider
226
+ if rustls::crypto::CryptoProvider::get_default().is_none() {
227
+ rustls::crypto::ring::default_provider()
228
+ .install_default()
229
+ .map_err(|e| BuilderError::Crypto(format!("{:?}", e)))?;
230
+ }
231
+ let rustls_config = tls::tls_config();
232
+
233
+ match builder.connected(&app_key.0, rustls_config).await? {
234
+ Some(sdk) => {
235
+ Ok((BuilderState::Finalized, Some(Arc::new(SDK { inner: sdk }))))
236
+ }
237
+ None => Ok((BuilderState::Disconnected(builder), None)),
238
+ }
239
+ }
240
+ _ => Err(BuilderError::InvalidState),
241
+ }
242
+ })
243
+ .await
244
+ }
245
+
246
+ /// Requests a new connection for the application.
247
+ ///
248
+ /// # Arguments
249
+ /// * `app` - Details of the application requesting connection.
250
+ pub async fn request_connection(&self, meta: AppMeta) -> Result<Self, BuilderError> {
251
+ self.with_state_transition(|state| async move {
252
+ if meta.id.len() != 32 {
253
+ return Err(BuilderError::Custom("app ID must be 32 bytes".to_string()));
254
+ }
255
+ let mut app_id = [0u8; 32];
256
+ app_id.copy_from_slice(&meta.id);
257
+ match state {
258
+ BuilderState::Disconnected(builder) => {
259
+ let builder = builder
260
+ .request_connection(&RegisterAppRequest {
261
+ app_id: app_id.into(),
262
+ name: meta.name,
263
+ description: meta.description,
264
+ service_url: Url::parse(&meta.service_url).map_err(|e| {
265
+ BuilderError::Custom(format!("invalid service url: {e}"))
266
+ })?,
267
+ logo_url: meta
268
+ .logo_url
269
+ .map(|s| {
270
+ Url::parse(&s).map_err(|e| {
271
+ BuilderError::Custom(format!("invalid logo url: {e}"))
272
+ })
273
+ })
274
+ .transpose()?,
275
+ callback_url: meta
276
+ .callback_url
277
+ .map(|s| {
278
+ Url::parse(&s).map_err(|e| {
279
+ BuilderError::Custom(format!("invalid callback url: {e}"))
280
+ })
281
+ })
282
+ .transpose()?,
283
+ })
284
+ .await?;
285
+ Ok((BuilderState::RequestingApproval(builder), ()))
286
+ }
287
+ _ => Err(BuilderError::InvalidState),
288
+ }
289
+ })
290
+ .await?;
291
+
292
+ Ok(Builder {
293
+ state: self.state.clone(),
294
+ })
295
+ }
296
+
297
+ /// Retrieves the response URL for the connection request.
298
+ /// This URL can be used to approve the connection request.
299
+ /// It should be displayed to the user.
300
+ pub fn response_url(&self) -> Result<String, BuilderError> {
301
+ self.with_state(|state| match state {
302
+ BuilderState::RequestingApproval(builder) => Ok(builder.response_url().to_owned()),
303
+ _ => Err(BuilderError::InvalidState),
304
+ })
305
+ }
306
+
307
+ /// Waits for the connection request to be approved.
308
+ /// Once approved, the app can be registered and used to create an
309
+ /// SDK instance.
310
+ pub async fn wait_for_approval(&self) -> Result<Self, BuilderError> {
311
+ self.with_state_transition(|state| async move {
312
+ match state {
313
+ BuilderState::RequestingApproval(builder) => {
314
+ let builder = builder.wait_for_approval().await?;
315
+ // transition to approved state
316
+ Ok((BuilderState::Approved(builder), ()))
317
+ }
318
+ _ => Err(BuilderError::InvalidState),
319
+ }
320
+ })
321
+ .await?;
322
+ Ok(Builder {
323
+ state: self.state.clone(),
324
+ })
325
+ }
326
+
327
+ /// Registers the application with the indexer using the provided mnemonic.
328
+ /// Once registered, returns an [SDK] instance that can be used to interact
329
+ /// with the indexer.
330
+ ///
331
+ /// # Arguments
332
+ /// * `mnemonic` - The user's mnemonic phrase used to derive the application key.
333
+ pub async fn register(&self, mnemonic: String) -> Result<Arc<SDK>, BuilderError> {
334
+ self.with_state_transition(|state| async move {
335
+ match state {
336
+ BuilderState::Approved(builder) => {
337
+ // install crypto provider
338
+ if rustls::crypto::CryptoProvider::get_default().is_none() {
339
+ rustls::crypto::ring::default_provider()
340
+ .install_default()
341
+ .map_err(|e| BuilderError::Crypto(format!("{:?}", e)))?;
342
+ }
343
+ let rustls_config = tls::tls_config();
344
+ let sdk = builder.register(&mnemonic, rustls_config).await?;
345
+ Ok((BuilderState::Finalized, Arc::new(SDK { inner: sdk })))
346
+ }
347
+ _ => Err(BuilderError::InvalidState),
348
+ }
349
+ })
350
+ .await
351
+ }
352
+ }
353
+
354
+ #[cfg(test)]
355
+ mod tests {
356
+ use super::*;
357
+
358
+ #[test]
359
+ fn test_generate_recovery_phrase() {
360
+ let phrase = generate_recovery_phrase();
361
+ assert!(validate_recovery_phrase(&phrase).is_ok());
362
+ }
363
+
364
+ #[test]
365
+ fn test_validate_recovery_phrase_invalid() {
366
+ let invalid_phrase = "invalid recovery phrase";
367
+ assert!(validate_recovery_phrase(invalid_phrase).is_err());
368
+ }
369
+
370
+ #[test]
371
+ fn test_app_key_export() {
372
+ let seed: [u8; 32] = rand::random();
373
+ let app_key = AppKey::new(seed.to_vec()).unwrap();
374
+ let exported = app_key.export();
375
+ assert_eq!(exported, seed.to_vec());
376
+ }
377
+ }
@@ -0,0 +1,155 @@
1
+ use std::io;
2
+ use std::pin::Pin;
3
+ use std::sync::Arc;
4
+ use std::task::{Context, Poll, ready};
5
+
6
+ use bytes::Bytes;
7
+ use thiserror::Error;
8
+ use tokio::io::{AsyncRead, AsyncWrite};
9
+ use tokio::sync::mpsc;
10
+ use tokio_stream::wrappers::ReceiverStream;
11
+ use tokio_util::io::StreamReader;
12
+ use tokio_util::sync::PollSender;
13
+ use tokio_util::task::AbortOnDropHandle;
14
+
15
+ use crate::spawn;
16
+
17
+ #[derive(Debug, Error, uniffi::Error)]
18
+ #[uniffi(flat_error)]
19
+ pub enum IOError {
20
+ #[error("i/o error: {0}")]
21
+ Io(String),
22
+
23
+ #[error("reader closed")]
24
+ Closed,
25
+
26
+ #[error("cancelled")]
27
+ Cancelled,
28
+ }
29
+
30
+ impl From<IOError> for std::io::Error {
31
+ fn from(e: IOError) -> Self {
32
+ match e {
33
+ IOError::Closed => std::io::Error::new(std::io::ErrorKind::UnexpectedEof, e),
34
+ IOError::Cancelled => std::io::Error::new(std::io::ErrorKind::Interrupted, e),
35
+ IOError::Io(msg) => std::io::Error::other(msg),
36
+ }
37
+ }
38
+ }
39
+
40
+ /// A foreign reader that can be used to transfer data across FFI boundaries.
41
+ ///
42
+ /// Implementations should send an empty chunk to signal completion. It is recommended
43
+ /// that implementations chunk data into reasonably sized pieces (e.g. 64KiB) to avoid
44
+ /// excessive memory usage.
45
+ ///
46
+ /// If an error is returned by `read`, the reader will be closed and no
47
+ /// further calls will be made.
48
+ #[uniffi::export(with_foreign)]
49
+ #[async_trait::async_trait]
50
+ pub trait Reader: Send + Sync {
51
+ async fn read(&self) -> Result<Vec<u8>, IOError>;
52
+ }
53
+
54
+ pub(crate) fn adapt_ffi_reader(reader: Arc<dyn Reader>) -> impl AsyncRead + Unpin {
55
+ let (tx, rx) = mpsc::channel(1);
56
+
57
+ // Spawn a task to pump data from foreign -> channel
58
+ tokio::spawn(async move {
59
+ loop {
60
+ match reader.read().await {
61
+ Ok(data) if data.is_empty() => break,
62
+ Ok(data) => {
63
+ if tx.send(Ok(Bytes::from(data))).await.is_err() {
64
+ break;
65
+ }
66
+ }
67
+ Err(e) => {
68
+ let _ = tx.send(Err(e)).await;
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ });
74
+
75
+ StreamReader::new(ReceiverStream::new(rx))
76
+ }
77
+
78
+ /// A foreign writer that can be used to transfer data across FFI boundaries.
79
+ /// The data may be sent in multiple chunks. The implementation should handle
80
+ /// buffering and writing the data as it is received.
81
+ #[uniffi::export(with_foreign)]
82
+ #[async_trait::async_trait]
83
+ pub trait Writer: Send + Sync {
84
+ async fn write(&self, data: Vec<u8>) -> Result<(), IOError>;
85
+ }
86
+
87
+ pub struct FFIWriter {
88
+ sender: PollSender<Bytes>,
89
+ join_handle: Option<AbortOnDropHandle<()>>,
90
+ }
91
+
92
+ impl AsyncWrite for FFIWriter {
93
+ fn poll_write(
94
+ self: Pin<&mut Self>,
95
+ cx: &mut Context<'_>,
96
+ buf: &[u8],
97
+ ) -> Poll<std::io::Result<usize>> {
98
+ let this = self.get_mut();
99
+
100
+ if ready!(this.sender.poll_reserve(cx)).is_err() {
101
+ return Poll::Ready(Err(io::Error::new(
102
+ io::ErrorKind::BrokenPipe,
103
+ "channel closed",
104
+ )));
105
+ }
106
+
107
+ let n = buf.len();
108
+ let item = Bytes::copy_from_slice(buf);
109
+
110
+ if this.sender.send_item(item).is_err() {
111
+ return Poll::Ready(Err(io::Error::new(
112
+ io::ErrorKind::BrokenPipe,
113
+ "channel closed",
114
+ )));
115
+ }
116
+
117
+ Poll::Ready(Ok(n))
118
+ }
119
+
120
+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
121
+ Poll::Ready(Ok(()))
122
+ }
123
+
124
+ fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
125
+ self.sender.close();
126
+
127
+ if let Some(join_handle) = self.join_handle.as_mut() {
128
+ match ready!(Pin::new(join_handle).poll(cx)) {
129
+ Ok(()) => {
130
+ self.join_handle = None;
131
+ }
132
+ Err(e) => {
133
+ self.join_handle = None;
134
+ return Poll::Ready(Err(io::Error::other(format!("join error: {}", e))));
135
+ }
136
+ }
137
+ }
138
+ Poll::Ready(Ok(()))
139
+ }
140
+ }
141
+
142
+ pub(crate) fn adapt_ffi_writer(writer: Arc<dyn Writer>) -> FFIWriter {
143
+ let (tx, mut rx) = mpsc::channel::<Bytes>(8);
144
+ let join_handle = spawn(async move {
145
+ while let Some(buf) = rx.recv().await {
146
+ if writer.write(buf.to_vec()).await.is_err() {
147
+ return;
148
+ }
149
+ }
150
+ });
151
+ FFIWriter {
152
+ sender: PollSender::new(tx),
153
+ join_handle: Some(join_handle),
154
+ }
155
+ }