Skip to main content

xrpl_common_stdlib/fields/
current_ledger_obj.rs

1//! # Current Ledger Object Field Retrieval Module (no slot)
2//!
3//! Typed accessors for reading fields from the *current* ledger object — the ledger entry the
4//! host is executing against — without first caching it into a slot. This is the no-slot
5//! counterpart to [`crate::fields::ledger_obj`]. `get_field` and `get_field_optional` are generic
6//! over any type implementing [`crate::fields::decoder::FromLedger`] — see
7//! [`crate::fields::decoder`] for how a type opts into that.
8//!
9//! ## Optional vs Required Fields
10//!
11//! - **Required** (`get_field`): Returns an error if the field is missing.
12//! - **Optional** (`get_field_optional`): Returns `Ok(None)` if the field is missing.
13
14use crate::fields::decoder::{FromLedger, decode_host_result};
15use crate::host::{Error, Result, home_le_field};
16use crate::sfield::SField;
17use crate::types::blob::Blob;
18use core::mem::MaybeUninit;
19
20/// Retrieves a field from the current ledger object using an SField constant.
21///
22/// # Returns
23///
24/// Returns a `Result<T>` where:
25/// * `Ok(T)` - The field value for the specified field
26/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
27#[inline]
28pub fn get_field<T: FromLedger, const CODE: i32>(_: SField<T, CODE>) -> Result<T> {
29    let mut buf = T::empty_buffer();
30    let n = {
31        let slice = buf.as_mut();
32        unsafe { home_le_field(CODE, slice.as_mut_ptr(), slice.len()) }
33    };
34    decode_host_result::<T>(buf, n)
35}
36
37/// Retrieves an optionally present field from the current ledger object.
38///
39/// # Returns
40///
41/// Returns a `Result<Option<T>>` where:
42/// * `Ok(Some(T))` - The field value for the specified field
43/// * `Ok(None)` - If the field is not present (i.e., result_code == FIELD_NOT_FOUND)
44/// * `Err(Error)` - If the field cannot be retrieved, has unexpected size, or fails to decode
45#[inline]
46pub fn get_field_optional<T: FromLedger, const CODE: i32>(
47    field: SField<T, CODE>,
48) -> Result<Option<T>> {
49    match get_field(field) {
50        Result::Ok(value) => Result::Ok(Some(value)),
51        Result::Err(Error::FieldNotFound) => Result::Ok(None),
52        Result::Err(e) => Result::Err(e),
53    }
54}
55
56// --- `Blob<N>`-specific accessors ------------------------------------------------------------
57//
58// See the matching comment in `fields::ledger_obj` for the full rationale — this is the same
59// zero-copy accessor, mirrored here for reading `Blob<N>` fields off the current ledger object
60// (no slot) instead of a slot-cached one. In short: the generic `get_field`/`get_field_optional`
61// above allocate their own scratch buffer and then reconstruct `T` from it, which for large
62// `Blob<N>` values (e.g. `WasmBlob`, N = 4096) compiles to a real, measured ~4092-byte `memcpy`
63// under this crate's size-optimized (`opt-level = "s"`) release profile. These functions instead
64// have the host write straight into the returned `Blob<N>`'s own `data` field, so no such
65// reconstruction — and no such copy — ever happens. Deliberately not folded into the generic
66// path, which stays untouched for every other (small, copy-is-negligible) field type.
67
68/// Retrieves a `Blob<N>` field from the current ledger object, writing the host's bytes directly
69/// into the returned `Blob<N>`'s own storage. See the comment above this function for why
70/// `Blob<N>` gets a dedicated accessor instead of using [`get_field`].
71#[inline]
72pub fn get_blob_field<const N: usize, const CODE: i32>(
73    _: SField<Blob<N>, CODE>,
74) -> Result<Blob<N>> {
75    let mut blob = MaybeUninit::<Blob<N>>::uninit();
76    // SAFETY: `data_ptr` points at the `data` field inside `blob`'s own (uninitialized) storage;
77    // `blob` outlives this pointer and no other reference to it exists yet.
78    let data_ptr = unsafe { core::ptr::addr_of_mut!((*blob.as_mut_ptr()).data) } as *mut u8;
79    // Zero the destination *before* the host call: a result code of 0 is a legitimate "empty
80    // field" success (not an error), and `Blob::data` is `pub`, so callers may read past `len`
81    // directly. Zeroing in place (rather than in a separate scratch buffer) costs the same one
82    // `memset` the old code paid anyway, just at the final address instead of a temporary one.
83    unsafe { core::ptr::write_bytes(data_ptr, 0u8, N) };
84    let n = unsafe { home_le_field(CODE, data_ptr, N) };
85    if n < 0 {
86        return Result::Err(Error::from_code(n));
87    }
88    if n as usize > N {
89        // A conformant host never reports writing more bytes than the buffer holds.
90        return Result::Err(Error::PointerOutOfBounds);
91    }
92    // SAFETY: `data` was fully zeroed above and then (partially) overwritten by the host, so
93    // every byte is initialized; `len` is set right before `assume_init`, completing the value.
94    unsafe {
95        core::ptr::addr_of_mut!((*blob.as_mut_ptr()).len).write(n as usize);
96        Result::Ok(blob.assume_init())
97    }
98}
99
100/// Optional variant of [`get_blob_field`]: returns `Ok(None)` if the field is not present,
101/// otherwise behaves identically (including the direct-write zero-copy behavior). Implemented
102/// in terms of [`get_blob_field`] itself rather than duplicating its body — `Error::FieldNotFound`
103/// round-trips exactly through `Error::from_code`/`.code()` (both are just `FIELD_NOT_FOUND`),
104/// so translating that one error case into `None` is all this needs to do.
105#[inline]
106pub fn get_blob_field_optional<const N: usize, const CODE: i32>(
107    field: SField<Blob<N>, CODE>,
108) -> Result<Option<Blob<N>>> {
109    match get_blob_field(field) {
110        Result::Ok(blob) => Result::Ok(Some(blob)),
111        Result::Err(Error::FieldNotFound) => Result::Ok(None),
112        Result::Err(e) => Result::Err(e),
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::{get_blob_field, get_blob_field_optional, get_field, get_field_optional};
119    use crate::host::error_codes::{FIELD_NOT_FOUND, SOME_ERROR};
120    use crate::host::host_bindings_trait::MockHostBindings;
121    use crate::host::setup_mock;
122    use crate::sfield;
123    use crate::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
124    use mockall::predicate::{always, eq};
125
126    fn expect_current_field(
127        mock: &mut MockHostBindings,
128        field_code: i32,
129        size: usize,
130        times: usize,
131    ) {
132        mock.expect_home_le_field()
133            .with(eq(field_code), always(), eq(size))
134            .times(times)
135            .returning(move |_, _, _| size as i32);
136    }
137
138    #[test]
139    fn test_get_field_success() {
140        let mut mock = MockHostBindings::new();
141        expect_current_field(&mut mock, sfield::Sequence.into(), 4, 1);
142        expect_current_field(&mut mock, sfield::Account.into(), ACCOUNT_ID_SIZE, 1);
143        let _guard = setup_mock(mock);
144
145        assert!(get_field::<u32, _>(sfield::Sequence).is_ok());
146        assert!(get_field::<AccountID, _>(sfield::Account).is_ok());
147    }
148
149    #[test]
150    fn test_get_field_optional_returns_none_on_field_not_found() {
151        let mut mock = MockHostBindings::new();
152        mock.expect_home_le_field()
153            .with(eq::<i32>(sfield::SourceTag.into()), always(), eq(4))
154            .times(1)
155            .returning(|_, _, _| FIELD_NOT_FOUND);
156        let _guard = setup_mock(mock);
157
158        let result = get_field_optional::<u32, _>(sfield::SourceTag);
159        assert!(result.is_ok());
160        assert!(result.unwrap().is_none());
161    }
162
163    #[test]
164    fn test_get_field_optional_returns_some_when_present() {
165        let mut mock = MockHostBindings::new();
166        expect_current_field(&mut mock, sfield::SourceTag.into(), 4, 1);
167        let _guard = setup_mock(mock);
168
169        let result = get_field_optional::<u32, _>(sfield::SourceTag);
170        assert!(result.is_ok());
171        assert!(result.unwrap().is_some());
172    }
173
174    #[test]
175    fn test_get_field_returns_decode_error_on_byte_mismatch() {
176        // u32's FieldDecoder requires exactly 4 bytes; a shorter write fails the length check
177        // and surfaces as InvalidDecoding.
178        let mut mock = MockHostBindings::new();
179        mock.expect_home_le_field()
180            .with(eq::<i32>(sfield::Sequence.into()), always(), eq(4))
181            .times(1)
182            .returning(|_, _, _| 3);
183        let _guard = setup_mock(mock);
184
185        let result = get_field::<u32, _>(sfield::Sequence);
186        assert!(result.is_err());
187        assert_eq!(
188            result.err().unwrap().code(),
189            crate::host::Error::InvalidDecoding.code()
190        );
191    }
192
193    #[test]
194    fn test_get_field_returns_err_on_internal_error() {
195        let mut mock = MockHostBindings::new();
196        mock.expect_home_le_field()
197            .with(eq::<i32>(sfield::Flags.into()), always(), eq(4))
198            .times(1)
199            .returning(|_, _, _| SOME_ERROR);
200        let _guard = setup_mock(mock);
201
202        let result = get_field::<u32, _>(sfield::Flags);
203        assert!(result.is_err());
204        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
205    }
206
207    #[test]
208    fn test_get_field_returns_err_when_host_reports_oversized_write() {
209        // A conformant host can't write past the buffer it was handed; a positive count larger
210        // than the buffer is reported as PointerOutOfBounds.
211        let mut mock = MockHostBindings::new();
212        mock.expect_home_le_field()
213            .with(eq::<i32>(sfield::Sequence.into()), always(), eq(4))
214            .times(1)
215            .returning(|_, _, _| 8); // claims 8 bytes into a 4-byte u32 buffer
216        let _guard = setup_mock(mock);
217
218        let result = get_field::<u32, _>(sfield::Sequence);
219        assert!(result.is_err());
220        assert_eq!(
221            result.err().unwrap().code(),
222            crate::host::Error::PointerOutOfBounds.code()
223        );
224    }
225
226    #[test]
227    fn test_get_blob_field_writes_bytes_directly_into_blob_data() {
228        let mut mock = MockHostBindings::new();
229        mock.expect_home_le_field()
230            .with(eq::<i32>(sfield::Condition.into()), always(), eq(128))
231            .times(1)
232            .returning(|_, buf, size| {
233                // Simulate the host writing 128 bytes of non-zero data.
234                let slice = unsafe { core::slice::from_raw_parts_mut(buf, size) };
235                slice.fill(0xAB);
236                size as i32
237            });
238        let _guard = setup_mock(mock);
239
240        let blob = get_blob_field(sfield::Condition).unwrap();
241        assert_eq!(blob.len(), 128);
242        assert!(blob.as_slice().iter().all(|&b| b == 0xAB));
243    }
244
245    #[test]
246    fn test_get_blob_field_zeroes_tail_when_host_writes_fewer_bytes() {
247        // A short write (e.g. an empty/undersized field) must leave the tail zeroed, not
248        // uninitialized -- `Blob::data` is `pub`, so callers may read past `len` directly.
249        let mut mock = MockHostBindings::new();
250        mock.expect_home_le_field()
251            .with(eq::<i32>(sfield::Condition.into()), always(), eq(128))
252            .times(1)
253            .returning(|_, buf, _size| {
254                let slice = unsafe { core::slice::from_raw_parts_mut(buf, 10) };
255                slice.fill(0xFF);
256                10
257            });
258        let _guard = setup_mock(mock);
259
260        let blob = get_blob_field(sfield::Condition).unwrap();
261        assert_eq!(blob.len(), 10);
262        assert_eq!(blob.data[9], 0xFF);
263        assert_eq!(blob.data[10], 0);
264        assert_eq!(blob.data[127], 0);
265    }
266
267    #[test]
268    fn test_get_blob_field_returns_err_on_internal_error() {
269        let mut mock = MockHostBindings::new();
270        mock.expect_home_le_field()
271            .with(eq::<i32>(sfield::Condition.into()), always(), eq(128))
272            .times(1)
273            .returning(|_, _, _| SOME_ERROR);
274        let _guard = setup_mock(mock);
275
276        let result = get_blob_field(sfield::Condition);
277        assert!(result.is_err());
278        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
279    }
280
281    #[test]
282    fn test_get_blob_field_returns_err_when_host_reports_oversized_write() {
283        let mut mock = MockHostBindings::new();
284        mock.expect_home_le_field()
285            .with(eq::<i32>(sfield::Condition.into()), always(), eq(128))
286            .times(1)
287            .returning(|_, _, _| 129); // claims 129 bytes into a 128-byte buffer
288        let _guard = setup_mock(mock);
289
290        let result = get_blob_field(sfield::Condition);
291        assert!(result.is_err());
292        assert_eq!(
293            result.err().unwrap().code(),
294            crate::host::Error::PointerOutOfBounds.code()
295        );
296    }
297
298    #[test]
299    fn test_get_blob_field_optional_returns_none_on_field_not_found() {
300        let mut mock = MockHostBindings::new();
301        mock.expect_home_le_field()
302            .with(eq::<i32>(sfield::Condition.into()), always(), eq(128))
303            .times(1)
304            .returning(|_, _, _| FIELD_NOT_FOUND);
305        let _guard = setup_mock(mock);
306
307        let result = get_blob_field_optional(sfield::Condition);
308        assert!(result.is_ok());
309        assert!(result.unwrap().is_none());
310    }
311
312    #[test]
313    fn test_get_blob_field_optional_returns_some_when_present() {
314        let mut mock = MockHostBindings::new();
315        expect_current_field(&mut mock, sfield::Condition.into(), 128, 1);
316        let _guard = setup_mock(mock);
317
318        let result = get_blob_field_optional(sfield::Condition);
319        assert!(result.is_ok());
320        assert!(result.unwrap().is_some());
321    }
322}