-
Notifications
You must be signed in to change notification settings - Fork 13.9k
uefi: fs: Add file times plumbing #138918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
(I'll initially mark these PRs as waiting on author in the sense that, that they are waiting for the author to summon @nicholasbishop and get a review. It can be marked as ready when there's a review.) |
|
@rustbot label +O-UEFI |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit worried about the edge cases here. It's not that uncommon to end up with a system time of 1970-1-1 if the devices clock is uninitialised. If the current timezone is positive, this might lead to a panic when converting it to a UNIX time. Considering the insanely large bounds afforded by secs being 64-bit, using a signed integer in the conversion algorithm would allow converting dates before the UNIX epoch as well.
Aside from that, the checked methods on SystemTime operate on the assumption that the operation will fail if the time is not representable in the operating system format – but this is currently not the case: UEFI allows times from the year 1900 up to and including the year 9999, whereas a Duration since the UNIX epoch allows representing times from 1970 up to the heat death of the universe, but not before that. I think it would be better to use the UEFI time representation for SystemTime and only convert it into a Duration for the addition/subtraction operations.
library/std/src/sys/pal/uefi/time.rs
Outdated
| let secs = if timezone == r_efi::efi::UNSPECIFIED_TIMEZONE { | ||
| dur.as_secs() | ||
| } else { | ||
| dur.as_secs().checked_add_signed(-timezone as i64).unwrap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd use subtraction here, just like the formula in the UEFI spec.
| dur.as_secs().checked_add_signed(-timezone as i64).unwrap() | |
| dur.as_secs().checked_sub_signed(timezone as i64).expect("times should be representable as local UEFI times") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checked_sub_signed is currently unstable. I can add the feature if that is fine though.
| let remaining_secs = secs % SECS_IN_DAY; | ||
| let z = days + 719468; | ||
| let era = z / 146097; | ||
| let doe = z - (era * 146097); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is clear by operator precedence, and the source doesn't use parenthesis here either.
| let doe = z - (era * 146097); | |
| let doe = z - era * 146097; |
Yes, I did come to the same conclusion. I think the reason I initially went with duration was to have a simple implementation for all the function implemented on SystemTime (since all the results there are in |
|
☔ The latest upstream changes (presumably #145300) made this pull request unmergeable. Please resolve the merge conflicts. |
4c497b4 to
48330eb
Compare
|
@rustbot ready ping @nicholasbishop @joboet |
|
r? @joboet |
library/std/src/sys/fs/uefi.rs
Outdated
|
|
||
| #[derive(Copy, Clone, Debug, Default)] | ||
| pub struct FileTimes {} | ||
| #[derive(Copy, Clone, Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Time implements Default, so can't we keep deriving Default here and remove ZERO_TIME?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
48330eb to
e21995b
Compare
library/std/src/sys/fs/uefi.rs
Outdated
| pub fn set_modified(&mut self, _t: SystemTime) {} | ||
| pub fn set_accessed(&mut self, t: SystemTime) { | ||
| self.accessed = | ||
| t.to_uefi(self.accessed.timezone, self.accessed.daylight).expect("Invalid Time"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid this panic? As the SystemTime was created successfully (i.e. checked_add did not return an error), this panic will come as a surprise to users.
A better way perhaps would be to try the initial timezone first, and if that fails, return the closest timezone (minute offset) that still allows the time to be represented. If filesystems ignore the timezone field, this would lead to the first and last 1440 minutes being collapsed into one minute, which doesn't seem too bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added the appropriate helpers to use the closest timezone. I have also added the tests that I have used locally to check the new timezone.
e21995b to
6637ec8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uefi lgtm
library/std/src/sys/pal/uefi/time.rs
Outdated
| system_time_internal::to_uefi(&self.0, timezone, daylight) | ||
| } | ||
|
|
||
| /// Create UEFI Time with the closes timezone (minute offset) that still allows the time to be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// Create UEFI Time with the closes timezone (minute offset) that still allows the time to be | |
| /// Create UEFI Time with the closest timezone (minute offset) that still allows the time to be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
6637ec8 to
0632fd5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this.
library/std/src/sys/fs/uefi.rs
Outdated
| pub struct FileAttr { | ||
| attr: u64, | ||
| size: u64, | ||
| created: r_efi::efi::Time, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UEFI allows creation time to be set, so created should be part of the FileTimes struct (a future PR can then use it to implement set_created in an extension trait like on Windows and Apple platforms).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
library/std/src/sys/fs/uefi.rs
Outdated
| pub fn set_accessed(&mut self, _t: SystemTime) {} | ||
| pub fn set_modified(&mut self, _t: SystemTime) {} | ||
| pub fn set_accessed(&mut self, t: SystemTime) { | ||
| self.accessed = t.to_uefi_loose(self.accessed.timezone, self.accessed.daylight); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.timezone and .daylight will always be zero here (unless set_accessed/set_modified was called previously and that time had to have it's timezone adjusted to fit in the range of EFI_TIME). The EDK II FAT driver will ignore the timezone when setting the time.. As FAT stores timestamps in the system local time, this conversion should use the current system timezone from the runtime services GetTime in order to be compatible with that driver's behaviour. This conversion should be done in the File::set_times implementation to have consistent behaviour in the (unlikely) event the timezone is changed between the call to set_accessed and the actual setting of the file times in File::set_times (which I believe isn't being implemented in this PR); therefore the FileTimes struct should look something like struct FileTimes { accessed: SystemTime, modified: SystemTime, created: SystemTime }.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have converted all FileTimes members to be SystemTime.
library/std/src/sys/fs/uefi.rs
Outdated
|
|
||
| pub fn modified(&self) -> io::Result<SystemTime> { | ||
| unsupported() | ||
| Ok(SystemTime::from_uefi(self.times.modified)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FAT timestamps are always stored in local time in the system timezone, and the EDK II FAT driver uses EFI_UNSPECIFIED_TIMEZONE to represent this. Therefore, if the modified/accessed/created EFI time has a timezone of unspecified, this conversion should use the timezone from the runtime services GetTime in order to be compatible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this PR does not have any boundary code, I have added helper uefi_to_systemtime that uses the timezone from current time in unspecified timezone cases.
When converting back to UEFI time, I am also using local timezone now in the helper systemtime_to_uefi
All these helpers are local fs since we do not know if some other subsystem might want to handle these specific cases differently.
0632fd5 to
fc63980
Compare
|
|
||
| // FIXME(#126043): use checked_sub_signed once stabilized | ||
| let secs = | ||
| dur.as_secs().checked_add_signed((-timezone as i64) * SECS_IN_MINUTE as i64).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the wrong sign – you need to add the new timezone offset. Also, this will panic for positive timezones when the duration is very small – the overflow check should occur below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UEFI fields are in locatime. And it is given in the spec that LocalTime = UTC - Timezone. When converting from UEFI to our time in secs from epoch, we do LocalTime + Timezone since we want to have the secs when time is in UTC.
So to get back localtime, wouldn't we do UTC secs - Timezone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I got confused because RFC 3339 uses the exact opposite sign convention. You are indeed correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will still panic for Duration::ZERO in e.g. timezone 1440 though.
library/std/src/sys/pal/uefi/time.rs
Outdated
| const SECS_IN_MINUTE: u64 = 60; | ||
| const SECS_IN_HOUR: u64 = SECS_IN_MINUTE * 60; | ||
| const SECS_IN_DAY: u64 = SECS_IN_HOUR * 24; | ||
| const TIMEZONE_DELTA: u64 = 1440 * SECS_IN_MINUTE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you could rename this to SYSTEMTIME_TIMEZONE so that its clearer in which direction the delta applies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed
library/std/src/sys/pal/uefi/time.rs
Outdated
|
|
||
| // Convert to seconds since 1900-01-01-00:00:00 in timezone. | ||
| let Some(secs) = secs.checked_sub(TIMEZONE_DELTA) else { return None }; | ||
| let Some(secs) = secs.checked_sub(TIMEZONE_DELTA) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let Some(secs) = secs.checked_sub(TIMEZONE_DELTA) else { | |
| let Some(secs) = secs.checked_add_signed((timezone - SYSTEMTIME_TIMEZONE) as i64 * SECS_IN_MINUTE) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also not be needed now. The first subtraction contains the comment explaining why it is fine to unwrap, since it can never fail.
| let Some(secs) = secs.checked_sub(TIMEZONE_DELTA) else { | ||
| let new_tz = | ||
| (secs / SECS_IN_MINUTE - if secs % SECS_IN_MINUTE == 0 { 0 } else { 1 }) as i16; | ||
| return Err(new_tz); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a usecase for to_uefi outside of to_uefi_loose? Otherwise you could update the timezone here instead of returning an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I am not sure how to deal with the other case (end of re-presentable time) early. So not sure if there is much benefit if I need to do a recursive call at the end.
I can think about optimizing it more over the weekend if that is required. But it is possible that having this strict version of the function will be required when adding something in the future anyway.
library/std/src/sys/fs/uefi.rs
Outdated
| /// Convert to UEFI Time with the current timezone. | ||
| #[allow(dead_code)] | ||
| fn systemtime_to_uefi(time: SystemTime) -> r_efi::efi::Time { | ||
| time.to_uefi_loose(current_timezone(), 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also take the daylight value from time::system_time_internal::now().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
library/std/src/sys/fs/uefi.rs
Outdated
| accessed: SystemTime, | ||
| modified: SystemTime, | ||
| created: SystemTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| accessed: SystemTime, | |
| modified: SystemTime, | |
| created: SystemTime, | |
| accessed: Option<SystemTime>, | |
| modified: Option<SystemTime>, | |
| created: Option<SystemTime>, |
Apologies, my suggestion in my previous review comment was incomplete. These all need to be optional as when using FileTimes to set file timestamps it's possible to only set some of the timestamps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
library/std/src/sys/fs/uefi.rs
Outdated
|
|
||
| /// EDK2 FAT driver uses EFI_UNSPECIFIED_TIMEZONE to represent localtime. So for proper | ||
| /// conversion to SystemTime, we use the current time to get the timezone in such cases. | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #[allow(dead_code)] | |
| #[expect(dead_code)] |
To ensure the attribute gets removed once the function is used (same with the #[allow(dead_code)] on systemtime_to_uefi).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
9bb2a65 to
c7325fc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
PS: Still not familiar with the development model and best practices within Rust std itself.
| FileType::from_attr(self.attr) | ||
| } | ||
|
|
||
| pub fn modified(&self) -> io::Result<SystemTime> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is returning a Result here enforced by higher-level APIs in Rust STD? If not, why not return Option<SystemTime>?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, it is.
|
|
||
| /// EDK2 FAT driver uses EFI_UNSPECIFIED_TIMEZONE to represent localtime. So for proper | ||
| /// conversion to SystemTime, we use the current time to get the timezone in such cases. | ||
| #[expect(dead_code)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why dead code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, just to keep the PRs small to review, and maintain continuity in future PRs. Any interactions with these conversions will require implementing GetInfo, which uses a C DST (and hence would require care and proper review) and thus needs to be a separate PR.
Also, the to and fro conversion are strongly related. So best to present them in the same PR.
It might be 2026 by the time that I start upstreaming code that requires conversion from UEFI to systemtime. So I would probably forget a lot of the important information in the current discussion. Having a conversion function already present saves having to repeat the same discussion (or worse, letting wrong code be merged).
c7325fc to
9425040
Compare
This comment has been minimized.
This comment has been minimized.
- Add FileTimes implementation. Signed-off-by: Ayush Singh <[email protected]>
9425040 to
0629cda
Compare
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
| pub struct FileAttr { | ||
| attr: u64, | ||
| size: u64, | ||
| times: FileTimes, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, UEFI's EFI_FILE_INFO always contains the file times, right? Then this shouldn't be FileTimes, but rather unconditionally contain the SystemTimes. That also gets rid of the weird unsupported error in the accessors below.
| assert_eq!(from_uefi(&t), Duration::new(1440 * SECS_IN_MINUTE, 0)); | ||
| assert_eq!(t, to_uefi(&from_uefi(&t), 0, 0).unwrap()); | ||
| assert!(to_uefi(&from_uefi(&t), -1440, 0).is_some()); | ||
| assert!(to_uefi(&from_uefi(&t), -1440, 0).is_err()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something is wrong in the code, this should succeed (localtime = UTC - timezone, so the local time is after the 1st of january 1900).
|
|
||
| let inp = Duration::from_secs(1450 * SECS_IN_MINUTE + 10); | ||
| let new_tz = to_uefi(&inp, 1440, 0).err().unwrap(); | ||
| assert_eq!(new_tz, 9); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect, if 1450 * SECS_IN_MINUTE is representable in timezone 10, then times after that should be too, right?
| const SECS_IN_HOUR: u64 = SECS_IN_MINUTE * 60; | ||
| const SECS_IN_DAY: u64 = SECS_IN_HOUR * 24; | ||
| const TIMEZONE_DELTA: u64 = 1440 * SECS_IN_MINUTE; | ||
| const SYSTEMTIME_TIMEZONE: u64 = 1440 * SECS_IN_MINUTE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timezone for SystemTime is -1440, since that is the timezone with the earliest representable value. So the sign of this value should be flipped (as well as the sign of the additions/subtractions you use it for).
| let adjusted_localtime_epoc: u64 = localtime_epoch + SYSTEMTIME_TIMEZONE; | ||
|
|
||
| let epoch: u64 = if t.timezone == r_efi::efi::UNSPECIFIED_TIMEZONE { | ||
| adjusted_localtime_epoc | ||
| } else { | ||
| adjusted_localtime_epoc | ||
| .checked_add_signed((t.timezone as i64) * SECS_IN_MINUTE as i64) | ||
| .unwrap() | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splitting this calculation is confusing to me, as we want to do a conversion from the original timezone to timezone -1440. Could you perhaps normalise the timezone first (change UNSPECIFIED_TIMEZONE to UTC) and then do the conversion in one go?
|
|
||
| // FIXME(#126043): use checked_sub_signed once stabilized | ||
| let secs = | ||
| dur.as_secs().checked_add_signed((-timezone as i64) * SECS_IN_MINUTE as i64).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will still panic for Duration::ZERO in e.g. timezone 1440 though.
|
☔ The latest upstream changes (presumably #147826) made this pull request unmergeable. Please resolve the merge conflicts. |
cc @nicholasbishop
r? @petrochenkov