use std::{
borrow::Cow,
fmt::{Display, Formatter},
ops::ControlFlow,
};
use crate::{Error, VarType};
const VARIABLE_STORE_SIGNATURE: &[u8; 4] = b"3VVN";
const VARIABLE_STORE_VERSION: u8 = 0x1;
const VARIABLE_DATA: u16 = 0x55AA;
const PARTITION_SIZE: usize = 0x10000;
const STORE_HEADER_SIZE: usize = 24;
const VAR_HEADER_SIZE: usize = 36;
const VAR_ADDED: u8 = 0x7F;
const VAR_IN_DELETED_TRANSITION: u8 = 0xFE;
const VAR_DELETED: u8 = 0xFD;
const APPLE_COMMON_VARIABLE_GUID: &[u8; 16] = &[
0x7C, 0x43, 0x61, 0x10, 0xAB, 0x2A, 0x4B, 0xBB, 0xA8, 0x80, 0xFE, 0x41, 0x99, 0x5C, 0x9F, 0x82,
];
const APPLE_SYSTEM_VARIABLE_GUID: &[u8; 16] = &[
0x40, 0xA0, 0xDD, 0xD2, 0x77, 0xF8, 0x43, 0x92, 0xB4, 0xA3, 0x1E, 0x73, 0x04, 0x20, 0x65, 0x16,
];
#[derive(Debug, Default)]
enum Slot<T> {
Valid(T),
Invalid,
#[default]
Empty,
}
impl<T> Slot<T> {
pub const fn as_ref(&self) -> Slot<&T> {
match self {
Slot::Valid(v) => Slot::Valid(v),
Slot::Invalid => Slot::Invalid,
Slot::Empty => Slot::Empty,
}
}
pub fn as_mut(&mut self) -> Slot<&mut T> {
match self {
Slot::Valid(v) => Slot::Valid(v),
Slot::Invalid => Slot::Invalid,
Slot::Empty => Slot::Empty,
}
}
pub fn unwrap(self) -> T {
match self {
Slot::Valid(v) => v,
Slot::Invalid => panic!("called `Slot::unwrap()` on an `Invalid` value"),
Slot::Empty => panic!("called `Slot::unwrap()` on an `Empty` value"),
}
}
pub fn empty(&self) -> bool {
match self {
Slot::Empty => true,
_ => false,
}
}
}
#[derive(Debug)]
pub struct Nvram<'a> {
partitions: [Slot<Partition<'a>>; 16],
partition_count: usize,
active: usize,
}
impl<'a> Nvram<'a> {
pub fn parse(nvr: &'a [u8]) -> crate::Result<Nvram<'_>> {
let partition_count = nvr.len() / PARTITION_SIZE;
let mut partitions: [Slot<Partition<'a>>; 16] = Default::default();
let mut active = 0;
let mut max_gen = 0;
let mut valid_partitions = 0;
for i in 0..partition_count {
let offset = i * PARTITION_SIZE;
if offset >= nvr.len() {
break;
}
match Partition::parse(&nvr[offset..offset + PARTITION_SIZE]) {
Ok(p) => {
let p_gen = p.generation();
if p_gen > max_gen {
active = i;
max_gen = p_gen;
}
partitions[i] = Slot::Valid(p);
valid_partitions += 1;
}
Err(V3Error::Empty) => {
partitions[i] = Slot::Empty;
}
Err(_) => {
partitions[i] = Slot::Invalid;
}
}
}
if valid_partitions == 0 {
return Err(Error::ParseError);
}
Ok(Nvram {
partitions,
partition_count,
active,
})
}
fn partitions(&self) -> impl Iterator<Item = &Partition<'a>> {
self.partitions
.iter()
.take(self.partition_count)
.filter_map(|x| match x {
Slot::Valid(p) => Some(p),
Slot::Invalid => None,
Slot::Empty => None,
})
}
fn active_part(&self) -> &Partition<'a> {
self.partitions[self.active].as_ref().unwrap()
}
#[cfg(test)]
fn active_part_mut(&mut self) -> &mut Partition<'a> {
self.partitions[self.active].as_mut().unwrap()
}
}
impl<'a> crate::Nvram<'a> for Nvram<'a> {
fn serialize(&self) -> crate::Result<Vec<u8>> {
let mut v = Vec::with_capacity(self.partition_count * PARTITION_SIZE);
for p in self.partitions() {
p.serialize(&mut v);
}
Ok(v)
}
fn prepare_for_write(&mut self) {
}
fn partitions(&self) -> Box<dyn Iterator<Item = &dyn crate::Partition<'a>> + '_> {
Box::new(self.partitions().map(|p| p as &dyn crate::Partition<'a>))
}
fn active_part_mut(&mut self) -> &mut dyn crate::Partition<'a> {
self.partitions[self.active].as_mut().unwrap()
}
fn apply(&mut self, w: &mut dyn crate::NvramWriter) -> crate::Result<()> {
let ap = self.active_part();
let offset;
if ap.system_used() > ap.system_size() {
return Err(Error::SectionTooBig);
}
if ap.common_used() > ap.common_size() {
return Err(Error::SectionTooBig);
}
if ap.total_used() <= ap.usable_size() {
offset = (self.active * PARTITION_SIZE) as u32;
} else {
let new_active = (self.active + 1) % self.partition_count;
offset = (new_active * PARTITION_SIZE) as u32;
if !self.partitions[new_active].empty() {
w.erase_if_needed(offset, PARTITION_SIZE);
}
self.partitions[new_active] = Slot::Valid(
self.partitions[self.active]
.as_ref()
.unwrap()
.clone_active(),
);
self.active = new_active;
if self.active_part().total_used() > PARTITION_SIZE {
return Err(Error::SectionTooBig);
}
}
let mut data = Vec::with_capacity(PARTITION_SIZE);
self.active_part().serialize(&mut data);
w.write_all(offset, &data)
.map_err(|e| Error::ApplyError(e))?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Partition<'a> {
pub header: StoreHeader<'a>,
pub values: Vec<Variable<'a>>,
empty_region_end: usize,
}
#[derive(Debug)]
enum V3Error {
ParseError,
Empty,
}
type Result<T> = std::result::Result<T, V3Error>;
impl<'a> Partition<'a> {
fn parse(nvr: &'a [u8]) -> Result<Partition<'a>> {
if let Ok(header) = StoreHeader::parse(&nvr[..STORE_HEADER_SIZE]) {
let mut offset = STORE_HEADER_SIZE;
let mut values = Vec::new();
let mut empty_region_end = header.size();
while offset + VAR_HEADER_SIZE < header.size() {
let mut empty = true;
for i in 0..VAR_HEADER_SIZE {
if nvr[offset + i] != 0 && nvr[offset + i] != 0xFF {
empty = false;
break;
}
}
if empty {
offset += VAR_HEADER_SIZE;
while offset < header.size() {
if nvr[offset] != 0xFF {
empty_region_end = offset;
break;
}
offset += 1
}
break;
}
let Ok(v_header) = VarHeader::parse(&nvr[offset..]) else {
empty_region_end = offset;
break;
};
let k_begin = offset + VAR_HEADER_SIZE;
let k_end = k_begin + v_header.name_size as usize;
let key = &nvr[k_begin..k_end - 1];
let v_begin = k_end;
let v_end = v_begin + v_header.data_size as usize;
let value = &nvr[v_begin..v_end];
let crc = crc32fast::hash(value);
if crc != v_header.crc {
return Err(V3Error::ParseError);
}
let v = Variable {
header: v_header,
key,
value: Cow::Borrowed(value),
};
offset += v.size();
values.push(v);
}
Ok(Partition {
header,
values,
empty_region_end,
})
} else {
match nvr.iter().copied().try_for_each(|v| match v {
0xFF => ControlFlow::Continue(()),
_ => ControlFlow::Break(()),
}) {
ControlFlow::Continue(_) => Err(V3Error::Empty),
ControlFlow::Break(_) => Err(V3Error::ParseError),
}
}
}
fn generation(&self) -> u32 {
self.header.generation
}
fn entries(&mut self, key: &'a [u8], typ: VarType) -> impl Iterator<Item = &mut Variable<'a>> {
self.values
.iter_mut()
.filter(move |e| e.key == key && e.typ() == typ)
}
fn entries_added(
&mut self,
key: &'a [u8],
typ: VarType,
) -> impl Iterator<Item = &mut Variable<'a>> {
self.entries(key, typ)
.filter(|v| v.header.state == VAR_ADDED)
}
fn total_used(&self) -> usize {
STORE_HEADER_SIZE + self.values.iter().fold(0, |acc, v| acc + v.size())
}
fn system_used(&self) -> usize {
self.values
.iter()
.filter(|&v| v.header.state == VAR_ADDED && v.header.guid == APPLE_SYSTEM_VARIABLE_GUID)
.fold(0, |acc, v| acc + v.size())
}
fn common_used(&self) -> usize {
self.values
.iter()
.filter(|&v| v.header.state == VAR_ADDED && v.header.guid == APPLE_COMMON_VARIABLE_GUID)
.fold(0, |acc, v| acc + v.size())
}
fn system_size(&self) -> usize {
self.header.system_size as usize
}
fn common_size(&self) -> usize {
self.header.common_size as usize
}
fn usable_size(&self) -> usize {
self.empty_region_end
}
fn serialize(&self, v: &mut Vec<u8>) {
let start_size = v.len();
self.header.serialize(v);
for var in &self.values {
var.serialize(v);
}
let my_size = v.len() - start_size;
debug_assert!(v.len() == self.total_used());
for _ in 0..(self.header.size() - my_size) {
v.push(0xFF);
}
}
fn variables(&self) -> impl Iterator<Item = &Variable<'a>> {
self.values.iter().filter(|v| v.header.state == VAR_ADDED)
}
fn clone_active(&self) -> Partition<'a> {
let mut header = self.header.clone();
header.generation += 1;
Partition {
header,
values: self
.values
.iter()
.filter_map(|v| {
if v.header.state == VAR_ADDED {
Some(v.clone())
} else {
None
}
})
.collect(),
empty_region_end: self.header.size(),
}
}
}
impl<'a> crate::Partition<'a> for Partition<'a> {
fn get_variable(&self, key: &[u8], typ: VarType) -> Option<&dyn crate::Variable<'a>> {
self.values.iter().find_map(|e| {
if e.key == key && e.typ() == typ && e.header.state == VAR_ADDED {
Some(e as &dyn crate::Variable<'a>)
} else {
None
}
})
}
fn insert_variable(&mut self, key: &'a [u8], value: Cow<'a, [u8]>, typ: VarType) {
for var in self.entries_added(key, typ) {
var.header.state = var.header.state & VAR_DELETED & VAR_IN_DELETED_TRANSITION;
}
let guid = match typ {
VarType::Common => APPLE_COMMON_VARIABLE_GUID,
VarType::System => APPLE_SYSTEM_VARIABLE_GUID,
};
let var = Variable {
header: VarHeader {
state: VAR_ADDED,
attrs: 0,
name_size: (key.len() + 1) as u32,
data_size: value.len() as u32,
guid,
crc: crc32fast::hash(&value),
},
key,
value,
};
self.values.push(var);
}
fn remove_variable(&mut self, key: &'a [u8], typ: VarType) {
for var in self.entries_added(key, typ) {
var.header.state = var.header.state & VAR_DELETED & VAR_IN_DELETED_TRANSITION;
}
}
fn variables(&self) -> Box<dyn Iterator<Item = &dyn crate::Variable<'a>> + '_> {
Box::new(self.variables().map(|e| e as &dyn crate::Variable<'a>))
}
}
impl Display for Partition<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"size: {}, total_used: {}, system_used: {}, common_used: {}, generation: 0x{:02x}, state: 0x{:02x}, flags: 0x{:02x}, count: {}",
self.header.size,
self.total_used(),
self.system_used(),
self.common_used(),
self.generation(),
self.header.state,
self.header.flags,
self.values.len()
)
}
}
#[derive(Debug, Clone)]
pub struct StoreHeader<'a> {
pub name: &'a [u8],
pub size: u32,
pub generation: u32,
pub state: u8,
pub flags: u8,
pub version: u8,
pub system_size: u32,
pub common_size: u32,
}
impl<'a> StoreHeader<'a> {
fn parse(nvr: &[u8]) -> Result<StoreHeader<'_>> {
let name = &nvr[..4];
let size = u32::from_le_bytes(nvr[4..8].try_into().unwrap());
let generation = u32::from_le_bytes(nvr[8..12].try_into().unwrap());
let state = nvr[12];
let flags = nvr[13];
let version = nvr[14];
let system_size = u32::from_le_bytes(nvr[16..20].try_into().unwrap());
let common_size = u32::from_le_bytes(nvr[20..24].try_into().unwrap());
if name != VARIABLE_STORE_SIGNATURE {
return Err(V3Error::ParseError);
}
if version != VARIABLE_STORE_VERSION {
return Err(V3Error::ParseError);
}
Ok(StoreHeader {
name,
size,
generation,
state,
flags,
version,
system_size,
common_size,
})
}
fn serialize(&self, v: &mut Vec<u8>) {
v.extend_from_slice(VARIABLE_STORE_SIGNATURE);
v.extend_from_slice(&self.size.to_le_bytes());
v.extend_from_slice(&self.generation.to_le_bytes());
v.push(self.state);
v.push(self.flags);
v.push(self.version);
v.push(0); v.extend_from_slice(&self.system_size.to_le_bytes());
v.extend_from_slice(&self.common_size.to_le_bytes());
}
fn size(&self) -> usize {
self.size as usize
}
}
#[derive(Debug, Default, Clone)]
pub struct Variable<'a> {
pub header: VarHeader<'a>,
pub key: &'a [u8],
pub value: Cow<'a, [u8]>,
}
impl<'a> Variable<'a> {
fn size(&self) -> usize {
VAR_HEADER_SIZE + (self.header.name_size + self.header.data_size) as usize
}
fn typ(&self) -> VarType {
if self.header.guid == APPLE_SYSTEM_VARIABLE_GUID {
return VarType::System;
}
VarType::Common
}
fn serialize(&self, v: &mut Vec<u8>) {
self.header.serialize(v);
v.extend_from_slice(self.key);
v.push(0);
v.extend_from_slice(&self.value);
}
}
impl<'a> crate::Variable<'a> for Variable<'a> {
fn value(&self) -> Cow<'a, [u8]> {
self.value.clone()
}
}
impl Display for Variable<'_> {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let key = String::from_utf8_lossy(self.key);
let mut value = String::new();
for c in self.value.iter().copied() {
if (c as char).is_ascii() && !(c as char).is_ascii_control() {
value.push(c as char);
} else {
value.push_str(&format!("%{c:02x}"));
}
}
write!(f, "{}:{}={}", self.typ(), key, value)
}
}
#[derive(Debug, Default, Clone)]
pub struct VarHeader<'a> {
pub state: u8,
pub attrs: u32,
pub name_size: u32,
pub data_size: u32,
pub guid: &'a [u8],
pub crc: u32,
}
impl<'a> VarHeader<'a> {
fn parse(nvr: &[u8]) -> Result<VarHeader<'_>> {
let start_id = u16::from_le_bytes(nvr[..2].try_into().unwrap());
if start_id != VARIABLE_DATA {
return Err(V3Error::ParseError);
}
let state = nvr[2];
let attrs = u32::from_le_bytes(nvr[4..8].try_into().unwrap());
let name_size = u32::from_le_bytes(nvr[8..12].try_into().unwrap());
let data_size = u32::from_le_bytes(nvr[12..16].try_into().unwrap());
let guid = &nvr[16..32];
let crc = u32::from_le_bytes(nvr[32..36].try_into().unwrap());
if VAR_HEADER_SIZE + (name_size + data_size) as usize > nvr.len() {
return Err(V3Error::ParseError);
}
Ok(VarHeader {
state,
attrs,
name_size,
data_size,
guid,
crc,
})
}
fn serialize(&self, v: &mut Vec<u8>) {
v.extend_from_slice(&VARIABLE_DATA.to_le_bytes());
v.push(self.state);
v.push(0); v.extend_from_slice(&self.attrs.to_le_bytes());
v.extend_from_slice(&self.name_size.to_le_bytes());
v.extend_from_slice(&self.data_size.to_le_bytes());
v.extend_from_slice(self.guid);
v.extend_from_slice(&self.crc.to_le_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Nvram as NvramT, NvramWriter, Partition};
struct TestNvram {
data: Vec<u8>,
erase_count: usize,
}
impl TestNvram {
fn new(data: Vec<u8>) -> TestNvram {
Self {
data,
erase_count: 0,
}
}
fn get_data(&self) -> &[u8] {
&self.data
}
}
impl NvramWriter for TestNvram {
fn erase_if_needed(&mut self, offset: u32, size: usize) {
for b in self.data.iter_mut().skip(offset as usize).take(size) {
*b = 0xFF;
}
self.erase_count += 1;
}
fn write_all(&mut self, offset: u32, buf: &[u8]) -> std::io::Result<()> {
for (d, s) in self
.data
.iter_mut()
.skip(offset as usize)
.take(buf.len())
.zip(buf.iter().copied())
{
*d &= s;
}
Ok(())
}
}
#[rustfmt::skip]
fn store_header() -> &'static [u8] {
&[
0x33, 0x56, 0x56, 0x4e, 0x00, 0x00, 0x01, 0x00,
0x01, 0x00, 0x00, 0x00, 0xfe, 0x5a, 0x01, 0x00,
0x00, 0x40, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00,
]
}
fn empty_nvram(bank_count: usize) -> Vec<u8> {
let mut data = vec![0xFF; PARTITION_SIZE * bank_count];
data[0..STORE_HEADER_SIZE].copy_from_slice(store_header());
data
}
#[test]
fn test_insert_variable() -> crate::Result<()> {
let mut nvr = TestNvram::new(empty_nvram(2));
let data = nvr.get_data().to_owned();
let mut nv = Nvram::parse(&data)?;
assert!(matches!(nv.partitions[0], Slot::Valid(_)));
assert!(matches!(nv.partitions[1], Slot::Empty));
nv.active_part_mut().insert_variable(
b"test-variable",
Cow::Borrowed(b"test-value"),
VarType::Common,
);
nv.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 0);
let data_after = nvr.get_data().to_owned();
let mut nv_after = Nvram::parse(&data_after)?;
let test_var = nv_after
.active_part()
.get_variable(b"test-variable", VarType::Common)
.unwrap();
assert_eq!(test_var.value(), Cow::Borrowed(b"test-value"));
let test_var_entries: Vec<_> = nv_after
.active_part_mut()
.entries(b"test-variable", VarType::Common)
.collect();
assert_eq!(test_var_entries.len(), 1);
assert_eq!(test_var_entries[0].header.state, VAR_ADDED);
nv_after.active_part_mut().insert_variable(
b"test-variable",
Cow::Borrowed(b"test-value2"),
VarType::Common,
);
nv_after.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 0);
let data_after2 = nvr.get_data().to_owned();
let mut nv_after2 = Nvram::parse(&data_after2)?;
let test_var2 = nv_after2
.active_part()
.get_variable(b"test-variable", VarType::Common)
.unwrap();
assert_eq!(test_var2.value(), Cow::Borrowed(b"test-value2"));
let test_var2_entries: Vec<_> = nv_after2
.active_part_mut()
.entries(b"test-variable", VarType::Common)
.collect();
assert_eq!(test_var2_entries.len(), 2);
assert_eq!(
test_var2_entries[0].header.state,
VAR_ADDED & VAR_DELETED & VAR_IN_DELETED_TRANSITION
);
assert_eq!(test_var2_entries[1].header.state, VAR_ADDED);
Ok(())
}
#[test]
fn test_write_to_next_bank() -> crate::Result<()> {
let mut nvr = TestNvram::new(empty_nvram(2));
nvr.data[0x10000..0x10007].copy_from_slice(b"garbage");
let data = nvr.get_data().to_owned();
let mut nv = Nvram::parse(&data)?;
assert!(matches!(nv.partitions[0], Slot::Valid(_)));
assert!(matches!(nv.partitions[1], Slot::Invalid));
let orig_sys_val = vec![b'.'; 8192];
nv.active_part_mut().insert_variable(
b"test-large-variable",
Cow::Borrowed(&orig_sys_val),
VarType::System,
);
let orig_common_val = vec![b'.'; 24576];
nv.active_part_mut().insert_variable(
b"test-large-variable",
Cow::Borrowed(&orig_common_val),
VarType::Common,
);
nv.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 0);
assert_eq!(nv.active, 0);
let data_after = nvr.get_data().to_owned();
let mut nv_after = Nvram::parse(&data_after)?;
assert_eq!(nv_after.active_part().header.generation, 1);
assert!(matches!(nv_after.partitions[0], Slot::Valid(_)));
assert!(matches!(nv_after.partitions[1], Slot::Invalid));
let updated_sys_val = vec![b'.'; 9000];
nv_after.active_part_mut().insert_variable(
b"test-large-variable",
Cow::Borrowed(&updated_sys_val),
VarType::System,
);
let updated_common_val = vec![b'.'; 25000];
nv_after.active_part_mut().insert_variable(
b"test-large-variable",
Cow::Borrowed(&updated_common_val),
VarType::Common,
);
assert_eq!(nv_after.active_part().values.len(), 4);
nv_after.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 1);
assert_eq!(nv_after.active, 1);
assert_eq!(nv_after.active_part().values.len(), 2);
let data_after2 = nvr.get_data().to_owned();
let nv_after2 = Nvram::parse(&data_after2)?;
assert_eq!(nv_after2.active, 1);
assert_eq!(nv_after2.active_part().values.len(), 2);
assert_eq!(nv_after2.active_part().header.generation, 2);
assert!(matches!(nv_after2.partitions[0], Slot::Valid(_)));
assert!(matches!(nv_after2.partitions[1], Slot::Valid(_)));
let test_sys_var2 = nv_after2
.active_part()
.get_variable(b"test-large-variable", VarType::System)
.unwrap();
let test_common_var2 = nv_after2
.active_part()
.get_variable(b"test-large-variable", VarType::Common)
.unwrap();
assert_eq!(test_sys_var2.value(), Cow::Borrowed(&updated_sys_val));
assert_eq!(test_common_var2.value(), Cow::Borrowed(&updated_common_val));
let test_old_sys_var = nv_after2.partitions[0]
.as_ref()
.unwrap()
.get_variable(b"test-large-variable", VarType::System)
.unwrap();
let test_old_common_var = nv_after2.partitions[0]
.as_ref()
.unwrap()
.get_variable(b"test-large-variable", VarType::Common)
.unwrap();
assert_eq!(test_old_sys_var.value(), Cow::Borrowed(&orig_sys_val));
assert_eq!(test_old_common_var.value(), Cow::Borrowed(&orig_common_val));
Ok(())
}
#[test]
fn test_insert_with_low_space() -> crate::Result<()> {
let mut nvr = TestNvram::new(empty_nvram(2));
nvr.data[STORE_HEADER_SIZE + 100] = 0x42;
let data = nvr.get_data().to_owned();
let mut nv = Nvram::parse(&data)?;
assert!(matches!(nv.partitions[0], Slot::Valid(_)));
assert!(matches!(nv.partitions[1], Slot::Empty));
nv.active_part_mut().insert_variable(
b"test-variable",
Cow::Borrowed(b"test-value"),
VarType::Common,
);
nv.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 0);
assert_eq!(nv.active, 0);
let data_after = nvr.get_data().to_owned();
let mut nv_after = Nvram::parse(&data_after)?;
let test_var = nv_after
.active_part()
.get_variable(b"test-variable", VarType::Common)
.unwrap();
assert_eq!(test_var.value(), Cow::Borrowed(b"test-value"));
let test_var_entries: Vec<_> = nv_after
.active_part_mut()
.entries(b"test-variable", VarType::Common)
.collect();
assert_eq!(test_var_entries.len(), 1);
assert_eq!(test_var_entries[0].header.state, VAR_ADDED);
nv_after.active_part_mut().insert_variable(
b"test-variable",
Cow::Borrowed(b"test-value2"),
VarType::Common,
);
nv_after.apply(&mut nvr)?;
assert_eq!(nvr.erase_count, 0);
assert_eq!(nv_after.active, 1);
Ok(())
}
}