Skip to content

Commit 27cfbc7

Browse files
authored
Merge pull request #292 from Zondax/fix/irc21
Improve time unit format
2 parents 14ad432 + ea29508 commit 27cfbc7

File tree

18 files changed

+281
-11
lines changed

18 files changed

+281
-11
lines changed

app/Makefile.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ APPVERSION_M=4
33
# This is the minor version of this release
44
APPVERSION_N=0
55
# This is the patch version of this release
6-
APPVERSION_P=5
6+
APPVERSION_P=6

app/rust/src/parser/certificate/cert.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ mod test_certificate {
428428
// Expected field pairs from the encoded data
429429
let expected_fields = [
430430
("User", "Hello, world!"),
431-
("created_at", "1752218864 s"),
432-
("active_for", "600 s"),
431+
("created_at", "2025-07-11 15:26:31"),
432+
("active_for", "10 minutes"),
433433
("amount", "2 ICP"),
434434
];
435435

app/rust/src/parser/consent_message/msg.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,16 @@ impl DisplayableItem for ConsentMessage<'_> {
397397
let (rem, _) = parse_text(rem).map_err(|_| ViewError::NoData)?;
398398
rem
399399
}
400-
2 | 3 => {
400+
2 => {
401+
// TimestampSeconds
402+
if rem.len() < 8 {
403+
return Err(ViewError::NoData);
404+
}
405+
let (rem, _) = read_u64_le(rem).map_err(|_| ViewError::NoData)?;
406+
rem
407+
}
408+
3 => {
409+
// DurationSeconds
401410
if rem.len() < 8 {
402411
return Err(ViewError::NoData);
403412
}
@@ -427,12 +436,36 @@ impl DisplayableItem for ConsentMessage<'_> {
427436
let (_, text) = parse_text(rem).map_err(|_| ViewError::NoData)?;
428437
handle_ui_message(text.as_bytes(), message, page)
429438
}
430-
2 | 3 => {
431-
// TimestampSeconds or DurationSeconds
439+
2 => {
440+
// TimestampSeconds
441+
if rem.len() < 8 {
442+
return Err(ViewError::NoData);
443+
}
444+
let (_, timestamp) = read_u64_le(rem).map_err(|_| ViewError::NoData)?;
445+
446+
// Format directly into message buffer
447+
let m_len = message.len() - 1;
448+
if m_len < 1 {
449+
return Err(ViewError::NoData);
450+
}
451+
452+
// Use a portion of the message buffer for formatting
453+
let format_len =
454+
crate::utils::format_timestamp(timestamp, message)
455+
.map_err(|_| ViewError::NoData)?;
456+
457+
// Null terminate
458+
message[format_len] = 0;
459+
460+
// Return number of pages (always 1 for these values)
461+
Ok(1)
462+
}
463+
3 => {
464+
// DurationSeconds
432465
if rem.len() < 8 {
433466
return Err(ViewError::NoData);
434467
}
435-
let (_, amount) = read_u64_le(rem).map_err(|_| ViewError::NoData)?;
468+
let (_, duration) = read_u64_le(rem).map_err(|_| ViewError::NoData)?;
436469

437470
// Format directly into message buffer
438471
let m_len = message.len() - 1;
@@ -442,7 +475,7 @@ impl DisplayableItem for ConsentMessage<'_> {
442475

443476
// Use a portion of the message buffer for formatting
444477
let format_len =
445-
crate::utils::format_u64_with_suffix(amount, b"s", message)
478+
crate::utils::format_duration(duration, message)
446479
.map_err(|_| ViewError::NoData)?;
447480

448481
// Null terminate
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
source: src/parser/consent_message/msg_response.rs
3-
assertion_line: 224
3+
assertion_line: 229
44
expression: reduced
55
input_file: src/parser/consent_message/testvectors/ui_data.json
66
snapshot_kind: text
77
---
88
[
99
"User": "Hello, world!",
10-
"created_at": "1752218864 s",
11-
"active_for": "600 s",
10+
"created_at": "2025-07-11 07:27:44",
11+
"active_for": "10 minutes",
1212
"amount": "2 ICP",
1313
]

app/rust/src/utils.rs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,243 @@ pub fn format_u64_with_suffix(
233233
Ok(total_len)
234234
}
235235

236+
// Format duration in seconds to human-readable format with all units
237+
#[inline(never)]
238+
pub fn format_duration(seconds: u64, out: &mut [u8]) -> Result<usize, ParserError> {
239+
const MINUTE: u64 = 60;
240+
const HOUR: u64 = 60 * MINUTE;
241+
const DAY: u64 = 24 * HOUR;
242+
const YEAR: u64 = 365 * DAY;
243+
244+
let mut remaining = seconds;
245+
let mut write_pos = 0;
246+
let mut first = true;
247+
248+
// Helper function to add a unit to the output
249+
fn add_unit(
250+
value: u64,
251+
singular: &[u8],
252+
plural: &[u8],
253+
out: &mut [u8],
254+
write_pos: &mut usize,
255+
first: &mut bool,
256+
) -> Result<(), ParserError> {
257+
if value == 0 {
258+
return Ok(());
259+
}
260+
261+
// Add space if not first
262+
if !*first && *write_pos < out.len() {
263+
out[*write_pos] = b' ';
264+
*write_pos += 1;
265+
}
266+
267+
// Write the number
268+
let len = u64_to_str(value, &mut out[*write_pos..])?;
269+
*write_pos += len;
270+
271+
// Add space before unit
272+
if *write_pos < out.len() {
273+
out[*write_pos] = b' ';
274+
*write_pos += 1;
275+
}
276+
277+
// Add the unit (singular or plural)
278+
let unit_text = if value == 1 { singular } else { plural };
279+
let unit_len = unit_text.len();
280+
281+
if *write_pos + unit_len > out.len() {
282+
return Err(ParserError::UnexpectedBufferEnd);
283+
}
284+
285+
out[*write_pos..*write_pos + unit_len].copy_from_slice(unit_text);
286+
*write_pos += unit_len;
287+
288+
*first = false;
289+
Ok(())
290+
}
291+
292+
// Years
293+
if remaining >= YEAR {
294+
let years = remaining / YEAR;
295+
remaining %= YEAR;
296+
add_unit(years, b"year", b"years", out, &mut write_pos, &mut first)?;
297+
}
298+
299+
// Days
300+
if remaining >= DAY {
301+
let days = remaining / DAY;
302+
remaining %= DAY;
303+
add_unit(days, b"day", b"days", out, &mut write_pos, &mut first)?;
304+
}
305+
306+
// Hours
307+
if remaining >= HOUR {
308+
let hours = remaining / HOUR;
309+
remaining %= HOUR;
310+
add_unit(hours, b"hour", b"hours", out, &mut write_pos, &mut first)?;
311+
}
312+
313+
// Minutes
314+
if remaining >= MINUTE {
315+
let minutes = remaining / MINUTE;
316+
remaining %= MINUTE;
317+
add_unit(minutes, b"minute", b"minutes", out, &mut write_pos, &mut first)?;
318+
}
319+
320+
// Seconds (always show if there are any remaining, or if nothing else was shown)
321+
if remaining > 0 || first {
322+
add_unit(remaining, b"second", b"seconds", out, &mut write_pos, &mut first)?;
323+
}
324+
325+
Ok(write_pos)
326+
}
327+
328+
// Format a Unix timestamp (seconds since epoch) into a human-readable date and time
329+
#[inline(never)]
330+
pub fn format_timestamp(timestamp: u64, out: &mut [u8]) -> Result<usize, ParserError> {
331+
// Days per month (non-leap year)
332+
const DAYS_PER_MONTH: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
333+
const SECONDS_PER_DAY: u64 = 86400;
334+
const SECONDS_PER_HOUR: u64 = 3600;
335+
const SECONDS_PER_MINUTE: u64 = 60;
336+
const DAYS_PER_YEAR: u64 = 365;
337+
const DAYS_PER_LEAP_CYCLE: u64 = 365 * 4 + 1; // 4 years including one leap year
338+
339+
// Calculate total days since epoch and remaining seconds
340+
let total_days = timestamp / SECONDS_PER_DAY;
341+
let remaining_seconds = timestamp % SECONDS_PER_DAY;
342+
343+
// Calculate time components
344+
let hours = remaining_seconds / SECONDS_PER_HOUR;
345+
let minutes = (remaining_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
346+
let seconds = remaining_seconds % SECONDS_PER_MINUTE;
347+
348+
// Start from 1970
349+
let mut year = 1970u64;
350+
let mut days = total_days;
351+
352+
// Calculate years using 4-year cycles (more efficient)
353+
let leap_cycles = days / DAYS_PER_LEAP_CYCLE;
354+
year += leap_cycles * 4;
355+
days %= DAYS_PER_LEAP_CYCLE;
356+
357+
// Handle remaining years
358+
while days >= DAYS_PER_YEAR {
359+
if is_leap_year(year) && days >= 366 {
360+
days -= 366;
361+
year += 1;
362+
} else if !is_leap_year(year) && days >= 365 {
363+
days -= 365;
364+
year += 1;
365+
} else {
366+
break;
367+
}
368+
}
369+
370+
// Calculate month and day
371+
let mut month = 1u8;
372+
let mut day = days as u8 + 1; // days are 1-indexed
373+
374+
for i in 0..12 {
375+
let mut days_in_month = DAYS_PER_MONTH[i];
376+
if i == 1 && is_leap_year(year) {
377+
days_in_month = 29;
378+
}
379+
380+
if day <= days_in_month {
381+
month = (i + 1) as u8;
382+
break;
383+
}
384+
day -= days_in_month;
385+
}
386+
387+
// Format as YYYY-MM-DD HH:MM:SS
388+
let mut write_pos = 0;
389+
390+
// Year
391+
let len = u64_to_str(year, &mut out[write_pos..])?;
392+
write_pos += len;
393+
394+
// Dash
395+
if write_pos < out.len() {
396+
out[write_pos] = b'-';
397+
write_pos += 1;
398+
}
399+
400+
// Month (with leading zero if needed)
401+
if month < 10 && write_pos < out.len() {
402+
out[write_pos] = b'0';
403+
write_pos += 1;
404+
}
405+
let len = u64_to_str(month as u64, &mut out[write_pos..])?;
406+
write_pos += len;
407+
408+
// Dash
409+
if write_pos < out.len() {
410+
out[write_pos] = b'-';
411+
write_pos += 1;
412+
}
413+
414+
// Day (with leading zero if needed)
415+
if day < 10 && write_pos < out.len() {
416+
out[write_pos] = b'0';
417+
write_pos += 1;
418+
}
419+
let len = u64_to_str(day as u64, &mut out[write_pos..])?;
420+
write_pos += len;
421+
422+
// Space
423+
if write_pos < out.len() {
424+
out[write_pos] = b' ';
425+
write_pos += 1;
426+
}
427+
428+
// Hours (with leading zero if needed)
429+
if hours < 10 && write_pos < out.len() {
430+
out[write_pos] = b'0';
431+
write_pos += 1;
432+
}
433+
let len = u64_to_str(hours, &mut out[write_pos..])?;
434+
write_pos += len;
435+
436+
// Colon
437+
if write_pos < out.len() {
438+
out[write_pos] = b':';
439+
write_pos += 1;
440+
}
441+
442+
// Minutes (with leading zero if needed)
443+
if minutes < 10 && write_pos < out.len() {
444+
out[write_pos] = b'0';
445+
write_pos += 1;
446+
}
447+
let len = u64_to_str(minutes, &mut out[write_pos..])?;
448+
write_pos += len;
449+
450+
// Colon
451+
if write_pos < out.len() {
452+
out[write_pos] = b':';
453+
write_pos += 1;
454+
}
455+
456+
// Seconds (with leading zero if needed)
457+
if seconds < 10 && write_pos < out.len() {
458+
out[write_pos] = b'0';
459+
write_pos += 1;
460+
}
461+
let len = u64_to_str(seconds, &mut out[write_pos..])?;
462+
write_pos += len;
463+
464+
Ok(write_pos)
465+
}
466+
467+
// Helper function to determine if a year is a leap year
468+
#[inline(always)]
469+
fn is_leap_year(year: u64) -> bool {
470+
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
471+
}
472+
236473
// Format a u64 value with decimal places and optional symbol
237474
#[inline(never)]
238475
pub fn format_token_amount(
475 Bytes
Loading
13.4 KB
Loading
21 Bytes
Loading
5 Bytes
Loading
5 Bytes
Loading

0 commit comments

Comments
 (0)