ver_shim/lib.rs
1//! Runtime access to version data injected via a link section.
2//!
3//! This crate provides a way to access git version information that has been
4//! injected into the binary via the `.ver_shim_data` link section.
5//!
6//! The section format is:
7//! - First byte: number of members in the section (for forward compatibility)
8//! - Next `num_members * 2` bytes: array of end offsets (u16, little-endian, relative to header)
9//! - Remaining bytes: concatenated string data
10//!
11//! Header size = 1 + num_members * 2
12//!
13//! For member N:
14//! - start = header_size + end[N-1] if N > 0, else header_size
15//! - end = header_size + end[N]
16//! - If start == end, the member is not present.
17//! - If N >= num_members (from first byte), the member is not present.
18//!
19//! Using relative offsets means a zero-initialized buffer reads as "all members absent".
20//! The num_members byte enables forward and backwards compatibility: old sections can be read by new code
21//! which has more members added in the future, and new sections can be read by old code as well,
22//! as long as we never change the index of any existing member.
23
24#![no_std]
25
26// Size of the version data buffer in bytes.
27// Can be overridden by setting VER_SHIM_BUFFER_SIZE env var at compile time.
28// Parsed as u16 since offsets in the header are u16 (max buffer size is 65535).
29#[doc(hidden)]
30pub const BUFFER_SIZE: usize = match option_env!("VER_SHIM_BUFFER_SIZE") {
31 Some(s) => match u16::from_str_radix(s, 10) {
32 Ok(n) => n as usize,
33 Err(_) => panic!("VER_SHIM_BUFFER_SIZE must be a valid u16 integer (0-65535)"),
34 },
35 None => 512,
36};
37
38// Calculate header size for a given number of members.
39// Header = 1 byte (num_members) + 2 bytes per member (end offsets).
40#[doc(hidden)]
41pub const fn header_size(num_members: usize) -> usize {
42 1 + num_members * 2
43}
44
45// Compile-time checks for buffer size validity.
46// We use 32 as a minimum threshold because:
47// - The header must fit (currently 19 bytes for 9 members)
48// - There must be room for actual data
49// - Anything smaller than 32 bytes is impractical
50// - We want to give clear error messages, so a simpler condition is better.
51const _: () = assert!(
52 header_size(Member::COUNT) <= 32,
53 "header_size(Member::COUNT) exceeds 32, these asserts must be updated"
54);
55const _: () = assert!(
56 BUFFER_SIZE > 32,
57 "VER_SHIM_BUFFER_SIZE must be greater than 32"
58);
59
60/// The section name used for version data.
61#[doc(hidden)]
62pub const SECTION_NAME: &str = ".ver_shim_data";
63
64// Members that can be stored in the version data.
65#[doc(hidden)]
66#[repr(u16)]
67#[derive(Clone, Copy)]
68pub enum Member {
69 GitSha = 0,
70 GitDescribe = 1,
71 GitBranch = 2,
72 GitCommitTimestamp = 3,
73 GitCommitDate = 4,
74 GitCommitMsg = 5,
75 BuildTimestamp = 6,
76 BuildDate = 7,
77 Custom = 8,
78}
79
80impl Member {
81 /// Number of members in the version data.
82 #[doc(hidden)]
83 pub const COUNT: usize = 9;
84}
85
86/// Static buffer for version data, placed in a custom link section.
87//
88// Note: We use "links" in the cargo toml for this crate to try to ensure that
89// only one version of this crate appears in the build graph, and so only one
90// version of the BUFFER exists, and BUFFER_SIZE = section size.
91#[unsafe(link_section = ".ver_shim_data")]
92#[used]
93static BUFFER: [u8; BUFFER_SIZE] = [0u8; BUFFER_SIZE];
94
95// Reads a byte from the buffer using volatile read to prevent optimization.
96// This is necessary because the compiler would otherwise inline the zeros
97// since the buffer is initialized to all zeros at compile time.
98#[inline(never)]
99fn read_buffer_byte(index: usize) -> u8 {
100 // SAFETY: index is bounds-checked by caller, BUFFER is static
101 unsafe { core::ptr::read_volatile(BUFFER.as_ptr().add(index)) }
102}
103
104// Reads a u16 from the buffer at the given offset (little-endian).
105fn read_buffer_u16(offset: usize) -> u16 {
106 let lo = read_buffer_byte(offset) as u16;
107 let hi = read_buffer_byte(offset + 1) as u16;
108 lo | (hi << 8)
109}
110
111// Reads a member from the version buffer.
112//
113// Returns:
114// - `None` if the member is not present (start == end, or member >= actual num_members)
115// - `Some(&str)` containing the member's string data
116//
117// Panics:
118// - If end < start (invalid range)
119// - If end > BUFFER_SIZE (out of bounds)
120// - If the data is not valid UTF-8
121fn get_member(member: Member) -> Option<&'static str> {
122 let idx = member as usize;
123
124 // Read the actual number of members from the first byte
125 let actual_num_members = read_buffer_byte(0) as usize;
126
127 // If first byte is 0, section is uninitialized (all zeros)
128 if actual_num_members == 0 {
129 return None;
130 }
131
132 // Forward compatibility: if requested member >= actual num_members, return None
133 if idx >= actual_num_members {
134 return None;
135 }
136
137 // Compute header size based on actual number of members in the section
138 let actual_header_size = header_size(actual_num_members);
139
140 // Read end offset for this member (stored at byte 1 + idx * 2, relative to header)
141 let end_offset_pos = 1 + idx * 2;
142 let end = actual_header_size + read_buffer_u16(end_offset_pos) as usize;
143
144 // Calculate start: header_size + previous member's end, or header_size for member 0
145 let start = if idx == 0 {
146 actual_header_size
147 } else {
148 let prev_end_pos = 1 + (idx - 1) * 2;
149 actual_header_size + read_buffer_u16(prev_end_pos) as usize
150 };
151
152 // If start == end, member is not present
153 if start == end {
154 return None;
155 }
156
157 // Validate range
158 if end < start {
159 panic!(
160 "ver-shim: invalid range for {:?}: start={}, end={}",
161 member as u16, start, end
162 );
163 }
164 if end > BUFFER_SIZE {
165 panic!(
166 "ver-shim: end offset {} exceeds buffer size {} for {:?}",
167 end, BUFFER_SIZE, member as u16
168 );
169 }
170
171 // Get the slice and convert to UTF-8.
172 // Use black_box to prevent the compiler from optimizing away the read,
173 // since the buffer is initialized to zeros at compile time, but changed at link time.
174 let bytes = core::hint::black_box(&BUFFER[start..end]);
175 match core::str::from_utf8(bytes) {
176 Ok(s) => Some(s),
177 Err(e) => panic!("ver-shim: invalid UTF-8 for {:?}: {:?}", member as u16, e),
178 }
179}
180
181/// Returns the git SHA, if present.
182///
183/// This is the full SHA from `git rev-parse HEAD`.
184pub fn git_sha() -> Option<&'static str> {
185 get_member(Member::GitSha)
186}
187
188/// Returns the git describe output, if present.
189///
190/// This is the output of `git describe --always --dirty`, which includes:
191/// - The most recent tag (if any)
192/// - Number of commits since that tag
193/// - Abbreviated commit hash
194/// - `-dirty` suffix if there are uncommitted changes
195pub fn git_describe() -> Option<&'static str> {
196 get_member(Member::GitDescribe)
197}
198
199/// Returns the git branch name, if present.
200///
201/// This is the output of `git rev-parse --abbrev-ref HEAD`.
202pub fn git_branch() -> Option<&'static str> {
203 get_member(Member::GitBranch)
204}
205
206/// Returns the git commit timestamp, if present.
207///
208/// This is the author date of HEAD formatted as RFC 3339
209/// (e.g., `2024-01-15T10:30:00+00:00`).
210pub fn git_commit_timestamp() -> Option<&'static str> {
211 get_member(Member::GitCommitTimestamp)
212}
213
214/// Returns the git commit date, if present.
215///
216/// This is the author date of HEAD formatted as a date only
217/// (e.g., `2024-01-15`).
218pub fn git_commit_date() -> Option<&'static str> {
219 get_member(Member::GitCommitDate)
220}
221
222/// Returns the git commit message, if present.
223///
224/// This is the first line of the commit message (subject line),
225/// truncated to at most 100 characters.
226pub fn git_commit_msg() -> Option<&'static str> {
227 get_member(Member::GitCommitMsg)
228}
229
230/// Returns the build timestamp, if present.
231///
232/// This is the time the binary was built, formatted as RFC 3339
233/// (e.g., `2024-01-15T10:30:00Z`).
234pub fn build_timestamp() -> Option<&'static str> {
235 get_member(Member::BuildTimestamp)
236}
237
238/// Returns the build date, if present.
239///
240/// This is the date the binary was built, formatted as YYYY-MM-DD
241/// (e.g., `2024-01-15`).
242pub fn build_date() -> Option<&'static str> {
243 get_member(Member::BuildDate)
244}
245
246/// Returns the custom application-specific string, if present.
247///
248/// This can be any string your application wants to embed into the binary.
249/// Set it using `LinkSection::with_custom()` in your build script.
250pub fn custom() -> Option<&'static str> {
251 get_member(Member::Custom)
252}