use crc::Crc;
use log::*;
use std::collections::BTreeMap;
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{Cursor, Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
use std::path::Path;
use crate::disk;
use crate::partition;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Header {
pub signature: String, pub revision: u32, pub header_size_le: u32, pub crc32: u32, pub reserved: u32, pub current_lba: u64, pub backup_lba: u64, pub first_usable: u64, pub last_usable: u64, pub disk_guid: uuid::Uuid, pub part_start: u64, pub num_parts: u32, pub part_size: u32, pub crc32_parts: u32, }
impl Header {
pub(crate) fn compute_new(
primary: bool,
pp: &BTreeMap<u32, partition::Partition>,
guid: uuid::Uuid,
backup_offset: u64,
original_header: &Option<Header>,
lb_size: disk::LogicalBlockSize,
num_parts: Option<u32>,
) -> Result<Self> {
let (cur, bak) = if primary {
(1, backup_offset)
} else {
(backup_offset, 1)
};
let parts = match num_parts {
Some(p) => {p}
None => {
match original_header {
Some(header) => header.num_parts,
None => (pp.iter().filter(|p| p.1.is_used()).count() as u32).max(128),
}
}
};
let part_size = match original_header {
Some(header) => header.part_size,
None => 128,
};
let part_array_num_bytes = u64::from(parts * part_size);
let lb_size_u64 = Into::<u64>::into(lb_size);
let part_array_num_lbs = (part_array_num_bytes + (lb_size_u64 - 1)) / lb_size_u64;
let first = match num_parts {
Some(_) => 1 + 1 + part_array_num_lbs,
None => {
match original_header {
Some(header) => header.first_usable,
None => 1 + 1 + part_array_num_lbs, }
}
};
let last = match num_parts {
Some(_) => {
backup_offset
.checked_sub(part_array_num_lbs + 1)
.ok_or_else(|| Error::new(ErrorKind::Other, "header underflow - last usable"))?
},
None => {
match original_header {
Some(header) => header.last_usable,
None => {
backup_offset
.checked_sub(part_array_num_lbs + 1)
.ok_or_else(|| Error::new(ErrorKind::Other, "header underflow - last usable"))?
}
}
}
};
let part_start = if primary { 2 } else { last + 1 };
let hdr = Header {
signature: "EFI PART".to_string(),
revision: 65536,
header_size_le: 92,
crc32: 0,
reserved: 0,
current_lba: cur,
backup_lba: bak,
first_usable: first,
last_usable: last,
disk_guid: guid,
part_start,
num_parts: parts,
part_size,
crc32_parts: 0,
};
Ok(hdr)
}
pub fn write_primary<D: Read + Write + Seek>(
&self,
file: &mut D,
lb_size: disk::LogicalBlockSize,
) -> Result<usize> {
if self.current_lba >= self.backup_lba {
debug!(
"current lba: {} backup_lba: {}",
self.current_lba, self.backup_lba
);
return Err(Error::new(
ErrorKind::Other,
"primary header does not start before backup one",
));
}
self.file_write_header(file, self.current_lba, lb_size)
}
pub fn write_backup<D: Read + Write + Seek>(
&self,
file: &mut D,
lb_size: disk::LogicalBlockSize,
) -> Result<usize> {
if self.current_lba <= self.backup_lba {
debug!(
"current lba: {} backup_lba: {}",
self.current_lba, self.backup_lba
);
return Err(Error::new(
ErrorKind::Other,
"backup header does not start after primary one",
));
}
self.file_write_header(file, self.current_lba, lb_size)
}
fn file_write_header<D: Read + Write + Seek>(
&self,
file: &mut D,
lba: u64,
lb_size: disk::LogicalBlockSize,
) -> Result<usize> {
let parts_checksum = partentry_checksum(file, self, lb_size)?;
trace!("computed partitions CRC32: {:#x}", parts_checksum);
let bytes = self.as_bytes(None, Some(parts_checksum))?;
trace!("bytes before checksum: {:?}", bytes);
let checksum = calculate_crc32(&bytes);
trace!("computed header CRC32: {:#x}", checksum);
let start = lba
.checked_mul(lb_size.into())
.ok_or_else(|| Error::new(ErrorKind::Other, "header overflow - offset"))?;
trace!("Seeking to {}", start);
let _ = file.seek(SeekFrom::Start(start))?;
let mut header_bytes = self.as_bytes(Some(checksum), Some(parts_checksum))?;
header_bytes.resize(Into::<usize>::into(lb_size), 0x00);
let len = file.write(&header_bytes)?;
trace!("Wrote {} bytes", len);
Ok(len)
}
fn as_bytes(
&self,
header_checksum: Option<u32>,
partitions_checksum: Option<u32>,
) -> Result<Vec<u8>> {
let mut buff: Vec<u8> = Vec::new();
let disk_guid_fields = self.disk_guid.as_fields();
buff.write_all(self.signature.as_bytes())?;
buff.write_all(&self.revision.to_le_bytes())?;
buff.write_all(&self.header_size_le.to_le_bytes())?;
match header_checksum {
Some(c) => buff.write_all(&c.to_le_bytes())?,
None => buff.write_all(&[0_u8; 4])?,
};
buff.write_all(&[0; 4])?;
buff.write_all(&self.current_lba.to_le_bytes())?;
buff.write_all(&self.backup_lba.to_le_bytes())?;
buff.write_all(&self.first_usable.to_le_bytes())?;
buff.write_all(&self.last_usable.to_le_bytes())?;
buff.write_all(&disk_guid_fields.0.to_le_bytes())?;
buff.write_all(&disk_guid_fields.1.to_le_bytes())?;
buff.write_all(&disk_guid_fields.2.to_le_bytes())?;
buff.write_all(disk_guid_fields.3)?;
buff.write_all(&self.part_start.to_le_bytes())?;
buff.write_all(&self.num_parts.to_le_bytes())?;
buff.write_all(&self.part_size.to_le_bytes())?;
match partitions_checksum {
Some(c) => buff.write_all(&c.to_le_bytes())?,
None => buff.write_all(&[0_u8; 4])?,
};
Ok(buff)
}
}
pub fn parse_uuid(rdr: &mut Cursor<&[u8]>) -> Result<uuid::Uuid> {
let d1 = u32::from_le_bytes(read_exact_buff!(d1b, rdr, 4));
let d2 = u16::from_le_bytes(read_exact_buff!(d2b, rdr, 2));
let d3 = u16::from_le_bytes(read_exact_buff!(d3b, rdr, 2));
let d4 = read_exact_buff!(d4b, rdr, 8);
let uuid = uuid::Uuid::from_fields(
d1,
d2,
d3,
&d4,
);
Ok(uuid)
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Disk:\t\t{}\nCRC32:\t\t{}\nTable CRC:\t{}",
self.disk_guid, self.crc32, self.crc32_parts
)
}
}
pub fn read_header(
path: impl AsRef<Path>,
sector_size: disk::LogicalBlockSize
) -> Result<Header> {
let mut file = File::open(path)?;
read_primary_header(&mut file, sector_size)
}
pub fn read_header_from_arbitrary_device<D: Read + Seek>(
device: &mut D,
sector_size: disk::LogicalBlockSize,
) -> Result<Header> {
read_primary_header(device, sector_size)
}
pub(crate) fn read_primary_header<D: Read + Seek>(
file: &mut D,
sector_size: disk::LogicalBlockSize,
) -> Result<Header> {
let cur = file.seek(SeekFrom::Current(0)).unwrap_or(0);
let offset: u64 = sector_size.into();
let res = file_read_header(file, offset);
let _ = file.seek(SeekFrom::Start(cur));
res
}
pub(crate) fn read_backup_header<D: Read + Seek>(
file: &mut D,
sector_size: disk::LogicalBlockSize,
) -> Result<Header> {
let cur = file.seek(SeekFrom::Current(0)).unwrap_or(0);
let h2sect = find_backup_lba(file, sector_size)?;
let offset = h2sect
.checked_mul(sector_size.into())
.ok_or_else(|| Error::new(ErrorKind::Other, "backup header overflow - offset"))?;
let res = file_read_header(file, offset);
let _ = file.seek(SeekFrom::Start(cur));
res
}
pub(crate) fn file_read_header<D: Read + Seek>(file: &mut D, offset: u64) -> Result<Header> {
let _ = file.seek(SeekFrom::Start(offset));
let mut hdr: [u8; 92] = [0; 92];
let _ = file.read_exact(&mut hdr);
let mut reader = Cursor::new(&hdr[..]);
let sigstr = String::from_utf8_lossy(
&reader.get_ref()[reader.position() as usize..reader.position() as usize + 8],
);
reader.seek(SeekFrom::Current(8))?;
if sigstr != "EFI PART" {
return Err(Error::new(ErrorKind::Other, "invalid GPT signature"));
};
let h = Header {
signature: sigstr.to_string(),
revision: u32::from_le_bytes(read_exact_buff!(rev, reader, 4)),
header_size_le: u32::from_le_bytes(read_exact_buff!(hsle, reader, 4)),
crc32: u32::from_le_bytes(read_exact_buff!(crc32, reader, 4)),
reserved: u32::from_le_bytes(read_exact_buff!(reserv, reader, 4)),
current_lba: u64::from_le_bytes(read_exact_buff!(clba, reader, 8)),
backup_lba: u64::from_le_bytes(read_exact_buff!(blba, reader, 8)),
first_usable: u64::from_le_bytes(read_exact_buff!(fusable, reader, 8)),
last_usable: u64::from_le_bytes(read_exact_buff!(lusable, reader, 8)),
disk_guid: parse_uuid(&mut reader)?,
part_start: u64::from_le_bytes(read_exact_buff!(pstart, reader, 8)),
num_parts: u32::from_le_bytes(read_exact_buff!(nparts, reader, 4)),
part_size: u32::from_le_bytes(read_exact_buff!(partsize, reader, 4)),
crc32_parts: u32::from_le_bytes(read_exact_buff!(crc32parts, reader, 4)),
};
trace!("header: {:?}", &hdr[..]);
trace!("header gpt: {}", h.disk_guid.as_hyphenated().to_string());
let mut hdr_crc = hdr;
for crc_byte in hdr_crc.iter_mut().skip(16).take(4) {
*crc_byte = 0;
}
let c = calculate_crc32(&hdr_crc);
trace!("header CRC32: {:#x} - computed CRC32: {:#x}", h.crc32, c);
if c == h.crc32 {
Ok(h)
} else {
Err(Error::new(ErrorKind::Other, "invalid CRC32 checksum"))
}
}
pub(crate) fn find_backup_lba<D: Read + Seek>(
f: &mut D,
sector_size: disk::LogicalBlockSize,
) -> Result<u64> {
trace!("querying file size to find backup header location");
let lb_size: u64 = sector_size.into();
let old_pos = f.seek(std::io::SeekFrom::Current(0))?;
let len = f.seek(std::io::SeekFrom::End(0))?;
f.seek(std::io::SeekFrom::Start(old_pos))?;
if len <= lb_size {
return Err(Error::new(
ErrorKind::Other,
"disk image too small for backup header",
));
}
let bak_offset = len.saturating_sub(lb_size);
let bak_lba = bak_offset / lb_size;
trace!(
"backup header: LBA={}, bytes offset={}",
bak_lba,
bak_offset
);
Ok(bak_lba)
}
const CRC_32: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
fn calculate_crc32(b: &[u8]) -> u32 {
let mut digest = CRC_32.digest();
trace!("Writing buffer to digest calculator");
digest.update(b);
digest.finalize()
}
pub(crate) fn partentry_checksum<D: Read + Seek>(
file: &mut D,
hdr: &Header,
lb_size: disk::LogicalBlockSize,
) -> Result<u32> {
trace!("Computing partition checksum");
let start = hdr
.part_start
.checked_mul(lb_size.into())
.ok_or_else(|| Error::new(ErrorKind::Other, "header overflow - partition table start"))?;
trace!("Seek to {}", start);
let _ = file.seek(SeekFrom::Start(start))?;
let pt_len = u64::from(hdr.num_parts)
.checked_mul(hdr.part_size.into())
.ok_or_else(|| Error::new(ErrorKind::Other, "partition table - size"))?;
trace!("Reading {} bytes", pt_len);
let mut buf = vec![0; pt_len as usize];
file.read_exact(&mut buf)?;
Ok(calculate_crc32(&buf))
}
pub fn write_header(
p: impl AsRef<Path>,
uuid: Option<uuid::Uuid>,
sector_size: disk::LogicalBlockSize,
) -> Result<uuid::Uuid> {
debug!("opening {} for writing", p.as_ref().display());
let mut file = OpenOptions::new().write(true).read(true).open(p)?;
let bak = find_backup_lba(&mut file, sector_size)?;
let guid = match uuid {
Some(u) => u,
None => {
let u = uuid::Uuid::new_v4();
debug!("Generated random uuid: {}", u);
u
}
};
let hdr = Header::compute_new(true, &BTreeMap::new(), guid, bak, &None, sector_size, None)?;
debug!("new header: {:#?}", hdr);
hdr.write_primary(&mut file, sector_size)?;
Ok(guid)
}
#[test]
fn test_compute_new_fdisk_no_header() {
use tempfile;
let lb_size = disk::DEFAULT_SECTOR_SIZE;
let diskpath = Path::new("tests/fixtures/test.img");
let h = read_header(diskpath, lb_size).unwrap();
let cfg = crate::GptConfig::new().writable(false).initialized(true);
let disk = cfg.open(diskpath).unwrap();
println!("original Disk {:#?}", disk);
let partitions: BTreeMap<u32, partition::Partition> = BTreeMap::new();
let mut file = std::fs::OpenOptions::new()
.write(false)
.read(true)
.open(diskpath)
.unwrap();
let bak = find_backup_lba(&mut file, *disk.logical_block_size()).unwrap();
println!("Back offset {}", bak);
let mut tempdisk = tempfile::tempfile().expect("failed to create tempfile disk");
{
let data: [u8; 4096] = [0; 4096];
println!("Creating blank header file for testing");
let min_file_size = (bak * Into::<u64>::into(lb_size)) + Into::<u64>::into(lb_size);
for _ in 0..((min_file_size + 4095) / 4096) {
tempdisk.write_all(&data).unwrap();
}
};
let new_primary =
Header::compute_new(true, &partitions, uuid::Uuid::new_v4(), bak, &None, lb_size, None).unwrap();
println!("new primary header {:#?}", new_primary);
let new_backup =
Header::compute_new(false, &partitions, uuid::Uuid::new_v4(), bak, &None, lb_size, None).unwrap();
println!("new backup header {:#?}", new_backup);
new_primary
.write_primary(&mut tempdisk, lb_size)
.unwrap();
new_backup
.write_backup(&mut tempdisk, lb_size)
.unwrap();
let mbr = crate::mbr::ProtectiveMBR::new();
mbr.overwrite_lba0(&mut tempdisk).unwrap();
assert_eq!(h.signature, new_primary.signature);
assert_eq!(h.revision, new_primary.revision);
assert_eq!(h.header_size_le, new_primary.header_size_le);
assert_eq!(h.reserved, new_primary.reserved);
assert_eq!(h.current_lba, new_primary.current_lba);
assert_eq!(h.backup_lba, new_primary.backup_lba);
assert_eq!(34, new_primary.first_usable); assert_eq!(h.last_usable, new_primary.last_usable);
assert_ne!(h.disk_guid, new_primary.disk_guid); assert_eq!(2, new_primary.part_start);
assert_eq!(128, new_primary.num_parts);
assert_eq!(128, new_primary.part_size); let bh = read_backup_header(&mut file, *disk.logical_block_size()).unwrap();
assert_eq!(h.backup_lba, new_backup.current_lba);
assert_eq!(h.current_lba, new_backup.backup_lba);
assert_eq!(bh.current_lba, new_backup.current_lba);
assert_eq!(bh.backup_lba, new_backup.backup_lba);
assert_eq!(bh.part_start, new_backup.part_start);
}
#[test]
fn test_compute_new_fdisk_pass_header() {
let diskpath = Path::new("tests/fixtures/test.img");
let h = read_header(diskpath, disk::DEFAULT_SECTOR_SIZE).unwrap();
let cfg = crate::GptConfig::new().writable(false).initialized(true);
let disk = cfg.open(diskpath).unwrap();
println!("original Disk {:#?}", disk);
let partitions: BTreeMap<u32, partition::Partition> = BTreeMap::new();
let mut file = std::fs::OpenOptions::new()
.write(false)
.read(true)
.open(diskpath)
.unwrap();
let bak = find_backup_lba(&mut file, *disk.logical_block_size()).unwrap();
println!("Back offset {}", bak);
let mut tempdisk = tempfile::tempfile().expect("failed to create tempfile disk");
{
let data: [u8; 4096] = [0; 4096];
println!("Creating copy of test header file for testing");
for _ in 0..2560 {
tempdisk.write_all(&data).unwrap();
}
};
let bh = read_backup_header(&mut file, *disk.logical_block_size()).unwrap();
let mbr = crate::mbr::ProtectiveMBR::new();
mbr.overwrite_lba0(&mut tempdisk).unwrap();
let new_primary = Header::compute_new(
true,
&partitions,
uuid::Uuid::new_v4(),
bak,
&Some(h.clone()),
disk::DEFAULT_SECTOR_SIZE,
None,
)
.unwrap();
println!("new primary header {:#?}", new_primary);
let new_backup = Header::compute_new(
false,
&partitions,
uuid::Uuid::new_v4(),
bak,
&Some(h.clone()),
disk::DEFAULT_SECTOR_SIZE,
None,
)
.unwrap();
println!("new backup header {:#?}", new_backup);
new_primary
.write_primary(&mut tempdisk, disk::DEFAULT_SECTOR_SIZE)
.unwrap();
new_backup
.write_backup(&mut tempdisk, disk::DEFAULT_SECTOR_SIZE)
.unwrap();
assert_eq!(h.signature, new_primary.signature);
assert_eq!(h.revision, new_primary.revision);
assert_eq!(h.header_size_le, new_primary.header_size_le);
assert_eq!(h.reserved, new_primary.reserved);
assert_eq!(h.current_lba, new_primary.current_lba);
assert_eq!(h.backup_lba, new_primary.backup_lba);
assert_eq!(h.first_usable, new_primary.first_usable); assert_eq!(h.last_usable, new_primary.last_usable);
assert_ne!(h.disk_guid, new_primary.disk_guid); assert_eq!(2, new_primary.part_start);
assert_eq!(h.num_parts, new_primary.num_parts);
assert_eq!(h.part_size, new_primary.part_size); assert_eq!(h.backup_lba, new_backup.current_lba);
assert_eq!(h.current_lba, new_backup.backup_lba);
assert_eq!(bh.current_lba, new_backup.current_lba);
assert_eq!(bh.backup_lba, new_backup.backup_lba);
assert_eq!(bh.part_start, new_backup.part_start);
}
#[test]
fn test_compute_new_gpt_no_header() {
use tempfile;
let lb_size = disk::DEFAULT_SECTOR_SIZE;
let diskpath = Path::new("tests/fixtures/gpt-linux-disk-01.img");
let h = read_header(diskpath, lb_size).unwrap();
let cfg = crate::GptConfig::new().writable(false).initialized(true);
let disk = cfg.open(diskpath).unwrap();
println!("original Disk {:#?}", disk);
let partitions: BTreeMap<u32, partition::Partition> = BTreeMap::new();
let mut file = std::fs::OpenOptions::new()
.write(false)
.read(true)
.open(diskpath)
.unwrap();
let bak = find_backup_lba(&mut file, *disk.logical_block_size()).unwrap();
println!("Back offset {}", bak);
let mut tempdisk = tempfile::tempfile().expect("failed to create tempfile disk");
{
let data: [u8; 4096] = [0; 4096];
println!("Creating blank header file for testing");
for _ in 0..100 {
tempdisk.write_all(&data).unwrap();
}
};
let new_primary =
Header::compute_new(true, &partitions, uuid::Uuid::new_v4(), bak, &None, lb_size, None).unwrap();
println!("new primary header {:#?}", new_primary);
let new_backup =
Header::compute_new(false, &partitions, uuid::Uuid::new_v4(), bak, &None, lb_size, None).unwrap();
println!("new backup header {:#?}", new_backup);
new_primary
.write_primary(&mut tempdisk, lb_size)
.unwrap();
new_backup
.write_backup(&mut tempdisk, lb_size)
.unwrap();
let mbr = crate::mbr::ProtectiveMBR::new();
mbr.overwrite_lba0(&mut tempdisk).unwrap();
assert_eq!(h.signature, new_primary.signature);
assert_eq!(h.revision, new_primary.revision);
assert_eq!(h.header_size_le, new_primary.header_size_le);
assert_eq!(h.reserved, new_primary.reserved);
assert_eq!(h.current_lba, new_primary.current_lba);
assert_eq!(h.backup_lba, new_primary.backup_lba);
assert_eq!(34, new_primary.first_usable); assert_eq!(h.last_usable, new_primary.last_usable);
assert_ne!(h.disk_guid, new_primary.disk_guid); assert_eq!(2, new_primary.part_start);
assert_eq!(128, new_primary.num_parts);
assert_eq!(128, new_primary.part_size); let bh = read_backup_header(&mut file, *disk.logical_block_size()).unwrap();
assert_eq!(h.backup_lba, new_backup.current_lba);
assert_eq!(h.current_lba, new_backup.backup_lba);
assert_eq!(bh.current_lba, new_backup.current_lba);
assert_eq!(bh.backup_lba, new_backup.backup_lba);
assert_eq!(bh.part_start, new_backup.part_start);
}
#[test]
fn test_compute_new_fdisk_gpt_header() {
let diskpath = Path::new("tests/fixtures/gpt-linux-disk-01.img");
let h = read_header(diskpath, disk::DEFAULT_SECTOR_SIZE).unwrap();
let cfg = crate::GptConfig::new().writable(false).initialized(true);
let disk = cfg.open(diskpath).unwrap();
println!("original Disk {:#?}", disk);
let partitions: BTreeMap<u32, partition::Partition> = BTreeMap::new();
let mut file = std::fs::OpenOptions::new()
.write(false)
.read(true)
.open(diskpath)
.unwrap();
let bak = find_backup_lba(&mut file, *disk.logical_block_size()).unwrap();
println!("Back offset {}", bak);
let mut tempdisk = tempfile::tempfile().expect("failed to create tempfile disk");
{
let data: [u8; 4096] = [0; 4096];
println!("Creating copy of test header file for testing");
for _ in 0..2560 {
tempdisk.write_all(&data).unwrap();
}
};
let bh = read_backup_header(&mut file, *disk.logical_block_size()).unwrap();
let mbr = crate::mbr::ProtectiveMBR::new();
mbr.overwrite_lba0(&mut tempdisk).unwrap();
let new_primary = Header::compute_new(
true,
&partitions,
uuid::Uuid::new_v4(),
bak,
&Some(h.clone()),
disk::DEFAULT_SECTOR_SIZE,
None,
)
.unwrap();
println!("new primary header {:#?}", new_primary);
let new_backup = Header::compute_new(
false,
&partitions,
uuid::Uuid::new_v4(),
bak,
&Some(h.clone()),
disk::DEFAULT_SECTOR_SIZE,
None,
)
.unwrap();
println!("new backup header {:#?}", new_backup);
new_primary
.write_primary(&mut tempdisk, disk::DEFAULT_SECTOR_SIZE)
.unwrap();
new_backup
.write_backup(&mut tempdisk, disk::DEFAULT_SECTOR_SIZE)
.unwrap();
assert_eq!(h.signature, new_primary.signature);
assert_eq!(h.revision, new_primary.revision);
assert_eq!(h.header_size_le, new_primary.header_size_le);
assert_eq!(h.reserved, new_primary.reserved);
assert_eq!(h.current_lba, new_primary.current_lba);
assert_eq!(h.backup_lba, new_primary.backup_lba);
assert_eq!(h.first_usable, new_primary.first_usable); assert_eq!(h.last_usable, new_primary.last_usable);
assert_ne!(h.disk_guid, new_primary.disk_guid); assert_eq!(2, new_primary.part_start);
assert_eq!(h.num_parts, new_primary.num_parts);
assert_eq!(h.part_size, new_primary.part_size); assert_eq!(h.backup_lba, new_backup.current_lba);
assert_eq!(h.current_lba, new_backup.backup_lba);
assert_eq!(bh.current_lba, new_backup.current_lba);
assert_eq!(bh.backup_lba, new_backup.backup_lba);
assert_eq!(bh.part_start, new_backup.part_start);
}