Improve ticket copy and parsing

This commit is contained in:
2026-07-03 01:03:39 +02:00
parent 1c525288a5
commit 3e1a5268f4
3 changed files with 48 additions and 5 deletions

View File

@@ -52,6 +52,26 @@ mod tests {
); );
} }
#[test]
fn metadata_ticket_round_trip_tolerates_wrapped_whitespace() {
let secret = SecretKey::generate();
let addr = iroh::EndpointAddr::new(secret.public());
let blob_ticket = BlobTicket::new(addr, Hash::new([9; 32]), BlobFormat::HashSeq);
let metadata = TransferMetadata::new(7, "Wrapped", None, blob_ticket.hash(), 1, 10);
let encoded = VnidropTicket::new(blob_ticket.clone(), metadata)
.encode()
.unwrap();
let wrapped = encoded
.as_bytes()
.chunks(8)
.map(|chunk| std::str::from_utf8(chunk).unwrap())
.collect::<Vec<_>>()
.join("\n ");
let parsed = parse_transfer_ticket(&wrapped).unwrap();
assert_eq!(parsed.blob_ticket.hash(), blob_ticket.hash());
}
#[test] #[test]
fn invalid_ticket_is_rejected() { fn invalid_ticket_is_rejected() {
assert!(parse_transfer_ticket("not-a-ticket").is_err()); assert!(parse_transfer_ticket("not-a-ticket").is_err());

View File

@@ -34,7 +34,8 @@ impl VnidropTicket {
} }
fn decode(value: &str) -> Result<Self> { fn decode(value: &str) -> Result<Self> {
let encoded = value let normalized = normalize_ticket_input(value);
let encoded = normalized
.strip_prefix(VNIDROP_TICKET_PREFIX) .strip_prefix(VNIDROP_TICKET_PREFIX)
.context("not a VniDrop ticket")?; .context("not a VniDrop ticket")?;
let bytes = BASE64URL_NOPAD let bytes = BASE64URL_NOPAD
@@ -51,8 +52,9 @@ pub(crate) struct ParsedTransferTicket {
} }
pub(crate) fn parse_transfer_ticket(value: &str) -> Result<ParsedTransferTicket> { pub(crate) fn parse_transfer_ticket(value: &str) -> Result<ParsedTransferTicket> {
if value.starts_with(VNIDROP_TICKET_PREFIX) { let normalized = normalize_ticket_input(value);
let ticket = VnidropTicket::decode(value)?; if normalized.starts_with(VNIDROP_TICKET_PREFIX) {
let ticket = VnidropTicket::decode(&normalized)?;
let blob_ticket = BlobTicket::from_str(&ticket.blob_ticket) let blob_ticket = BlobTicket::from_str(&ticket.blob_ticket)
.context("invalid BlobTicket inside VniDrop ticket")?; .context("invalid BlobTicket inside VniDrop ticket")?;
return Ok(ParsedTransferTicket { return Ok(ParsedTransferTicket {
@@ -61,9 +63,13 @@ pub(crate) fn parse_transfer_ticket(value: &str) -> Result<ParsedTransferTicket>
}); });
} }
let blob_ticket = BlobTicket::from_str(value).context("invalid BlobTicket")?; let blob_ticket = BlobTicket::from_str(&normalized).context("invalid BlobTicket")?;
Ok(ParsedTransferTicket { Ok(ParsedTransferTicket {
blob_ticket, blob_ticket,
metadata: None, metadata: None,
}) })
} }
fn normalize_ticket_input(value: &str) -> String {
value.chars().filter(|char| !char.is_whitespace()).collect()
}

View File

@@ -2,12 +2,14 @@ package com.vnidrop.app
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -18,6 +20,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vnidrop.app.core.CoreRepository import com.vnidrop.app.core.CoreRepository
@@ -33,6 +37,7 @@ fun App() {
val repository = remember { CoreRepository() } val repository = remember { CoreRepository() }
val state by repository.state.collectAsState() val state by repository.state.collectAsState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) } var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) }
var sourcePath by remember { mutableStateOf("") } var sourcePath by remember { mutableStateOf("") }
var selectedFile by remember { mutableStateOf<PickedShareFile?>(null) } var selectedFile by remember { mutableStateOf<PickedShareFile?>(null) }
@@ -129,7 +134,19 @@ fun App() {
} }
state.lastShare?.let { share -> state.lastShare?.let { share ->
Text("Ticket") Text("Ticket")
Text(share.ticket, style = MaterialTheme.typography.bodySmall) SelectionContainer {
Text(share.ticket, style = MaterialTheme.typography.bodySmall)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = {
clipboardManager.setText(AnnotatedString(share.ticket))
}) {
Text("Copy ticket")
}
Button(onClick = { ticket = share.ticket }) {
Text("Use locally")
}
}
} }
} }
} }