Skip to content

Commit 2fa257c

Browse files
committed
util/format_datetime: stop using strftime
This function takes up ~4kB of binary space, which is huge. The reason is that it handles every formatting char even though we only use a few like '%Y', '%M', etc. This reduces the firmware binary size by 3628 bytes.
1 parent 0354821 commit 2fa257c

File tree

4 files changed

+97
-35
lines changed

4 files changed

+97
-35
lines changed

.ci/ci

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ if arm-none-eabi-nm build/bin/firmware.elf | grep -q "float_to_decimal_common_sh
5959
echo "Use something simpler like (float*10).round() as u64, then format with util::decimal::format"
6060
exit 1
6161
fi
62+
if arm-none-eabi-nm build/bin/firmware.elf | grep -q "strftime"; then
63+
echo "strftime adds significant binary bloat. Use custom formatting like in `format_dateimte()`."
64+
exit 1
65+
fi
6266

6367
(cd tools/atecc608; go test ./...)
6468

src/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ add_custom_target(rust-bindgen
296296
--with-derive-default
297297
--ctypes-prefix util::c_types
298298
--allowlist-function bip32_derive_xpub
299-
--allowlist-function strftime
300299
--allowlist-function localtime
301300
--allowlist-function wally_free_string
302301
--allowlist-function mock_memory_factoryreset

src/rust/bitbox02-rust/src/backup.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,20 @@ pub fn create(
228228
let dir = id(seed);
229229
let files = bitbox02::sd::list_subdir(Some(&dir)).or(Err(Error::SdList))?;
230230

231+
let filename_datetime = {
232+
let tm = bitbox02::get_datetime(backup_create_timestamp).map_err(|_| Error::Generic)?;
233+
format!(
234+
"{}_{}T{}-{}-{}Z",
235+
tm.weekday(),
236+
tm.date(),
237+
tm.hour(),
238+
tm.minute(),
239+
tm.second()
240+
)
241+
};
242+
231243
for i in 0..3 {
232-
let filename = format!(
233-
"backup_{}_{}.bin",
234-
bitbox02::strftime(backup_create_timestamp, "%a_%Y-%m-%dT%H-%M-%SZ"),
235-
i,
236-
);
244+
let filename = format!("backup_{}_{}.bin", filename_datetime, i,);
237245
// Timestamp must be different from an existing backup when recreating a backup, otherwise
238246
// we might end up corrupting the existing backup.
239247
if files.contains(&filename) {

src/rust/bitbox02/src/lib.rs

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub mod secp256k1;
4848
pub mod securechip;
4949
pub mod ui;
5050

51+
use ::util::c_types::c_int;
5152
use core::time::Duration;
5253

5354
pub use bitbox02_sys::buffer_t;
@@ -116,19 +117,73 @@ pub fn reset(status: bool) {
116117
unsafe { bitbox02_sys::reset_reset(status) }
117118
}
118119

119-
pub fn strftime(timestamp: u32, format: &str) -> String {
120-
let mut out = [0u8; 100];
121-
unsafe {
122-
bitbox02_sys::strftime(
123-
out.as_mut_ptr(),
124-
out.len() as _,
125-
crate::util::str_to_cstr_vec(format).unwrap().as_ptr(),
126-
bitbox02_sys::localtime(&(timestamp as bitbox02_sys::time_t)),
127-
);
120+
pub struct Tm {
121+
tm: bitbox02_sys::tm,
122+
}
123+
124+
fn range(low: c_int, item: c_int, high: c_int) -> c_int {
125+
core::cmp::max(low, core::cmp::min(item, high))
126+
}
127+
128+
impl Tm {
129+
/// Returns the weekday, one of "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
130+
pub fn weekday(&self) -> String {
131+
// Same as '%a' in strftime:
132+
// https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#435
133+
let wday = self.tm.tm_wday;
134+
if !(0..=6).contains(&wday) {
135+
return "?".into();
136+
}
137+
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][wday as usize].into()
138+
}
139+
140+
/// Returns 'year-month-day', e.g. 2024-07-16, equivalent of '%Y-%m-%d' in strftime.
141+
pub fn date(&self) -> String {
142+
// Same as strftime:
143+
// %Y - https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#L712
144+
// %m - https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#L600
145+
// %d - https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#L498
146+
format!(
147+
"{}-{:02}-{:02}",
148+
1900 + self.tm.tm_year,
149+
range(0, self.tm.tm_mon, 11) + 1,
150+
range(1, self.tm.tm_mday, 31)
151+
)
152+
}
153+
154+
/// Returns the zero-padded hour from 00-23, e.g. "07".
155+
pub fn hour(&self) -> String {
156+
// Same as '%H' in strftime:
157+
// https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#582
158+
format!("{:02}", range(0, self.tm.tm_hour, 23))
159+
}
160+
161+
/// Returns the zero-padded minute from 00-59, e.g. "07".
162+
pub fn minute(&self) -> String {
163+
// Same as '%M' in strftime:
164+
// https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#L605
165+
format!("{:02}", range(0, self.tm.tm_min, 59))
128166
}
129-
crate::util::str_from_null_terminated(&out[..])
130-
.unwrap()
131-
.into()
167+
168+
/// Returns the zero-padded second from 00-60, e.g. "07".
169+
pub fn second(&self) -> String {
170+
// Same as '%S' in strftime:
171+
// https://github.com/arnoldrobbins/strftime/blob/2011b7e82365d25220b8949e252eb5f28c0994cd/strftime.c#L645
172+
format!("{:02}", range(0, self.tm.tm_sec, 60))
173+
}
174+
}
175+
176+
pub fn get_datetime(timestamp: u32) -> Result<Tm, ()> {
177+
Ok(Tm {
178+
tm: unsafe {
179+
let localtime = bitbox02_sys::localtime(&(timestamp as bitbox02_sys::time_t));
180+
if localtime.is_null() {
181+
return Err(());
182+
}
183+
184+
*localtime
185+
},
186+
})
132187
}
133188

134189
/// Formats the timestamp in the local timezone.
@@ -146,15 +201,19 @@ pub fn format_datetime(
146201
if !(MAX_WEST_UTC_OFFSET..=MAX_EAST_UTC_OFFSET).contains(&timezone_offset) {
147202
return Err(());
148203
}
149-
150-
Ok(strftime(
151-
((timestamp as i64) + (timezone_offset as i64)) as u32,
152-
if date_only {
153-
"%a %Y-%m-%d"
154-
} else {
155-
"%a %Y-%m-%d\n%H:%M"
156-
},
157-
))
204+
let ts = ((timestamp as i64) + (timezone_offset as i64)) as u32;
205+
let tm = get_datetime(ts)?;
206+
Ok(if date_only {
207+
format!("{} {}", tm.weekday(), tm.date())
208+
} else {
209+
format!(
210+
"{} {}\n{}:{}",
211+
tm.weekday(),
212+
tm.date(),
213+
tm.hour(),
214+
tm.minute()
215+
)
216+
})
158217
}
159218

160219
#[cfg(not(feature = "testing"))]
@@ -200,14 +259,6 @@ pub fn println_stdout(msg: &str) {
200259
mod tests {
201260
use super::*;
202261

203-
#[test]
204-
fn test_strftime() {
205-
assert_eq!(
206-
strftime(1601281809, "%a %Y-%m-%d\n%H:%M").as_str(),
207-
"Mon 2020-09-28\n08:30",
208-
);
209-
}
210-
211262
#[test]
212263
fn test_format_datetime() {
213264
assert_eq!(

0 commit comments

Comments
 (0)