1use std::io::{self, BufReader, Read, Seek, SeekFrom, Write};
6
7use sha2::{Digest, Sha256};
8
9use crate::crypto::ctr;
10use crate::error::{Error, Result};
11use crate::format::nca::is_ctr;
12use crate::format::ncz::{self, BlockHeader, INCOMPRESSIBLE_HEADER_SIZE, Section};
13use crate::format::read_vec;
14
15const CHUNK: usize = 0x100000;
16
17struct BlockReader<R: Read + Seek> {
20 reader: R,
21 block_size: usize,
22 compressed_offsets: Vec<u64>,
23 compressed_sizes: Vec<u32>,
24 decompressed_size: u64,
25 position: u64,
26 cached: Option<(usize, Vec<u8>)>,
27}
28
29impl<R: Read + Seek> BlockReader<R> {
30 fn new(reader: R, bh: &BlockHeader, payload_start: u64) -> Result<BlockReader<R>> {
31 if !(14..=32).contains(&bh.block_size_exponent) {
32 return Err(Error::Corrupt(
33 "NCZBLOCK: block size exponent must be 14..=32".into(),
34 ));
35 }
36 let offsets = bh
37 .compressed_block_size_list
38 .iter()
39 .scan(payload_start, |off, &sz| {
40 let start = *off;
41 *off += sz as u64;
42 Some(start)
43 })
44 .collect();
45 Ok(BlockReader {
46 reader,
47 block_size: 1usize << bh.block_size_exponent,
48 compressed_offsets: offsets,
49 compressed_sizes: bh.compressed_block_size_list.clone(),
50 decompressed_size: bh.decompressed_size as u64,
51 position: 0,
52 cached: None,
53 })
54 }
55
56 fn decompress_block(&mut self, block_id: usize) -> io::Result<Vec<u8>> {
57 let mut decompressed_block_size = self.block_size;
58 if block_id + 1 == self.compressed_offsets.len() {
59 let remainder = (self.decompressed_size % self.block_size as u64) as usize;
60 if remainder > 0 {
61 decompressed_block_size = remainder;
62 }
63 }
64 self.reader
65 .seek(SeekFrom::Start(self.compressed_offsets[block_id]))?;
66 let csize = self.compressed_sizes[block_id] as usize;
67 if csize < decompressed_block_size {
68 let mut comp = vec![0u8; csize];
69 self.reader.read_exact(&mut comp)?;
70 zstd::bulk::decompress(&comp, decompressed_block_size)
71 } else {
72 let mut raw = vec![0u8; decompressed_block_size];
73 self.reader.read_exact(&mut raw)?;
74 Ok(raw)
75 }
76 }
77}
78
79impl<R: Read + Seek> Read for BlockReader<R> {
80 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
81 let block_id = (self.position / self.block_size as u64) as usize;
82 if self.position >= self.decompressed_size || block_id >= self.compressed_offsets.len() {
83 return Ok(0);
84 }
85 if self.cached.as_ref().map(|(id, _)| *id) != Some(block_id) {
86 self.cached = Some((block_id, self.decompress_block(block_id)?));
87 }
88 let block = &self.cached.as_ref().unwrap().1;
89 let offset = (self.position % self.block_size as u64) as usize;
90 let n = out.len().min(block.len().saturating_sub(offset));
91 out[..n].copy_from_slice(&block[offset..offset + n]);
92 self.position += n as u64;
93 Ok(n)
94 }
95}
96
97pub fn read_ncz_header<R: Read + Seek>(
102 f: &mut R,
103) -> Result<(Vec<Section>, Option<BlockHeader>, u64)> {
104 f.seek(SeekFrom::Start(INCOMPRESSIBLE_HEADER_SIZE))?;
105 let mut head = [0u8; 16];
106 f.read_exact(&mut head)?;
107 let table_len = u64::try_from(ncz::le_i64(&head, 8))
108 .ok()
109 .and_then(|n| n.checked_mul(Section::WIRE_SIZE as u64))
110 .ok_or_else(|| Error::Corrupt("bad section count".into()))?;
111 let buf = [&head[..], &read_vec(f, table_len)?].concat();
112 let (mut sections, _) = ncz::parse_header(&buf)?;
113 let block_start = INCOMPRESSIBLE_HEADER_SIZE + buf.len() as u64;
114
115 let mut fixed = [0u8; BlockHeader::FIXED_SIZE];
117 let has_block = f.read_exact(&mut fixed).is_ok() && fixed[..8] == *ncz::BLOCK_MAGIC;
118 let (block, payload_start) = if has_block {
119 let nblocks = u64::try_from(ncz::le_i32(&fixed, 12))
120 .map_err(|_| Error::Corrupt("negative block count".into()))?;
121 let bh = [&fixed[..], &read_vec(f, nblocks * 4)?].concat();
122 (Some(BlockHeader::read(&bh)?), block_start + bh.len() as u64)
123 } else {
124 (None, block_start)
125 };
126
127 if !sections.is_empty() && sections[0].offset > INCOMPRESSIBLE_HEADER_SIZE as i64 {
130 let fake = Section {
131 offset: INCOMPRESSIBLE_HEADER_SIZE as i64,
132 size: sections[0].offset - INCOMPRESSIBLE_HEADER_SIZE as i64,
133 crypto_type: 0,
134 crypto_key: [0u8; 16],
135 crypto_counter: [0u8; 16],
136 };
137 sections.insert(0, fake);
138 }
139
140 Ok((sections, block, payload_start))
141}
142
143pub fn decompress_ncz<R: Read + Seek, W: Write>(f: &mut R, out: &mut W) -> Result<(u64, String)> {
146 f.seek(SeekFrom::Start(0))?;
148 let mut header = vec![0u8; INCOMPRESSIBLE_HEADER_SIZE as usize];
149 f.read_exact(&mut header)?;
150 out.write_all(&header)?;
151 let mut hasher = Sha256::new();
152 hasher.update(&header);
153
154 let (sections, block, payload_start) = read_ncz_header(f)?;
155 f.seek(SeekFrom::Start(payload_start))?;
156 let body: Box<dyn Read + '_> = match block.as_ref() {
157 Some(bh) => Box::new(BlockReader::new(f.by_ref(), bh, payload_start)?),
158 None => Box::new(
159 zstd::stream::read::Decoder::new(BufReader::new(f.by_ref()))
160 .map_err(|e| Error::Corrupt(format!("zstd init: {e}")))?,
161 ),
162 };
163
164 let body_written = decompress_body(body, §ions, out, &mut hasher)?;
165 Ok((
166 INCOMPRESSIBLE_HEADER_SIZE + body_written,
167 hex::encode(hasher.finalize()),
168 ))
169}
170
171fn decompress_body<S: Read, W: Write>(
174 mut body: S,
175 sections: &[Section],
176 out: &mut W,
177 hasher: &mut Sha256,
178) -> Result<u64> {
179 let mut written: u64 = 0;
180 let mut chunk = vec![0u8; CHUNK];
181 for (idx, s) in sections.iter().enumerate() {
182 let mut i = s.offset;
183 let end = s.offset.saturating_add(s.size);
184 if idx == 0 {
185 i = i.max(INCOMPRESSIBLE_HEADER_SIZE as i64);
187 }
188 while i < end {
189 let want = std::cmp::min(CHUNK as i64, end - i) as usize;
190 let buf = &mut chunk[..want];
191 body.read_exact(buf).map_err(|e| match e.kind() {
192 io::ErrorKind::UnexpectedEof => {
193 Error::Corrupt(format!("NCZ truncated: body ends before offset {i:#x}"))
194 }
195 _ => Error::Io(e),
196 })?;
197 if is_ctr(s.crypto_type) {
198 ctr::keystream_xor(&s.crypto_key, &s.crypto_counter, i as u64, buf);
199 }
200 out.write_all(buf)?;
201 hasher.update(&*buf);
202 written += want as u64;
203 i += want as i64;
204 }
205 }
206 Ok(written)
207}