Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable {
symbol: symbol,
onSendCash: onSendCash,
model: barModel,
focusOnAppear: focusOnAppear
focusOnAppear: focusOnAppear,
isTipDm: isTipDm
)
.environment(conversationController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ struct ConversationScreen: View {
onSendCash: sendCash,
conversationController: conversationController,
barModel: barModel,
focusOnAppear: openKeyboard
focusOnAppear: openKeyboard,
isTipDm: tipCounterpart != nil
)
.ignoresSafeArea(.keyboard)
Expand Down
17 changes: 15 additions & 2 deletions Flipcash/Core/Screens/Main/Bill/TipCode.Payload+Encoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@ nonisolated extension TipCode.Payload {

private static let userIDRange = 1..<17

/// The trailing reserved bytes, filled with a non-zero sentinel on encode.
private static let reservedRange = 17..<length

/// Incrementing digits written into `reservedRange` so the frame always ends
/// non-zero — `KikCodes.decode` strips trailing zero bytes, so a zero-filled
/// reserved region (or a user id ending in zeros) comes back short.
private static let reservedFill: [UInt8] = [1, 2, 3]

init(data: Data) throws {
guard !data.isEmpty, data.count <= Self.length else {
throw Error.invalidDataSize
}

// `KikCodes.decode` drops trailing zero bytes, so a user id ending in
// zeros comes back short. Restore the fixed frame before reading it.
// `KikCodes.decode` drops trailing zero bytes, so a frame that ends in
// zeros comes back short. Restore the fixed frame before reading it —
// still required for legacy frames encoded before the reserved fill.
var bytes = Data(data)
bytes.append(Data(count: Self.length - bytes.count))

Expand All @@ -39,6 +48,10 @@ nonisolated extension TipCode.Payload {
var data = Data(count: Self.length)
data[0] = Self.kind
data.replaceSubrange(Self.userIDRange, with: userID.data)
// Fill the reserved trailing region with a non-zero sentinel so the
// frame survives the scannable code at full length regardless of the
// user id's trailing bytes. Decoding ignores this region.
data.replaceSubrange(Self.reservedRange, with: Self.reservedFill)
return data
}

Expand Down
18 changes: 14 additions & 4 deletions FlipcashTests/TipCodeEncodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ struct TipCodeEncodingTests {
#expect(try TipCode.Payload(data: scanned) == payload)
}

@Test("A zero-stripped frame decodes to the same user id")
/// The encoder now fills the reserved region with a non-zero sentinel, so a
/// freshly-encoded frame never ends in zeros. This still has to hold for a
/// legacy frame (zero reserved region) whose trailing zeros the scannable
/// code strips — reconstruct one explicitly rather than from `encode()`.
@Test("A zero-stripped legacy frame decodes to the same user id")
func decodesAZeroStrippedFrame() throws {
let userID = UUID(uuidString: "3f2504e0-4f89-41d3-9a0c-000000000000")!
let payload = TipCode.Payload(userID: userID)

// Zero the reserved region back out to mimic a pre-fill encoder, then
// drop the trailing zeros the way `KikCodes.decode` would.
var truncated = payload.encode()
truncated.replaceSubrange(17..<TipCode.Payload.length, with: [0, 0, 0])
while truncated.last == 0 { truncated.removeLast() }

#expect(truncated.count < TipCode.Payload.length)
Expand All @@ -74,12 +81,15 @@ struct TipCodeEncodingTests {
#expect(encoded.count == CashCode.Payload.length)
}

@Test("The trailing bytes are reserved and zeroed", arguments: TipCodeEncodingTests.userIDs)
func reservedBytesAreZero(userID: UUID) {
/// The reserved trailing bytes are filled with a non-zero sentinel (1, 2, 3)
/// rather than left zero, so the frame always ends non-zero and survives the
/// scannable code — `KikCodes.decode` strips trailing zeros.
@Test("The trailing bytes carry the reserved non-zero fill", arguments: TipCodeEncodingTests.userIDs)
func reservedBytesCarryFill(userID: UUID) {
let encoded = TipCode.Payload(userID: userID).encode()

#expect(encoded[0] == TipCode.Payload.kind)
#expect(encoded[17...].allSatisfy { $0 == 0 })
#expect(Data(encoded[17...]) == Data([1, 2, 3]))
}

// MARK: - Rejection -
Expand Down