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]
fn invalid_ticket_is_rejected() {
assert!(parse_transfer_ticket("not-a-ticket").is_err());

View File

@@ -34,7 +34,8 @@ impl VnidropTicket {
}
fn decode(value: &str) -> Result<Self> {
let encoded = value
let normalized = normalize_ticket_input(value);
let encoded = normalized
.strip_prefix(VNIDROP_TICKET_PREFIX)
.context("not a VniDrop ticket")?;
let bytes = BASE64URL_NOPAD
@@ -51,8 +52,9 @@ pub(crate) struct ParsedTransferTicket {
}
pub(crate) fn parse_transfer_ticket(value: &str) -> Result<ParsedTransferTicket> {
if value.starts_with(VNIDROP_TICKET_PREFIX) {
let ticket = VnidropTicket::decode(value)?;
let normalized = normalize_ticket_input(value);
if normalized.starts_with(VNIDROP_TICKET_PREFIX) {
let ticket = VnidropTicket::decode(&normalized)?;
let blob_ticket = BlobTicket::from_str(&ticket.blob_ticket)
.context("invalid BlobTicket inside VniDrop ticket")?;
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 {
blob_ticket,
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -18,6 +20,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.unit.dp
import com.vnidrop.app.core.CoreRepository
@@ -33,6 +37,7 @@ fun App() {
val repository = remember { CoreRepository() }
val state by repository.state.collectAsState()
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
var appDataDir by remember { mutableStateOf(platform.defaultCoreDataDir) }
var sourcePath by remember { mutableStateOf("") }
var selectedFile by remember { mutableStateOf<PickedShareFile?>(null) }
@@ -129,7 +134,19 @@ fun App() {
}
state.lastShare?.let { share ->
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")
}
}
}
}
}