diff --git a/Cargo.toml b/Cargo.toml index 8ae0372..40e2406 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amd-apcb" -version = "0.3.3" +version = "0.4.0" authors = ["Oxide Computer"] edition = "2021" @@ -12,14 +12,8 @@ byteorder = { version = "1.4.3", default-features = false } four-cc = { version = "0.3.0", default-features = false } memoffset = "0.5" modular-bitfield = { version = "0.11.2", default-features = false } -# -# In order to use macros as discriminants in enums that make use of derive -# macros (e.g., AsBytes, FromPrimitive), we need the syn crate to have "full" -# enabled. The easiest way to do this is to use num-derive's "full-syntax", -# which passes "full" through to syn. -# -num-derive = { version = "0.3.0", features = [ "full-syntax" ] } -num-traits = { version = "0.2.12", default-features = false } +num-derive = { version = "0.4.2", features = [ ] } +num-traits = { version = "0.2.19", default-features = false } paste = "1.0" pre = { version = "0.2.1", default-features = false, features = [] } static_assertions = "1.1.0" diff --git a/rust-toolchain b/rust-toolchain index beefec5..48c31e1 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,3 +1,3 @@ [toolchain] channel = "nightly-2024-06-19" -components = [ "rustfmt", "rust-src" ] +components = [ "rustfmt", "rust-src", "rust-analyzer" ] diff --git a/src/apcb.rs b/src/apcb.rs index 3b53b2b..7f67a13 100644 --- a/src/apcb.rs +++ b/src/apcb.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::types::{Error, FileSystemError, PtrMut, Result}; +use crate::types::{ApcbContext, Error, FileSystemError, PtrMut, Result}; use crate::entry::EntryItemBody; use crate::group::{GroupItem, GroupMutItem}; @@ -50,11 +50,12 @@ use std::borrow::Cow; #[derive(Clone)] pub struct ApcbIoOptions { pub check_checksum: bool, + pub context: ApcbContext, } impl Default for ApcbIoOptions { fn default() -> Self { - Self { check_checksum: true } + Self { check_checksum: true, context: ApcbContext::default() } } } @@ -62,10 +63,20 @@ impl ApcbIoOptions { pub fn builder() -> Self { Self::default() } + pub fn check_checksum(&self) -> bool { + self.check_checksum + } + pub fn context(&self) -> ApcbContext { + self.context + } pub fn with_check_checksum(&mut self, value: bool) -> &mut Self { self.check_checksum = value; self } + pub fn with_context(&mut self, value: ApcbContext) -> &mut Self { + self.context = value; + self + } pub fn build(&self) -> Self { self.clone() } @@ -73,6 +84,7 @@ impl ApcbIoOptions { #[cfg_attr(feature = "std", derive(Clone))] pub struct Apcb<'a> { + context: ApcbContext, used_size: usize, pub backing_store: PtrMut<'a, [u8]>, } @@ -83,6 +95,11 @@ pub struct Apcb<'a> { #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SerdeApcb { + /// This field is out-of-band information. At the cost of slight redundancy + /// in user config and another extra field that isn't actually in the blob, + /// we can actually handle the out-of-band information quite natually. + #[cfg_attr(feature = "serde", serde(default))] + pub context: ApcbContext, pub version: String, pub header: V2_HEADER, pub v3_header_ext: Option, @@ -110,11 +127,18 @@ impl<'a> schemars::JsonSchema for Apcb<'a> { use core::convert::TryFrom; #[cfg(feature = "serde")] -impl<'a> TryFrom for Apcb<'a> { - type Error = Error; +impl<'a> Apcb<'a> { + pub fn context(&self) -> ApcbContext { + self.context + } + // type Error = Error; fn try_from(serde_apcb: SerdeApcb) -> Result { let buf = Cow::from(vec![0xFFu8; Self::MAX_SIZE]); - let mut apcb = Apcb::create(buf, 42, &ApcbIoOptions::default())?; + let mut apcb = Apcb::create( + buf, + 42, + &ApcbIoOptions::default().with_context(serde_apcb.context).build(), + )?; *apcb.header_mut()? = serde_apcb.header; match serde_apcb.v3_header_ext { Some(v3) => { @@ -204,6 +228,7 @@ impl<'a> Serialize for Apcb<'a> { } } +/// Note: Caller should probably verify sanity of context() afterwards #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for Apcb<'_> { fn deserialize(deserializer: D) -> core::result::Result @@ -217,11 +242,13 @@ impl<'de> Deserialize<'de> for Apcb<'_> { } pub struct ApcbIterMut<'a> { + context: ApcbContext, buf: &'a mut [u8], remaining_used_size: usize, } pub struct ApcbIter<'a> { + context: ApcbContext, buf: &'a [u8], remaining_used_size: usize, } @@ -230,7 +257,10 @@ impl<'a> ApcbIterMut<'a> { /// It's useful to have some way of NOT mutating self.buf. This is what /// this function does. Note: The caller needs to manually decrease /// remaining_used_size for each call if desired. - fn next_item<'b>(buf: &mut &'b mut [u8]) -> Result> { + fn next_item<'b>( + context: ApcbContext, + buf: &mut &'b mut [u8], + ) -> Result> { if buf.is_empty() { return Err(Error::FileSystem( FileSystemError::InconsistentHeader, @@ -256,7 +286,7 @@ impl<'a> ApcbIterMut<'a> { ))?; let body_len = body.len(); - Ok(GroupMutItem { header, buf: body, used_size: body_len }) + Ok(GroupMutItem { context, header, buf: body, used_size: body_len }) } /// Moves the point to the group with the given GROUP_ID. Returns (offset, @@ -273,12 +303,12 @@ impl<'a> ApcbIterMut<'a> { if buf.is_empty() { break; } - let group = ApcbIterMut::next_item(&mut buf)?; + let group = ApcbIterMut::next_item(self.context, &mut buf)?; let group_size = group.header.group_size.get(); if group.header.group_id.get() == group_id { return Ok((offset, group_size as usize)); } - let group = ApcbIterMut::next_item(&mut self.buf)?; + let group = ApcbIterMut::next_item(self.context, &mut self.buf)?; let group_size = group.header.group_size.get() as usize; offset = offset .checked_add(group_size) @@ -295,7 +325,7 @@ impl<'a> ApcbIterMut<'a> { pub(crate) fn next1(&mut self) -> Result> { assert!(self.remaining_used_size != 0, "Internal error"); - let item = Self::next_item(&mut self.buf)?; + let item = Self::next_item(self.context, &mut self.buf)?; let group_size = item.header.group_size.get() as usize; if group_size <= self.remaining_used_size { self.remaining_used_size -= group_size; @@ -324,7 +354,10 @@ impl<'a> ApcbIter<'a> { /// It's useful to have some way of NOT mutating self.buf. This is what /// this function does. Note: The caller needs to manually decrease /// remaining_used_size for each call if desired. - fn next_item<'b>(buf: &mut &'b [u8]) -> Result> { + fn next_item<'b>( + context: ApcbContext, + buf: &mut &'b [u8], + ) -> Result> { if buf.is_empty() { return Err(Error::FileSystem( FileSystemError::InconsistentHeader, @@ -351,12 +384,12 @@ impl<'a> ApcbIter<'a> { let body_len = body.len(); - Ok(GroupItem { header, buf: body, used_size: body_len }) + Ok(GroupItem { context, header, buf: body, used_size: body_len }) } pub(crate) fn next1(&mut self) -> Result> { assert!(self.remaining_used_size != 0, "Internal error"); - let item = Self::next_item(&mut self.buf)?; + let item = Self::next_item(self.context, &mut self.buf)?; let group_size = item.header.group_size.get() as usize; if group_size <= self.remaining_used_size { self.remaining_used_size -= group_size; @@ -400,7 +433,7 @@ impl<'a> Apcb<'a> { const ROME_VERSION: u16 = 0x30; const V3_HEADER_EXT_SIZE: usize = size_of::() + size_of::(); - pub const MAX_SIZE: usize = 0x6500; + pub const MAX_SIZE: usize = 0x8000; pub fn header(&self) -> Result> { LayoutVerified::<&[u8], V2_HEADER>::new_unaligned_from_prefix( @@ -509,6 +542,7 @@ impl<'a> Apcb<'a> { pub fn groups(&self) -> Result> { Ok(ApcbIter { + context: self.context, buf: self.beginning_of_groups()?, remaining_used_size: self.used_size, }) @@ -529,6 +563,7 @@ impl<'a> Apcb<'a> { pub fn groups_mut(&mut self) -> Result> { let used_size = self.used_size; Ok(ApcbIterMut { + context: self.context, buf: &mut *self.beginning_of_groups_mut()?, remaining_used_size: used_size, }) @@ -1030,6 +1065,7 @@ impl<'a> Apcb<'a> { signature: [u8; 4], ) -> Result> { // TODO: insert sorted. + let context = self.context; if !match group_id { GroupId::Psp => signature == *b"PSPG", @@ -1090,7 +1126,7 @@ impl<'a> Apcb<'a> { ))?; let body_len = body.len(); - Ok(GroupMutItem { header, buf: body, used_size: body_len }) + Ok(GroupMutItem { context, header, buf: body, used_size: body_len }) } pub(crate) fn calculate_checksum( @@ -1263,7 +1299,8 @@ impl<'a> Apcb<'a> { )); } } - let result = Self { backing_store: bs, used_size }; + let result = + Self { context: options.context(), backing_store: bs, used_size }; match result.groups()?.validate() { Ok(_) => {} diff --git a/src/entry.rs b/src/entry.rs index 8eae855..ca8b48b 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -12,7 +12,9 @@ use crate::ondisk::{ }; use crate::ondisk::{Parameters, ParametersIter}; use crate::tokens_entry::TokensEntryBodyItem; -use crate::types::{Error, FileSystemError, Result}; +use crate::types::{ + ApcbContext, Error, FileSystemError, MemDfeSearchVersion, Result, +}; use core::marker::PhantomData; use core::mem::size_of; use num_traits::FromPrimitive; @@ -46,6 +48,7 @@ impl<'a> EntryItemBody<&'a mut [u8]> { pub(crate) fn from_slice( header: &ENTRY_HEADER, b: &'a mut [u8], + context: ApcbContext, ) -> Result> { let context_type = ContextType::from_u8(header.context_type).ok_or( Error::FileSystem( @@ -66,7 +69,7 @@ impl<'a> EntryItemBody<&'a mut [u8]> { ContextType::Tokens => { let used_size = b.len(); Ok(Self::Tokens(TokensEntryBodyItem::<&mut [u8]>::new( - header, b, used_size, + header, b, used_size, context, )?)) } ContextType::Parameters => Err(Error::EntryTypeMismatch), @@ -78,6 +81,7 @@ impl<'a> EntryItemBody<&'a [u8]> { pub(crate) fn from_slice( header: &ENTRY_HEADER, b: &'a [u8], + context: ApcbContext, ) -> Result> { let context_type = ContextType::from_u8(header.context_type).ok_or( Error::FileSystem( @@ -98,7 +102,7 @@ impl<'a> EntryItemBody<&'a [u8]> { ContextType::Tokens => { let used_size = b.len(); Ok(Self::Tokens(TokensEntryBodyItem::<&[u8]>::new( - header, b, used_size, + header, b, used_size, context, )?)) } ContextType::Parameters => Err(Error::EntryTypeMismatch), @@ -117,6 +121,7 @@ impl<'a> EntryItemBody<&'a [u8]> { #[derive(Debug)] pub struct EntryMutItem<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a mut ENTRY_HEADER, pub body: EntryItemBody<&'a mut [u8]>, } @@ -420,6 +425,7 @@ use std::fmt; #[derive(Clone)] pub struct EntryItem<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a ENTRY_HEADER, pub body: EntryItemBody<&'a [u8]>, } @@ -677,12 +683,6 @@ impl<'a> Serialize for EntryItem<'a> { } else if let Some(s) = self.body_as_struct_array::() { let v = s.iter().collect::>(); state.serialize_field("Ddr5CaPinMapElement", &v)?; -// } else if let Some(s) = self.body_as_struct_array::() { // UH OH -// let v = s.iter().collect::>(); -// state.serialize_field("MemDfeSearchElement32", &v)?; - } else if let Some(s) = self.body_as_struct_array::() { - let v = s.iter().collect::>(); - state.serialize_field("MemDfeSearchElement36", &v)?; } else if let Some(s) = self.body_as_struct_array::() { let v = s.iter().collect::>(); state.serialize_field("DdrDqPinMapElement", &v)?; @@ -747,7 +747,27 @@ self.body_as_struct_sequence::>() { let v = parameters.collect::>(); state.serialize_field("parameters", &v)?; } else { - state.serialize_field("struct_body", &buf)?; + match self.context.mem_dfe_search_version() { + Some(MemDfeSearchVersion::Genoa2) => { + if let Some(s) = self.body_as_struct_array::() { + let v = s.iter().collect::>(); + state.serialize_field("MemDfeSearchElement32", &v)?; + } else { + state.serialize_field("struct_body", &buf)?; + } + }, + Some(MemDfeSearchVersion::Turin1) => { + if let Some(s) = self.body_as_struct_array::() { + let v = s.iter().collect::>(); + state.serialize_field("MemDfeSearchElement36", &v)?; + } else { + state.serialize_field("struct_body", &buf)?; + } + } + _ => { + state.serialize_field("struct_body", &buf)?; + } + } } } } @@ -1223,12 +1243,22 @@ impl<'de> Deserialize<'de> for SerdeEntryItem { )?; } Field::MemDfeSearchElement32 => { + // TODO: maybe also sanity-check + // context.mem_dfe_search_version() + // Note: context.mem_dfe_search_version is optional. + // If it's not specified, it deserialization should + // still work. struct_vec_to_body::< memory::MemDfeSearchElement32, V, >(&mut body, &mut map)?; } Field::MemDfeSearchElement36 => { + // TODO: maybe also sanity-check + // context.mem_dfe_search_version() + // Note: context.mem_dfe_search_version is optional. + // If it's not specified, it deserialization should + // still work. struct_vec_to_body::< memory::MemDfeSearchElement36, V, diff --git a/src/group.rs b/src/group.rs index 888312f..e56ce85 100644 --- a/src/group.rs +++ b/src/group.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::types::{Error, FileSystemError, Result}; +use crate::types::{ApcbContext, Error, FileSystemError, Result}; use crate::entry::{EntryItem, EntryItemBody, EntryMutItem}; use crate::ondisk::GroupId; @@ -25,6 +25,7 @@ use pre::pre; #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct GroupItem<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a GROUP_HEADER, #[cfg_attr(feature = "serde", serde(skip))] pub(crate) buf: &'a [u8], @@ -60,6 +61,7 @@ impl<'a> schemars::JsonSchema for GroupItem<'a> { #[derive(Debug)] pub struct GroupIter<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a GROUP_HEADER, buf: &'a [u8], remaining_used_size: usize, @@ -82,7 +84,10 @@ impl<'a> GroupIter<'a> { /// It's useful to have some way of NOT mutating self.buf. This is what /// this function does. Note: The caller needs to manually decrease /// remaining_used_size for each call if desired. - fn next_item<'b>(buf: &mut &'b [u8]) -> Result> { + fn next_item<'b>( + context: ApcbContext, + buf: &mut &'b [u8], + ) -> Result> { if buf.is_empty() { return Err(Error::FileSystem( FileSystemError::InconsistentHeader, @@ -125,16 +130,16 @@ impl<'a> GroupIter<'a> { } }; - let body = EntryItemBody::<&[u8]>::from_slice(header, body)?; + let body = EntryItemBody::<&[u8]>::from_slice(header, body, context)?; - Ok(EntryItem { header, body }) + Ok(EntryItem { context, header, body }) } pub(crate) fn next1(&mut self) -> Result> { if self.remaining_used_size == 0 { panic!("Internal error"); } - match Self::next_item(&mut self.buf) { + match Self::next_item(self.context, &mut self.buf) { Ok(e) => { if e.header.group_id.get() == self.header.group_id.get() { } else { @@ -219,6 +224,7 @@ impl GroupItem<'_> { pub fn entries(&self) -> GroupIter<'_> { GroupIter { + context: self.context, header: self.header, buf: self.buf, remaining_used_size: self.used_size, @@ -241,6 +247,7 @@ impl core::fmt::Debug for GroupItem<'_> { #[derive(Debug)] pub struct GroupMutIter<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a mut GROUP_HEADER, buf: &'a mut [u8], remaining_used_size: usize, @@ -250,7 +257,10 @@ impl<'a> GroupMutIter<'a> { /// It's useful to have some way of NOT mutating self.buf. This is what /// this function does. Note: The caller needs to manually decrease /// remaining_used_size for each call if desired. - fn next_item<'b>(buf: &mut &'b mut [u8]) -> Result> { + fn next_item<'b>( + context: ApcbContext, + buf: &mut &'b mut [u8], + ) -> Result> { if buf.is_empty() { return Err(Error::FileSystem( FileSystemError::InconsistentHeader, @@ -289,8 +299,9 @@ impl<'a> GroupMutIter<'a> { } }; - let body = EntryItemBody::<&mut [u8]>::from_slice(header, body)?; - Ok(EntryMutItem { header, body }) + let body = + EntryItemBody::<&mut [u8]>::from_slice(header, body, context)?; + Ok(EntryMutItem { context, header, body }) } /// Find the place BEFORE which the entry (GROUP_ID, ENTRY_ID, INSTANCE_ID, @@ -307,7 +318,7 @@ impl<'a> GroupMutIter<'a> { if buf.is_empty() { break; } - match Self::next_item(&mut buf) { + match Self::next_item(self.context, &mut buf) { Ok(e) => { if ( e.group_id(), @@ -355,7 +366,7 @@ impl<'a> GroupMutIter<'a> { if buf.is_empty() { return Err(Error::EntryNotFound); } - match Self::next_item(&mut buf) { + match Self::next_item(self.context, &mut buf) { Ok(e) => { let entry_size = e.header.entry_size.get(); if (e.id(), e.instance_id(), e.board_instance_mask()) @@ -487,6 +498,7 @@ impl<'a> GroupMutIter<'a> { #[derive(Debug)] pub struct GroupMutItem<'a> { + pub(crate) context: ApcbContext, pub(crate) header: &'a mut GROUP_HEADER, pub(crate) buf: &'a mut [u8], pub(crate) used_size: usize, @@ -688,6 +700,7 @@ impl<'a> GroupMutItem<'a> { pub fn entries(&self) -> GroupIter<'_> { GroupIter { + context: self.context, header: self.header, buf: self.buf, remaining_used_size: self.used_size, @@ -696,6 +709,7 @@ impl<'a> GroupMutItem<'a> { pub fn entries_mut(&mut self) -> GroupMutIter<'_> { GroupMutIter { + context: self.context, header: self.header, buf: self.buf, remaining_used_size: self.used_size, @@ -710,7 +724,7 @@ impl<'a> Iterator for GroupMutIter<'a> { if self.remaining_used_size == 0 { return None; } - match Self::next_item(&mut self.buf) { + match Self::next_item(self.context, &mut self.buf) { Ok(e) => { assert!(e.header.group_id.get() == self.header.group_id.get()); let entry_size = e.header.entry_size.get() as usize; diff --git a/src/lib.rs b/src/lib.rs index 0ce7bf5..a7b6d65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -293,7 +293,9 @@ pub use apcb::Apcb; pub use apcb::ApcbIoOptions; pub use entry::EntryItemBody; pub use ondisk::*; +pub use types::ApcbContext; pub use types::Error; pub use types::FileSystemError; +pub use types::MemDfeSearchVersion; pub use types::PriorityLevel; pub use types::Result; diff --git a/src/ondisk.rs b/src/ondisk.rs index e94321e..b208deb 100644 --- a/src/ondisk.rs +++ b/src/ondisk.rs @@ -7,6 +7,7 @@ //#![feature(trace_macros)] trace_macros!(true); #![allow(non_snake_case)] +#![allow(clippy::new_without_default)] pub use crate::naples::{ParameterTimePoint, ParameterTokenConfig}; use crate::struct_accessors::{make_accessors, Getter, Setter}; diff --git a/src/tokens_entry.rs b/src/tokens_entry.rs index 5dbc534..506e3f9 100644 --- a/src/tokens_entry.rs +++ b/src/tokens_entry.rs @@ -7,7 +7,7 @@ use crate::ondisk::{ ByteToken, ContextFormat, DwordToken, TokenEntryId, WordToken, ENTRY_HEADER, TOKEN_ENTRY, }; -use crate::types::{Error, FileSystemError, Result}; +use crate::types::{ApcbContext, Error, FileSystemError, Result}; use core::convert::TryFrom; use core::mem::size_of; use num_traits::FromPrimitive; @@ -27,6 +27,7 @@ pub struct TokensEntryBodyItem { entry_id: u16, buf: BufferType, used_size: usize, + context: ApcbContext, } // Note: Only construct those if unit_size, key_pos and key_size are correct! @@ -42,6 +43,7 @@ impl TokensEntryBodyItem { header: &ENTRY_HEADER, buf: BufferType, used_size: usize, + context: ApcbContext, ) -> Result { Ok(Self { unit_size: header.unit_size, @@ -51,6 +53,7 @@ impl TokensEntryBodyItem { entry_id: header.entry_id.get(), buf, used_size, + context, }) } pub(crate) fn prepare_iter(&self) -> Result { @@ -522,7 +525,7 @@ impl<'a> TokensEntryIter<&'a [u8]> { } } /// Validates the entries (recursively). Also consumes iterator. - pub(crate) fn validate(mut self) -> Result<()> { + pub(crate) fn validate(mut self, context: ApcbContext) -> Result<()> { let context_format = ContextFormat::from_u8(self.context_format) .ok_or(Error::FileSystem( FileSystemError::InconsistentHeader, @@ -584,7 +587,7 @@ impl TokensEntryBodyItem { (self.iter().ok()?).find(|entry| entry.id() == token_id) } pub fn validate(&self) -> Result<()> { - self.iter()?.validate() + self.iter()?.validate(self.context) } } diff --git a/src/types.rs b/src/types.rs index 62048c8..e2a2b35 100644 --- a/src/types.rs +++ b/src/types.rs @@ -59,6 +59,8 @@ pub enum Error { // Errors used only for Serde #[cfg_attr(feature = "std", error("entry not extractable"))] EntryNotExtractable, + #[cfg_attr(feature = "std", error("context mismatch"))] + ContextMismatch, } pub type Result = core::result::Result; @@ -83,3 +85,50 @@ pub(crate) type PtrMut<'a, T> = Cow<'a, T>; #[cfg(not(feature = "std"))] pub(crate) type PtrMut<'a, T> = &'a mut T; + +// Note: The integer is 0x100 * MemDfeSearchElement.header_size + 0x10000 * +// MemDfeSearchElement.payload_size + 0x1000000 * +// MemDfeSearchElement.payload_ext_size +// and is mostly for debugging +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[non_exhaustive] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] // FIXME: Remove Copy? +pub enum MemDfeSearchVersion { + /// At least Genoa 1.0.0.? or lower. + Genoa1 = 0x000c08, + /// At least Genoa 1.0.0.8 or higher. + Genoa2 = 0x0c0c08, + /// At least Raphael AM5 1.7.0 or higher. + /// At least Granite Ridge AM5 1.7.0 or higher. + // At least Fire Range 0.0.6.0 or higher. + Turin1 = 0x0c0c0c, +} + +#[derive(Copy, Clone, Debug, Default)] // TODO: Remove Copy? +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ApcbContext { + #[cfg_attr(feature = "serde", serde(default))] + mem_dfe_search_version: Option, +} + +impl ApcbContext { + pub fn builder() -> Self { + Self::default() + } + pub fn mem_dfe_search_version(&self) -> Option { + self.mem_dfe_search_version + } + pub fn with_mem_dfe_search_version( + &mut self, + value: Option, + ) -> &mut Self { + self.mem_dfe_search_version = value; + self + } + pub fn build(&self) -> Self { + *self + } +}