Skip to main content

xrpl_common_stdlib/fields/
locator.rs

1//! Inner field access: encode a path (sfield codes and array indices) into the compact binary
2//! format the host understands, then read a field like `Memos[0].MemoType`.
3//!
4//! Two APIs share the same buffer layout:
5//!
6//! - [`TxPathBuilder`] and [`LedgerPathBuilder`] are the recommended fluent builders. Each is
7//!   rooted at its context —
8//!   [`ctx.tx().path()`](crate::current_tx::traits::TransactionCommonFields::path) for the current
9//!   transaction, [`obj.path()`](crate::objects::traits::LedgerObjectCommonFields::path) or
10//!   [`ctx.escrow().path()`](crate::objects::traits::CurrentLedgerObjectCommonFields::path) for a
11//!   ledger object — so no bare buffer escapes and the terminal `get` always dispatches to the
12//!   host function matching that context. Field codes come from typed `SField` constants:
13//!   ```no_run
14//!   use xrpl_common_stdlib::current_tx::traits::TransactionCommonFields;
15//!   use xrpl_common_stdlib::sfield;
16//!   # fn demo(tx: &impl TransactionCommonFields) {
17//!   // Read Memos[0].MemoData from the current transaction.
18//!   let data = tx.path()
19//!       .field(sfield::Memos)
20//!       .index(0)
21//!       .field(sfield::MemoData)
22//!       .get::<u32>();
23//!   # let _ = data; }
24//!   ```
25//! - [`Locator`] itself is the lower-level buffer: [`pack`](Locator::pack) values in and pass
26//!   [`as_ptr`](Locator::as_ptr) / [`num_packed_bytes`](Locator::num_packed_bytes) to a raw host
27//!   call. Prefer the builder unless you need that manual control.
28//!   ```no_run
29//!   use xrpl_common_stdlib::fields::locator::Locator;
30//!   use xrpl_common_stdlib::sfield;
31//!   let mut l = Locator::new();
32//!   l.pack(sfield::Memos);
33//!   l.pack(0);
34//!   l.pack(sfield::MemoType);
35//!   # let _ = (l.len() >= 3);
36//!   ```
37
38use crate::fields::decoder::{FromCurrentTx, FromLedger, decode_host_result};
39use crate::host::error_codes::match_result_code;
40use crate::host::{
41    self, Result, home_le_inner, home_le_inner_arr_len, le_inner, le_inner_arr_len, tx_inner,
42    tx_inner_arr_len,
43};
44use crate::sfield::SField;
45
46/// The size of the buffer, in bytes, to use for any new locator
47const LOCATOR_BUFFER_SIZE: usize = 64; // max depth: 64/4 = 16
48
49/// A Locator encodes a path to an inner field as a sequence of 4-byte packed values
50/// (sfield codes or array indices) in a compact binary format understood by the host.
51///
52/// ## Derived Traits
53///
54/// - `Debug`: Useful for development and debugging
55/// - `Clone`: Reasonable for this 72-byte struct when explicit copying is needed
56/// - `Eq, PartialEq`: Enable comparisons between locators
57///
58/// Note: `Copy` is intentionally not derived due to the struct's size (72 bytes).
59/// Large `Copy` types can lead to accidental expensive copies and poor performance.
60/// Use `.clone()` when you need to duplicate a locator.
61#[derive(Clone, PartialEq, Eq, Debug)]
62#[repr(C)]
63pub struct Locator {
64    buffer: [u8; LOCATOR_BUFFER_SIZE],
65
66    /// An index into `buffer` where the next packing operation can be stored.
67    cur_buffer_index: usize,
68}
69
70impl Default for Locator {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Locator {
77    /// Create a new empty Locator.
78    pub fn new() -> Locator {
79        Self {
80            buffer: [0; LOCATOR_BUFFER_SIZE],
81            cur_buffer_index: 0,
82        }
83    }
84
85    pub fn pack(&mut self, sfield_or_index: impl Into<i32>) -> bool {
86        // Narrow to i32 before the real work so it isn't re-monomorphized per `Into<i32>` caller.
87        self.pack_value(sfield_or_index.into())
88    }
89
90    fn pack_value(&mut self, value: i32) -> bool {
91        if self.cur_buffer_index + 4 > LOCATOR_BUFFER_SIZE {
92            return false;
93        }
94
95        let value_bytes: [u8; 4] = value.to_le_bytes();
96        self.buffer[self.cur_buffer_index..self.cur_buffer_index + 4].copy_from_slice(&value_bytes);
97        self.cur_buffer_index += 4;
98
99        true
100    }
101
102    pub fn as_ptr(&self) -> *const u8 {
103        self.buffer.as_ptr()
104    }
105
106    pub fn num_packed_bytes(&self) -> usize {
107        self.cur_buffer_index
108    }
109
110    pub fn len(&self) -> usize {
111        self.cur_buffer_index
112    }
113
114    pub fn is_empty(&self) -> bool {
115        self.cur_buffer_index == 0
116    }
117
118    pub fn repack_last(&mut self, sfield_or_index: impl Into<i32>) -> bool {
119        self.repack_last_value(sfield_or_index.into())
120    }
121
122    fn repack_last_value(&mut self, value: i32) -> bool {
123        if self.cur_buffer_index < 4 {
124            return false;
125        }
126
127        self.cur_buffer_index -= 4;
128
129        let value_bytes: [u8; 4] = value.to_le_bytes();
130        self.buffer[self.cur_buffer_index..self.cur_buffer_index + 4].copy_from_slice(&value_bytes);
131        self.cur_buffer_index += 4;
132
133        true
134    }
135}
136
137/// Ask the host how many entries the array a path points at holds, for whichever inner-array-length
138/// host call `read` issues.
139///
140/// Both path builders encode into the same [`Locator`] and differ only in which host function
141/// consumes it, so the length protocol — reject a malformed path locally, treat any non-negative
142/// answer including zero as a real length — lives here once.
143fn array_len_for(
144    overflowed: bool,
145    locator: &Locator,
146    read: impl FnOnce(*const u8, usize) -> i32,
147) -> Result<u32> {
148    if overflowed {
149        return Result::Err(host::Error::LocatorMalformed);
150    }
151    let n = read(locator.as_ptr(), locator.num_packed_bytes());
152    match_result_code(n, || n as u32)
153}
154
155/// Fluent builder for reading an inner field from the current transaction.
156///
157/// Obtained from the context via
158/// [`ctx.tx().path()`](crate::current_tx::traits::TransactionCommonFields::path); rooting
159/// it there is what guarantees the terminal [`get`](Self::get) reads through the
160/// current-transaction host function and never crosses into a ledger-object read. Each
161/// [`field`](Self::field) / [`index`](Self::index) call appends one 4-byte segment to the
162/// underlying [`Locator`] buffer.
163///
164/// A path longer than the 64-byte buffer (more than 16 segments) can hold is not silently
165/// truncated: the overflow is remembered and surfaced as [`host::Error::LocatorMalformed`] from
166/// [`get`](Self::get), rather than sending the host a shorter path than the author wrote.
167///
168/// ```no_run
169/// use xrpl_common_stdlib::current_tx::traits::TransactionCommonFields;
170/// use xrpl_common_stdlib::host::Result;
171/// use xrpl_common_stdlib::sfield;
172/// use xrpl_common_stdlib::types::blob::StandardBlob;
173/// # fn demo(tx: &impl TransactionCommonFields) {
174/// // Walk every entry of an array field.
175/// if let Result::Ok(count) = tx.path().field(sfield::Memos).array_len() {
176///     for i in 0..count {
177///         let memo_type = tx
178///             .path()
179///             .field(sfield::Memos)
180///             .index(i)
181///             .field(sfield::Memo)
182///             .field(sfield::MemoType)
183///             .get::<StandardBlob>();
184///         # let _ = memo_type;
185///     }
186/// }
187/// # }
188/// ```
189#[derive(Clone, PartialEq, Eq, Debug)]
190pub struct TxPathBuilder {
191    locator: Locator,
192    /// Set once a segment could not be encoded — either it did not fit in the buffer, or an
193    /// `index` exceeded [`i32::MAX`]. Sticky: further calls stay malformed so the terminals report
194    /// the bad path instead of reading a truncated or misencoded one.
195    overflowed: bool,
196}
197
198impl TxPathBuilder {
199    /// Root a new builder at the current transaction. Callers reach this through
200    /// [`TransactionCommonFields::path`](crate::current_tx::traits::TransactionCommonFields::path).
201    pub(crate) fn for_current_tx() -> Self {
202        Self {
203            locator: Locator::new(),
204            overflowed: false,
205        }
206    }
207
208    /// Append a field code to the path.
209    ///
210    /// Takes a typed [`SField<T, CODE>`] constant (e.g. `sfield::Memos`) so the field code is a
211    /// compile-time constant; only the code is encoded — the field's declared type `T` is
212    /// irrelevant to the path and is chosen instead at [`get`](Self::get).
213    pub fn field<T, const CODE: i32>(self, _field: SField<T, CODE>) -> Self {
214        self.push(CODE)
215    }
216
217    /// Append an array slot index to the path (e.g. the `0` in `Memos[0]`).
218    ///
219    /// Locator segments are `i32`, so an index above [`i32::MAX`] would pack as a negative value the
220    /// host would read back as a field code. Rather than encode that, the path is marked malformed
221    /// and the terminals report [`host::Error::LocatorMalformed`]. No array reachable through
222    /// [`array_len`](Self::array_len) can be that long — the host reports lengths as `i32` — so this
223    /// only rejects an index that did not come from walking the array.
224    pub fn index(mut self, index: u32) -> Self {
225        match i32::try_from(index) {
226            Ok(index) => self.push(index),
227            Err(_) => {
228                self.overflowed = true;
229                self
230            }
231        }
232    }
233
234    /// Append one 4-byte segment, recording buffer overflow so [`get`](Self::get) can reject a
235    /// truncated path.
236    fn push(mut self, value: i32) -> Self {
237        if !self.locator.pack(value) {
238            self.overflowed = true;
239        }
240        self
241    }
242
243    /// Execute the `tx_inner` host call for the built path and decode the result as `T`.
244    ///
245    /// `T` picks the terminal type (and therefore the read buffer size and decoder); it must be
246    /// readable from a transaction, hence the [`FromCurrentTx`] bound.
247    ///
248    /// Returns [`host::Error::LocatorMalformed`] without calling the host if the path overflowed
249    /// the buffer while being built.
250    pub fn get<T: FromCurrentTx>(&self) -> Result<T> {
251        if self.overflowed {
252            return Result::Err(host::Error::LocatorMalformed);
253        }
254        let (buf, n) = self.read::<T>();
255        decode_host_result::<T>(buf, n)
256    }
257
258    /// Like [`get`](Self::get) but treats an absent field as `Ok(None)` rather than an error —
259    /// the inner-path counterpart to
260    /// [`get_field_optional`](crate::current_tx::get_field_optional).
261    ///
262    /// Returns [`host::Error::LocatorMalformed`] without calling the host if the path overflowed.
263    pub fn get_optional<T: FromCurrentTx>(&self) -> Result<Option<T>> {
264        match self.get::<T>() {
265            Result::Ok(value) => Result::Ok(Some(value)),
266            Result::Err(host::Error::FieldNotFound) => Result::Ok(None),
267            Result::Err(e) => Result::Err(e),
268        }
269    }
270
271    /// Ask the host how many entries the array at this path holds, so it can be iterated.
272    ///
273    /// Named `array_len` rather than `len` because [`Locator::len`] already means "bytes packed into
274    /// the path"; this is the length of the array the path points *at*. Returns `Ok(0)` for a
275    /// present-but-empty array, and [`host::Error::LocatorMalformed`] without calling the host if
276    /// the path overflowed the buffer while being built.
277    ///
278    /// A single-segment path — one [`field`](Self::field) and no [`index`](Self::index) — is the
279    /// length of a top-level array such as `Memos`, so top-level arrays need no separate accessor.
280    pub fn array_len(&self) -> Result<u32> {
281        array_len_for(self.overflowed, &self.locator, |loc_ptr, loc_len| unsafe {
282            tx_inner_arr_len(loc_ptr, loc_len)
283        })
284    }
285
286    /// Run the built path through `tx_inner` into a fresh `T` buffer, returning that
287    /// buffer and the raw byte count the host reported (negative on error).
288    fn read<T: FromCurrentTx>(&self) -> (T::Buffer, i32) {
289        let mut buf = T::empty_buffer();
290        let n = {
291            let slice = buf.as_mut();
292            unsafe {
293                tx_inner(
294                    self.locator.as_ptr(),
295                    self.locator.num_packed_bytes(),
296                    slice.as_mut_ptr(),
297                    slice.len(),
298                )
299            }
300        };
301        (buf, n)
302    }
303}
304
305/// Which ledger object a [`LedgerPath`] reads from: selects the host function the terminals call,
306/// and for a slot-cached object carries the slot.
307///
308/// Deliberately holds only the two *ledger* sources. There is no transaction variant to select, so
309/// "a [`LedgerPathBuilder`] cannot read a transaction field" holds by construction rather than by
310/// convention — the [`FromLedger`] bound on the public terminals is a second line of defense, not
311/// the only one.
312#[derive(Clone, PartialEq, Eq, Debug)]
313enum LedgerSource {
314    /// The ledger object the contract is attached to (`home_le_inner`).
315    Current,
316    /// A ledger object cached in the given slot (`le_inner`).
317    Slot(i32),
318}
319
320impl LedgerSource {
321    /// Issue this source's inner-field host call for the packed `locator` bytes.
322    fn read_field(
323        &self,
324        loc_ptr: *const u8,
325        loc_len: usize,
326        out_ptr: *mut u8,
327        out_len: usize,
328    ) -> i32 {
329        match *self {
330            LedgerSource::Current => unsafe { home_le_inner(loc_ptr, loc_len, out_ptr, out_len) },
331            LedgerSource::Slot(slot) => unsafe {
332                le_inner(slot, loc_ptr, loc_len, out_ptr, out_len)
333            },
334        }
335    }
336
337    /// Issue this source's inner-array-length host call for the packed `locator` bytes.
338    fn read_array_len(&self, loc_ptr: *const u8, loc_len: usize) -> i32 {
339        match *self {
340            LedgerSource::Current => unsafe { home_le_inner_arr_len(loc_ptr, loc_len) },
341            LedgerSource::Slot(slot) => unsafe { le_inner_arr_len(slot, loc_ptr, loc_len) },
342        }
343    }
344}
345
346/// Fluent builder for reading an inner field from a ledger object — either the object the contract
347/// is attached to, or one cached into a slot.
348///
349/// Obtained from the context via
350/// [`obj.path()`](crate::objects::traits::LedgerObjectCommonFields::path) for a slot-cached object
351/// or [`ctx.escrow().path()`](crate::objects::traits::CurrentLedgerObjectCommonFields::path) for the
352/// current one. Any object type works, including ones with no bespoke wrapper: reach for
353/// [`LedgerObject::new(slot)`](crate::objects::LedgerObject::new) to build a handle around a raw slot
354/// from [`cache_le`](crate::objects::cache_le).
355///
356/// Mirrors [`TxPathBuilder`] — same [`Locator`] buffer, same overflow →
357/// [`host::Error::LocatorMalformed`] guard — with two differences: terminal reads are bounded on
358/// [`FromLedger`] instead of [`FromCurrentTx`], and a [`LedgerSource`] picks which of the two
359/// ledger-object host functions to call.
360///
361/// ```no_run
362/// use xrpl_common_stdlib::host::Result;
363/// use xrpl_common_stdlib::objects::traits::LedgerObjectCommonFields;
364/// use xrpl_common_stdlib::sfield;
365/// # fn demo(obj: &impl LedgerObjectCommonFields) {
366/// // Walk every entry of an array field.
367/// if let Result::Ok(count) = obj.path().field(sfield::PriceDataSeries).array_len() {
368///     for i in 0..count {
369///         let price = obj
370///             .path()
371///             .field(sfield::PriceDataSeries)
372///             .index(i)
373///             .field(sfield::AssetPrice)
374///             .get::<u64>();
375///         # let _ = price;
376///     }
377/// }
378/// # }
379/// ```
380#[derive(Clone, PartialEq, Eq, Debug)]
381pub struct LedgerPathBuilder {
382    locator: Locator,
383    /// Set once a segment could not be encoded — either it did not fit in the buffer, or an
384    /// `index` exceeded [`i32::MAX`]. Sticky: further calls stay malformed so the terminals report
385    /// the bad path instead of reading a truncated or misencoded one.
386    overflowed: bool,
387    source: LedgerSource,
388}
389
390impl LedgerPathBuilder {
391    /// Root a new builder at the current ledger object (no slot). Callers reach this through
392    /// [`CurrentLedgerObjectCommonFields::path`](crate::objects::traits::CurrentLedgerObjectCommonFields::path).
393    pub(crate) fn for_current_ledger_obj() -> Self {
394        Self::new(LedgerSource::Current)
395    }
396
397    /// Root a new builder at the ledger object cached in `slot`. Callers reach this through
398    /// [`LedgerObjectCommonFields::path`](crate::objects::traits::LedgerObjectCommonFields::path).
399    pub(crate) fn for_ledger_obj(slot: i32) -> Self {
400        Self::new(LedgerSource::Slot(slot))
401    }
402
403    fn new(source: LedgerSource) -> Self {
404        Self {
405            locator: Locator::new(),
406            overflowed: false,
407            source,
408        }
409    }
410
411    /// Append a field code to the path. See [`TxPathBuilder::field`].
412    pub fn field<T, const CODE: i32>(self, _field: SField<T, CODE>) -> Self {
413        self.push(CODE)
414    }
415
416    /// Append an array slot index to the path (e.g. the `0` in `SignerEntries[0]`). See
417    /// [`TxPathBuilder::index`].
418    pub fn index(mut self, index: u32) -> Self {
419        match i32::try_from(index) {
420            Ok(index) => self.push(index),
421            Err(_) => {
422                self.overflowed = true;
423                self
424            }
425        }
426    }
427
428    /// Append one 4-byte segment, recording overflow so the terminals can reject a truncated path.
429    fn push(mut self, value: i32) -> Self {
430        if !self.locator.pack(value) {
431            self.overflowed = true;
432        }
433        self
434    }
435
436    /// Execute the ledger-object inner-field host call for the built path and decode as `T`.
437    ///
438    /// `T` must be readable from a ledger object, hence the [`FromLedger`] bound. Returns
439    /// [`host::Error::LocatorMalformed`] without calling the host if the path is malformed.
440    pub fn get<T: FromLedger>(&self) -> Result<T> {
441        if self.overflowed {
442            return Result::Err(host::Error::LocatorMalformed);
443        }
444        let (buf, n) = self.read::<T>();
445        decode_host_result::<T>(buf, n)
446    }
447
448    /// Like [`get`](Self::get) but treats an absent field as `Ok(None)` rather than an error —
449    /// the inner-path counterpart to
450    /// [`get_field_optional`](crate::fields::ledger_obj::get_field_optional).
451    ///
452    /// Only reports absence for fields the host signals with `FieldNotFound`. Variable-length
453    /// fields (the `Blob<N>` family) are instead reported as a zero-byte write, so an absent one
454    /// yields `Ok(Some(blob))` with `blob.len == 0` rather than `Ok(None)` — the same distinction
455    /// [`ledger_obj::get_blob_field_optional`](crate::fields::ledger_obj::get_blob_field_optional)
456    /// exists to handle for flat fields.
457    pub fn get_optional<T: FromLedger>(&self) -> Result<Option<T>> {
458        match self.get::<T>() {
459            Result::Ok(value) => Result::Ok(Some(value)),
460            Result::Err(host::Error::FieldNotFound) => Result::Ok(None),
461            Result::Err(e) => Result::Err(e),
462        }
463    }
464
465    /// Ask the host how many entries the array at this path holds, so it can be iterated. See
466    /// [`TxPathBuilder::array_len`].
467    ///
468    /// A single-segment path — one [`field`](Self::field) and no [`index`](Self::index) — is the
469    /// length of a top-level array such as `SignerEntries`, so top-level arrays need no separate
470    /// accessor.
471    pub fn array_len(&self) -> Result<u32> {
472        array_len_for(self.overflowed, &self.locator, |loc_ptr, loc_len| {
473            self.source.read_array_len(loc_ptr, loc_len)
474        })
475    }
476
477    /// Run the built path through the source's host call into a fresh `T` buffer, returning that
478    /// buffer and the raw byte count the host reported (negative on error).
479    fn read<T: FromLedger>(&self) -> (T::Buffer, i32) {
480        let mut buf = T::empty_buffer();
481        let n = {
482            let slice = buf.as_mut();
483            self.source.read_field(
484                self.locator.as_ptr(),
485                self.locator.num_packed_bytes(),
486                slice.as_mut_ptr(),
487                slice.len(),
488            )
489        };
490        (buf, n)
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::sfield;
498
499    #[test]
500    fn test_pack_with_sfield_no_into_needed() {
501        // This test demonstrates that .into() is no longer needed when using SField constants
502        let mut locator = Locator::new();
503
504        // Pack SField constants directly without .into()
505        assert!(locator.pack(sfield::Memos));
506        assert!(locator.pack(0));
507        assert!(locator.pack(sfield::MemoData));
508
509        assert_eq!(locator.len(), 12); // 3 packed values * 4 bytes each
510    }
511
512    #[test]
513    fn test_pack_with_i32_still_works() {
514        // This test verifies that i32 values still work as before
515        let mut locator = Locator::new();
516
517        assert!(locator.pack(123i32));
518        assert!(locator.pack(456i32));
519
520        assert_eq!(locator.len(), 8); // 2 packed values * 4 bytes each
521    }
522
523    #[test]
524    fn test_repack_last_with_sfield() {
525        let mut locator = Locator::new();
526
527        locator.pack(sfield::Memos);
528        locator.pack(0);
529
530        // Repack the last value with a different SField
531        assert!(locator.repack_last(sfield::MemoData));
532
533        assert_eq!(locator.len(), 8); // Still 2 packed values
534    }
535
536    #[test]
537    fn test_new_starts_empty() {
538        let locator = Locator::new();
539        assert_eq!(locator.len(), 0);
540        assert!(locator.is_empty());
541    }
542
543    #[test]
544    fn test_default_same_as_new() {
545        assert_eq!(Locator::default(), Locator::new());
546    }
547
548    #[test]
549    fn test_pack_writes_correct_bytes() {
550        let mut locator = Locator::new();
551        assert!(locator.pack(0x12345678i32));
552        assert_eq!(locator.len(), 4);
553
554        let bytes = unsafe { core::slice::from_raw_parts(locator.as_ptr(), 4) };
555        assert_eq!(bytes, &0x12345678i32.to_le_bytes());
556    }
557
558    #[test]
559    fn test_pack_returns_false_when_buffer_full() {
560        let mut locator = Locator::new();
561
562        // Fill all 16 slots (64 bytes / 4 bytes per pack)
563        for i in 0..16 {
564            assert!(locator.pack(i));
565        }
566        assert_eq!(locator.len(), 64);
567
568        // 17th pack should fail
569        assert!(!locator.pack(999i32));
570        assert_eq!(locator.len(), 64);
571    }
572
573    #[test]
574    fn test_is_empty_false_after_pack() {
575        let mut locator = Locator::new();
576        assert!(locator.is_empty());
577
578        locator.pack(sfield::Memos);
579        assert!(!locator.is_empty());
580        assert_eq!(locator.len(), 4);
581    }
582
583    #[test]
584    fn test_num_packed_bytes_equals_len() {
585        let mut locator = Locator::new();
586        assert_eq!(locator.num_packed_bytes(), locator.len());
587
588        locator.pack(sfield::Memos);
589        assert_eq!(locator.num_packed_bytes(), locator.len());
590        assert_eq!(locator.num_packed_bytes(), 4);
591
592        locator.pack(0);
593        assert_eq!(locator.num_packed_bytes(), locator.len());
594        assert_eq!(locator.num_packed_bytes(), 8);
595    }
596
597    #[test]
598    fn test_repack_last_on_empty_returns_false() {
599        let mut locator = Locator::new();
600        assert!(!locator.repack_last(sfield::Memos));
601        assert_eq!(locator.len(), 0);
602    }
603
604    #[test]
605    fn test_repack_last_overwrites_correct_bytes() {
606        let mut locator = Locator::new();
607        locator.pack(0x11111111i32);
608        locator.pack(0x22222222i32);
609        assert_eq!(locator.len(), 8);
610
611        assert!(locator.repack_last(0x33333333i32));
612        assert_eq!(locator.len(), 8);
613
614        let bytes = unsafe { core::slice::from_raw_parts(locator.as_ptr(), 8) };
615        // First value unchanged
616        assert_eq!(&bytes[0..4], &0x11111111i32.to_le_bytes());
617        // Second value replaced
618        assert_eq!(&bytes[4..8], &0x33333333i32.to_le_bytes());
619    }
620
621    // ---- Fluent path builder (`ctx.tx().path()`) ----
622
623    use crate::host::error_codes::{FIELD_NOT_FOUND, SOME_ERROR};
624    use crate::host::host_bindings_trait::MockHostBindings;
625    use crate::host::setup_mock;
626    use crate::types::blob::StandardBlob;
627    use mockall::predicate::{always, eq};
628
629    /// The bytes a `TxPathBuilder` has packed so far, for asserting on the encoded path.
630    fn packed(builder: &TxPathBuilder) -> &[u8] {
631        &builder.locator.buffer[..builder.locator.cur_buffer_index]
632    }
633
634    #[test]
635    fn test_tx_field_encodes_single_field_code() {
636        let builder = TxPathBuilder::for_current_tx().field(sfield::Sequence);
637
638        assert!(!builder.overflowed);
639        assert_eq!(packed(&builder), &i32::from(sfield::Sequence).to_le_bytes());
640    }
641
642    #[test]
643    fn test_tx_multi_hop_encodes_each_field_in_order() {
644        let builder = TxPathBuilder::for_current_tx()
645            .field(sfield::Memos)
646            .field(sfield::MemoData);
647
648        assert!(!builder.overflowed);
649        let bytes = packed(&builder);
650        assert_eq!(bytes.len(), 8);
651        assert_eq!(&bytes[0..4], &i32::from(sfield::Memos).to_le_bytes());
652        assert_eq!(&bytes[4..8], &i32::from(sfield::MemoData).to_le_bytes());
653    }
654
655    #[test]
656    fn test_tx_index_encodes_array_slot() {
657        // Memos[2].MemoType
658        let builder = TxPathBuilder::for_current_tx()
659            .field(sfield::Memos)
660            .index(2)
661            .field(sfield::MemoType);
662
663        assert!(!builder.overflowed);
664        let bytes = packed(&builder);
665        assert_eq!(bytes.len(), 12);
666        assert_eq!(&bytes[0..4], &i32::from(sfield::Memos).to_le_bytes());
667        assert_eq!(&bytes[4..8], &2u32.to_le_bytes());
668        assert_eq!(&bytes[8..12], &i32::from(sfield::MemoType).to_le_bytes());
669    }
670
671    #[test]
672    fn test_tx_overflow_via_field_sets_flag_and_stops_at_64_bytes() {
673        // Fill all 16 slots (64 bytes) with array indices, then one more field can't fit.
674        let mut builder = TxPathBuilder::for_current_tx();
675        for i in 0..16 {
676            builder = builder.index(i);
677        }
678        assert!(!builder.overflowed);
679        assert_eq!(builder.locator.num_packed_bytes(), 64);
680
681        let builder = builder.field(sfield::Sequence);
682        assert!(builder.overflowed);
683        // The buffer is not grown or partially overwritten past its capacity.
684        assert_eq!(builder.locator.num_packed_bytes(), 64);
685    }
686
687    #[test]
688    fn test_tx_overflow_via_index_sets_flag_and_stops_at_64_bytes() {
689        // Same boundary, overflowing with `index` instead of `field`.
690        let mut builder = TxPathBuilder::for_current_tx();
691        for _ in 0..16 {
692            builder = builder.field(sfield::Sequence);
693        }
694        assert!(!builder.overflowed);
695        assert_eq!(builder.locator.num_packed_bytes(), 64);
696
697        let builder = builder.index(99);
698        assert!(builder.overflowed);
699        assert_eq!(builder.locator.num_packed_bytes(), 64);
700    }
701
702    #[test]
703    fn test_tx_index_above_i32_max_is_malformed_not_a_negative_segment() {
704        // Packing the bit pattern would hand the host a negative segment it reads as a field code.
705        let mut mock = MockHostBindings::new();
706        mock.expect_tx_inner().times(0);
707        let _guard = setup_mock(mock);
708
709        let builder = TxPathBuilder::for_current_tx()
710            .field(sfield::Memos)
711            .index(i32::MAX as u32 + 1);
712
713        assert!(builder.overflowed);
714        // The rejected index is not appended, so nothing misencoded reaches the buffer.
715        assert_eq!(builder.locator.num_packed_bytes(), 4);
716        assert_eq!(
717            builder.get::<u32>().err().unwrap().code(),
718            host::Error::LocatorMalformed.code()
719        );
720    }
721
722    #[test]
723    fn test_get_reads_and_decodes_inner_field() {
724        let mut mock = MockHostBindings::new();
725        // Path is Memos[0].MemoData -> three 4-byte segments = 12 bytes; u32 read buffer is 4.
726        mock.expect_tx_inner()
727            .with(always(), eq(12usize), always(), eq(4usize))
728            .times(1)
729            .returning(|_, _, _, _| 4);
730        let _guard = setup_mock(mock);
731
732        let result = TxPathBuilder::for_current_tx()
733            .field(sfield::Memos)
734            .index(0)
735            .field(sfield::MemoData)
736            .get::<u32>();
737
738        assert!(result.is_ok());
739    }
740
741    #[test]
742    fn test_get_returns_locator_malformed_when_overflowed_without_calling_host() {
743        // The host must not be queried for a path we know is truncated.
744        let mut mock = MockHostBindings::new();
745        mock.expect_tx_inner().times(0);
746        let _guard = setup_mock(mock);
747
748        let mut builder = TxPathBuilder::for_current_tx();
749        for i in 0..17 {
750            builder = builder.index(i);
751        }
752        assert!(builder.overflowed);
753
754        let result = builder.get::<u32>();
755        assert!(result.is_err());
756        assert_eq!(
757            result.err().unwrap().code(),
758            host::Error::LocatorMalformed.code()
759        );
760    }
761
762    #[test]
763    fn test_get_propagates_host_error() {
764        let mut mock = MockHostBindings::new();
765        mock.expect_tx_inner()
766            .with(always(), eq(4usize), always(), eq(4usize))
767            .times(1)
768            .returning(|_, _, _, _| SOME_ERROR);
769        let _guard = setup_mock(mock);
770
771        let result = TxPathBuilder::for_current_tx()
772            .field(sfield::Sequence)
773            .get::<u32>();
774
775        assert!(result.is_err());
776        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
777    }
778
779    #[test]
780    fn test_get_optional_returns_some_when_present() {
781        let mut mock = MockHostBindings::new();
782        mock.expect_tx_inner()
783            .with(always(), eq(12usize), always(), eq(4usize))
784            .times(1)
785            .returning(|_, _, _, _| 4);
786        let _guard = setup_mock(mock);
787
788        let result = TxPathBuilder::for_current_tx()
789            .field(sfield::Memos)
790            .index(0)
791            .field(sfield::MemoData)
792            .get_optional::<u32>();
793
794        assert!(result.is_ok());
795        assert!(result.unwrap().is_some());
796    }
797
798    #[test]
799    fn test_get_optional_returns_none_on_field_not_found() {
800        let mut mock = MockHostBindings::new();
801        mock.expect_tx_inner()
802            .with(always(), eq(4usize), always(), eq(4usize))
803            .times(1)
804            .returning(|_, _, _, _| FIELD_NOT_FOUND);
805        let _guard = setup_mock(mock);
806
807        let result = TxPathBuilder::for_current_tx()
808            .field(sfield::Sequence)
809            .get_optional::<u32>();
810
811        assert!(result.is_ok());
812        assert!(result.unwrap().is_none());
813    }
814
815    #[test]
816    fn test_tx_array_len_returns_count() {
817        let mut mock = MockHostBindings::new();
818        // A top-level array is a single 4-byte segment of path.
819        mock.expect_tx_inner_arr_len()
820            .with(always(), eq(4usize))
821            .times(1)
822            .returning(|_, _| 3);
823        let _guard = setup_mock(mock);
824
825        let result = TxPathBuilder::for_current_tx()
826            .field(sfield::Memos)
827            .array_len();
828
829        assert_eq!(result.unwrap(), 3);
830    }
831
832    #[test]
833    fn test_tx_array_len_zero_is_ok_not_an_error() {
834        let mut mock = MockHostBindings::new();
835        mock.expect_tx_inner_arr_len().times(1).returning(|_, _| 0);
836        let _guard = setup_mock(mock);
837
838        let result = TxPathBuilder::for_current_tx()
839            .field(sfield::Memos)
840            .array_len();
841
842        assert_eq!(result.unwrap(), 0);
843    }
844
845    #[test]
846    fn test_tx_array_len_propagates_host_error() {
847        use crate::host::error_codes::NO_ARRAY;
848        let mut mock = MockHostBindings::new();
849        mock.expect_tx_inner_arr_len()
850            .times(1)
851            .returning(|_, _| NO_ARRAY);
852        let _guard = setup_mock(mock);
853
854        let result = TxPathBuilder::for_current_tx()
855            .field(sfield::Sequence)
856            .array_len();
857
858        assert_eq!(result.err().unwrap().code(), NO_ARRAY);
859    }
860
861    #[test]
862    fn test_tx_array_len_returns_locator_malformed_when_overflowed_without_calling_host() {
863        let mut mock = MockHostBindings::new();
864        mock.expect_tx_inner_arr_len().times(0);
865        let _guard = setup_mock(mock);
866
867        let mut builder = TxPathBuilder::for_current_tx();
868        for i in 0..17 {
869            builder = builder.index(i);
870        }
871
872        assert_eq!(
873            builder.array_len().err().unwrap().code(),
874            host::Error::LocatorMalformed.code()
875        );
876    }
877
878    #[test]
879    fn test_tx_array_len_then_index_walks_every_entry() {
880        // The pattern `array_len()` exists for: count, then read each element.
881        let mut mock = MockHostBindings::new();
882        mock.expect_tx_inner_arr_len()
883            .with(always(), eq(4usize))
884            .times(1)
885            .returning(|_, _| 2);
886        // Two reads of Memos[i].Memo.MemoType -> 16 bytes of path.
887        mock.expect_tx_inner()
888            .with(always(), eq(16usize), always(), always())
889            .times(2)
890            .returning(|_, _, _, out_buff_len| out_buff_len as i32);
891        let _guard = setup_mock(mock);
892
893        let tx = TxPathBuilder::for_current_tx();
894        let count = tx.clone().field(sfield::Memos).array_len().unwrap();
895        assert_eq!(count, 2);
896        for i in 0..count {
897            let memo_type = tx
898                .clone()
899                .field(sfield::Memos)
900                .index(i)
901                .field(sfield::Memo)
902                .field(sfield::MemoType)
903                .get::<StandardBlob>();
904            assert!(memo_type.is_ok());
905        }
906    }
907
908    // ---- Fluent path builder (`obj.path()` / `ctx.escrow().path()`) ----
909
910    /// The bytes a `LedgerPathBuilder` has packed so far, for asserting on the encoded path.
911    fn ledger_packed(builder: &LedgerPathBuilder) -> &[u8] {
912        &builder.locator.buffer[..builder.locator.cur_buffer_index]
913    }
914
915    #[test]
916    fn test_ledger_field_encodes_single_field_code() {
917        let builder = LedgerPathBuilder::for_current_ledger_obj().field(sfield::Flags);
918
919        assert!(!builder.overflowed);
920        assert_eq!(
921            ledger_packed(&builder),
922            &i32::from(sfield::Flags).to_le_bytes()
923        );
924    }
925
926    #[test]
927    fn test_ledger_index_encodes_array_slot() {
928        // SignerEntries[2].Account
929        let builder = LedgerPathBuilder::for_ledger_obj(1)
930            .field(sfield::SignerEntries)
931            .index(2)
932            .field(sfield::Account);
933
934        assert!(!builder.overflowed);
935        let bytes = ledger_packed(&builder);
936        assert_eq!(bytes.len(), 12);
937        assert_eq!(
938            &bytes[0..4],
939            &i32::from(sfield::SignerEntries).to_le_bytes()
940        );
941        assert_eq!(&bytes[4..8], &2u32.to_le_bytes());
942        assert_eq!(&bytes[8..12], &i32::from(sfield::Account).to_le_bytes());
943    }
944
945    #[test]
946    fn test_ledger_overflow_sets_flag_and_stops_at_64_bytes() {
947        // Fill all 16 slots (64 bytes) with array indices, then one more field can't fit.
948        let mut builder = LedgerPathBuilder::for_current_ledger_obj();
949        for i in 0..16 {
950            builder = builder.index(i);
951        }
952        assert!(!builder.overflowed);
953        assert_eq!(builder.locator.num_packed_bytes(), 64);
954
955        let builder = builder.field(sfield::Flags);
956        assert!(builder.overflowed);
957        // The buffer is not grown or partially overwritten past its capacity.
958        assert_eq!(builder.locator.num_packed_bytes(), 64);
959    }
960
961    #[test]
962    fn test_ledger_index_above_i32_max_is_malformed_not_a_negative_segment() {
963        // The counterpart to the transaction-side guard: both builders share the segment encoding.
964        let mut mock = MockHostBindings::new();
965        mock.expect_le_inner().times(0);
966        let _guard = setup_mock(mock);
967
968        let builder = LedgerPathBuilder::for_ledger_obj(1)
969            .field(sfield::SignerEntries)
970            .index(i32::MAX as u32 + 1);
971
972        assert!(builder.overflowed);
973        assert_eq!(builder.locator.num_packed_bytes(), 4);
974        assert_eq!(
975            builder.get::<u32>().err().unwrap().code(),
976            host::Error::LocatorMalformed.code()
977        );
978    }
979
980    #[test]
981    fn test_ledger_current_get_reads_via_current_obj_host_fn() {
982        // A builder rooted at the current object must not reach for the slot-taking host function.
983        let mut mock = MockHostBindings::new();
984        mock.expect_home_le_inner()
985            .with(always(), eq(4usize), always(), eq(4usize))
986            .times(1)
987            .returning(|_, _, _, _| 4);
988        mock.expect_le_inner().times(0);
989        let _guard = setup_mock(mock);
990
991        let result = LedgerPathBuilder::for_current_ledger_obj()
992            .field(sfield::Flags)
993            .get::<u32>();
994
995        assert!(result.is_ok());
996    }
997
998    #[test]
999    fn test_ledger_by_slot_get_passes_slot_to_slot_host_fn() {
1000        const SLOT: i32 = 7;
1001        let mut mock = MockHostBindings::new();
1002        // Path is SignerEntries[0] -> two 4-byte segments = 8 bytes; u32 read buffer is 4.
1003        mock.expect_le_inner()
1004            .with(eq(SLOT), always(), eq(8usize), always(), eq(4usize))
1005            .times(1)
1006            .returning(|_, _, _, _, _| 4);
1007        mock.expect_home_le_inner().times(0);
1008        let _guard = setup_mock(mock);
1009
1010        let result = LedgerPathBuilder::for_ledger_obj(SLOT)
1011            .field(sfield::SignerEntries)
1012            .index(0)
1013            .get::<u32>();
1014
1015        assert!(result.is_ok());
1016    }
1017
1018    #[test]
1019    fn test_ledger_get_returns_locator_malformed_when_overflowed_without_calling_host() {
1020        // The host must not be queried for a path we know is truncated.
1021        let mut mock = MockHostBindings::new();
1022        mock.expect_le_inner().times(0);
1023        let _guard = setup_mock(mock);
1024
1025        let mut builder = LedgerPathBuilder::for_ledger_obj(1);
1026        for i in 0..17 {
1027            builder = builder.index(i);
1028        }
1029        assert!(builder.overflowed);
1030
1031        let result = builder.get::<u32>();
1032        assert!(result.is_err());
1033        assert_eq!(
1034            result.err().unwrap().code(),
1035            host::Error::LocatorMalformed.code()
1036        );
1037    }
1038
1039    #[test]
1040    fn test_ledger_get_propagates_host_error() {
1041        let mut mock = MockHostBindings::new();
1042        mock.expect_home_le_inner()
1043            .with(always(), eq(4usize), always(), eq(4usize))
1044            .times(1)
1045            .returning(|_, _, _, _| SOME_ERROR);
1046        let _guard = setup_mock(mock);
1047
1048        let result = LedgerPathBuilder::for_current_ledger_obj()
1049            .field(sfield::Flags)
1050            .get::<u32>();
1051
1052        assert!(result.is_err());
1053        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
1054    }
1055
1056    #[test]
1057    fn test_ledger_get_optional_returns_some_when_present() {
1058        const SLOT: i32 = 2;
1059        let mut mock = MockHostBindings::new();
1060        mock.expect_le_inner()
1061            .with(eq(SLOT), always(), eq(4usize), always(), eq(4usize))
1062            .times(1)
1063            .returning(|_, _, _, _, _| 4);
1064        let _guard = setup_mock(mock);
1065
1066        let result = LedgerPathBuilder::for_ledger_obj(SLOT)
1067            .field(sfield::Flags)
1068            .get_optional::<u32>();
1069
1070        assert!(result.is_ok());
1071        assert!(result.unwrap().is_some());
1072    }
1073
1074    #[test]
1075    fn test_ledger_get_optional_returns_none_on_field_not_found() {
1076        let mut mock = MockHostBindings::new();
1077        mock.expect_home_le_inner()
1078            .with(always(), eq(4usize), always(), eq(4usize))
1079            .times(1)
1080            .returning(|_, _, _, _| FIELD_NOT_FOUND);
1081        let _guard = setup_mock(mock);
1082
1083        let result = LedgerPathBuilder::for_current_ledger_obj()
1084            .field(sfield::Flags)
1085            .get_optional::<u32>();
1086
1087        assert!(result.is_ok());
1088        assert!(result.unwrap().is_none());
1089    }
1090
1091    #[test]
1092    fn test_ledger_index_past_i32_max_marks_path_malformed_without_calling_host() {
1093        // Packing `u32::MAX as i32` would encode -1, which the host would read back as a field
1094        // code. The path must be rejected instead of quietly pointing somewhere else.
1095        let mut mock = MockHostBindings::new();
1096        mock.expect_le_inner().times(0);
1097        mock.expect_le_inner_arr_len().times(0);
1098        let _guard = setup_mock(mock);
1099
1100        let builder = LedgerPathBuilder::for_ledger_obj(1)
1101            .field(sfield::SignerEntries)
1102            .index(u32::MAX);
1103
1104        assert!(builder.overflowed);
1105        // The out-of-range segment is not encoded at all, so only `SignerEntries` is packed.
1106        assert_eq!(builder.locator.num_packed_bytes(), 4);
1107        assert_eq!(
1108            builder.get::<u32>().err().unwrap().code(),
1109            host::Error::LocatorMalformed.code()
1110        );
1111        assert_eq!(
1112            builder.array_len().err().unwrap().code(),
1113            host::Error::LocatorMalformed.code()
1114        );
1115    }
1116
1117    // ---- `array_len()` terminal ----
1118
1119    #[test]
1120    fn test_array_len_current_obj_returns_count() {
1121        let mut mock = MockHostBindings::new();
1122        mock.expect_home_le_inner_arr_len()
1123            .with(always(), eq(4usize))
1124            .times(1)
1125            .returning(|_, _| 3);
1126        let _guard = setup_mock(mock);
1127
1128        let result = LedgerPathBuilder::for_current_ledger_obj()
1129            .field(sfield::SignerEntries)
1130            .array_len();
1131
1132        assert_eq!(result.unwrap(), 3);
1133    }
1134
1135    #[test]
1136    fn test_array_len_by_slot_passes_slot_and_returns_count() {
1137        const SLOT: i32 = 4;
1138        let mut mock = MockHostBindings::new();
1139        mock.expect_le_inner_arr_len()
1140            .with(eq(SLOT), always(), eq(4usize))
1141            .times(1)
1142            .returning(|_, _, _| 2);
1143        mock.expect_home_le_inner_arr_len().times(0);
1144        let _guard = setup_mock(mock);
1145
1146        let result = LedgerPathBuilder::for_ledger_obj(SLOT)
1147            .field(sfield::PriceDataSeries)
1148            .array_len();
1149
1150        assert_eq!(result.unwrap(), 2);
1151    }
1152
1153    #[test]
1154    fn test_array_len_zero_is_ok_not_an_error() {
1155        // An array that is present but empty is a legitimate answer.
1156        let mut mock = MockHostBindings::new();
1157        mock.expect_home_le_inner_arr_len()
1158            .times(1)
1159            .returning(|_, _| 0);
1160        let _guard = setup_mock(mock);
1161
1162        let result = LedgerPathBuilder::for_current_ledger_obj()
1163            .field(sfield::SignerEntries)
1164            .array_len();
1165
1166        assert_eq!(result.unwrap(), 0);
1167    }
1168
1169    #[test]
1170    fn test_array_len_propagates_host_error() {
1171        let mut mock = MockHostBindings::new();
1172        mock.expect_home_le_inner_arr_len()
1173            .times(1)
1174            .returning(|_, _| SOME_ERROR);
1175        let _guard = setup_mock(mock);
1176
1177        let result = LedgerPathBuilder::for_current_ledger_obj()
1178            .field(sfield::SignerEntries)
1179            .array_len();
1180
1181        assert_eq!(result.err().unwrap().code(), SOME_ERROR);
1182    }
1183
1184    #[test]
1185    fn test_array_len_returns_locator_malformed_when_overflowed_without_calling_host() {
1186        let mut mock = MockHostBindings::new();
1187        mock.expect_le_inner_arr_len().times(0);
1188        let _guard = setup_mock(mock);
1189
1190        let mut builder = LedgerPathBuilder::for_ledger_obj(1);
1191        for i in 0..17 {
1192            builder = builder.index(i);
1193        }
1194
1195        assert_eq!(
1196            builder.array_len().err().unwrap().code(),
1197            host::Error::LocatorMalformed.code()
1198        );
1199    }
1200
1201    #[test]
1202    fn test_array_len_then_index_walks_every_entry() {
1203        // The pattern `array_len()` exists for: count, then read each element.
1204        const SLOT: i32 = 6;
1205        let mut mock = MockHostBindings::new();
1206        mock.expect_le_inner_arr_len()
1207            .with(eq(SLOT), always(), eq(4usize))
1208            .times(1)
1209            .returning(|_, _, _| 2);
1210        // Two reads of PriceDataSeries[i].AssetPrice -> 12 bytes of path, 8-byte u64 buffer.
1211        mock.expect_le_inner()
1212            .with(eq(SLOT), always(), eq(12usize), always(), eq(8usize))
1213            .times(2)
1214            .returning(|_, _, _, _, _| 8);
1215        let _guard = setup_mock(mock);
1216
1217        let obj = LedgerPathBuilder::for_ledger_obj(SLOT);
1218        let count = obj
1219            .clone()
1220            .field(sfield::PriceDataSeries)
1221            .array_len()
1222            .unwrap();
1223        assert_eq!(count, 2);
1224        for i in 0..count {
1225            let price = obj
1226                .clone()
1227                .field(sfield::PriceDataSeries)
1228                .index(i)
1229                .field(sfield::AssetPrice)
1230                .get::<u64>();
1231            assert!(price.is_ok());
1232        }
1233    }
1234}