1use crate::{
14 errors::{CatBridgeError, FSError, NetworkError},
15 fsemul::errors::FSEmulAPIError,
16};
17use bytes::{Bytes, BytesMut};
18use fnv::{FnvHashSet, FnvHasher};
19use reqwest::{Client, ClientBuilder, Url};
20use sachet::{
21 common::{CafeContentFileInformation, CafeContentFilesystemTree},
22 content::decrypt,
23 title::{
24 TitleID,
25 key_generation::title_key_guesses,
26 metadata::{TitleMetadata, contents::ContentRecord},
27 ticket::Ticket,
28 },
29};
30use std::{
31 hash::{Hash, Hasher},
32 path::{Path, PathBuf},
33};
34use tracing::{debug, error, info, warn};
35use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
36
37#[derive(Clone, Debug)]
40pub struct NUSFuse {
41 base_url: Url,
45 client: Client,
47 nus_hash: String,
50 root_directory: PathBuf,
52 title_ids: FnvHashSet<TitleID>,
54}
55
56impl NUSFuse {
57 pub fn new(
63 nus_url: &str,
64 root_directory: PathBuf,
65 title_ids: FnvHashSet<TitleID>,
66 ) -> Result<Self, FSEmulAPIError> {
67 let client = ClientBuilder::new()
68 .user_agent(concat!(
69 env!("CARGO_PKG_NAME"),
70 "/",
71 env!("CARGO_PKG_VERSION"),
72 ))
73 .brotli(true)
74 .deflate(true)
75 .gzip(true)
76 .zstd(true)
77 .build()?;
78 let base_url = Url::parse(nus_url)?;
79 let nus_hash = {
80 let mut hasher = FnvHasher::with_key(0x69420);
81 nus_url.hash(&mut hasher);
82 format!("{:016x}", hasher.finish())
83 };
84
85 Ok(Self {
86 base_url,
87 client,
88 nus_hash,
89 root_directory,
90 title_ids,
91 })
92 }
93
94 #[must_use]
96 pub fn get_sorted_group_ids(&self) -> Vec<u32> {
97 let mut group_ids = Vec::new();
98 for title in &self.title_ids {
99 if !group_ids.contains(&title.group_id()) {
100 group_ids.push(title.group_id());
101 }
102 }
103 group_ids.sort_by(|a, b| format!("{a:08x}").cmp(&format!("{b:08x}")));
104 group_ids
105 }
106
107 #[must_use]
109 pub fn get_sorted_tids_in_group(&self, group_id: u32) -> Vec<u32> {
110 let mut tids = Vec::new();
111 for title in &self.title_ids {
112 if title.group_id() == group_id && !tids.contains(&title.title_id()) {
113 tids.push(title.title_id());
114 }
115 }
116 tids.sort_by(|a, b| format!("{a:08x}").cmp(&format!("{b:08x}")));
117 tids
118 }
119
120 pub async fn get_files_in_folder(
122 &self,
123 tid: TitleID,
124 folder_relative_to_nus: &Path,
125 is_code: bool,
126 ) -> Vec<(PathBuf, Option<CafeContentFileInformation>)> {
127 let mut nus_title_path = self.root_directory.clone();
128 nus_title_path.push(format!("{:08x}", tid.group_id()));
129 nus_title_path.push(format!("{:08x}", tid.title_id()));
130 nus_title_path.push(".nus");
131 nus_title_path.push(&self.nus_hash);
132
133 let Ok((tmd, tmd_raw)) = self.get_tmd(&nus_title_path, tid).await else {
134 return Vec::with_capacity(0);
135 };
136 let Ok((fst, fst_raw)) = self.get_fst(&nus_title_path, tid, &tmd).await else {
137 return Vec::with_capacity(0);
138 };
139
140 let mut saw_fst = false;
141 let mut saw_preload = false;
142 let mut saw_tmd = false;
143
144 let mut items = Vec::new();
145 for (relative_path, file_data) in fst.paths() {
146 let Ok(leftover_path) = relative_path.strip_prefix(folder_relative_to_nus) else {
147 continue;
148 };
149 if leftover_path.components().count() != 1 {
151 continue;
152 }
153
154 if leftover_path.to_string_lossy() == "title.fst" {
155 saw_fst = true;
156 }
157 if leftover_path.to_string_lossy() == "title.tmd" {
158 saw_tmd = true;
159 }
160 if leftover_path.to_string_lossy() == "preload.txt" {
161 saw_preload = true;
162 }
163
164 items.push((leftover_path.to_path_buf(), *file_data));
165 }
166
167 if is_code {
168 if !saw_fst {
169 items.push((
170 PathBuf::from("title.fst"),
171 Some(CafeContentFileInformation::new(
172 u16::MAX,
173 u32::try_from(fst_raw.len()).unwrap_or(u32::MAX),
174 u64::MAX - 1,
175 )),
176 ));
177 }
178 if !saw_preload {
179 items.push((
180 PathBuf::from("preload.txt"),
181 Some(CafeContentFileInformation::new(u16::MAX, 0, u64::MAX - 2)),
182 ));
183 }
184 if !saw_tmd {
185 items.push((
186 PathBuf::from("title.tmd"),
187 Some(CafeContentFileInformation::new(
188 u16::MAX,
189 u32::try_from(tmd_raw.len()).unwrap_or(u32::MAX),
190 u64::MAX,
191 )),
192 ));
193 }
194 }
195
196 items
197 }
198
199 #[must_use]
201 pub async fn exists(
202 &self,
203 tid: TitleID,
204 path_in_title: &Path,
205 ) -> Option<Option<CafeContentFileInformation>> {
206 if !self.title_ids.contains(&tid) {
208 debug!(
209 "NUS queried for unknown title: {:016x}, not serving.",
210 tid.full()
211 );
212 return None;
213 }
214 let mut nus_title_path = self.root_directory.clone();
215 nus_title_path.push(format!("{:08x}", tid.group_id()));
216 nus_title_path.push(format!("{:08x}", tid.title_id()));
217 nus_title_path.push(".nus");
218 nus_title_path.push(&self.nus_hash);
219 if !nus_title_path.exists()
220 && let Err(cause) = tokio::fs::create_dir_all(&nus_title_path).await
221 {
222 error!(
223 ?cause,
224 title = format!("{:016x}", tid.full()),
225 "Failed to create NUS cache directory for title, cannot download from NUS!",
226 );
227
228 return None;
229 }
230
231 let (tmd, tmd_raw) = match self.get_tmd(&nus_title_path, tid).await {
232 Ok(t) => t,
233 Err(cause) => {
234 warn!(
235 ?cause,
236 lisa.force_combine_fields = true,
237 title = format!("{:016x}", tid.full()),
238 "Failed to get TMD for title, will not be processing.",
239 );
240 return None;
241 }
242 };
243 let (fst, fst_raw) = match self.get_fst(&nus_title_path, tid, &tmd).await {
244 Ok(f) => f,
245 Err(cause) => {
246 warn!(
247 ?cause,
248 lisa.force_combine_fields = true,
249 title = format!("{:016x}", tid.full()),
250 "Failed to get FST for title, will not be processing.",
251 );
252 return None;
253 }
254 };
255 for (key, file_info) in fst.paths() {
256 if key == path_in_title {
257 return Some(*file_info);
258 }
259 }
260
261 if path_in_title.to_string_lossy().replace('\\', "/") == "code/title.tmd" {
262 return Some(Some(CafeContentFileInformation::new(
263 u16::MAX,
264 u32::try_from(tmd_raw.len()).unwrap_or(u32::MAX),
265 u64::MAX,
266 )));
267 }
268 if path_in_title.to_string_lossy().replace('\\', "/") == "code/title.fst" {
269 return Some(Some(CafeContentFileInformation::new(
270 u16::MAX,
271 u32::try_from(fst_raw.len()).unwrap_or(u32::MAX),
272 u64::MAX - 1,
273 )));
274 }
275 if path_in_title.to_string_lossy().replace('\\', "/") == "code/preload.txt" {
276 return Some(Some(CafeContentFileInformation::new(
277 u16::MAX,
278 0,
279 u64::MAX - 2,
280 )));
281 }
282
283 None
284 }
285
286 pub async fn download_to(
294 &self,
295 disk_path: &Path,
296 title_id: TitleID,
297 file_info: CafeContentFileInformation,
298 ) -> Result<(), CatBridgeError> {
299 info!(
300 lisa.force_combine_fields = true,
301 file.path = %disk_path.display(),
302 title_id = format!("{:016x}", title_id.full()),
303 "Downloading File To Disk From NUS!",
304 );
305 let mut nus_title_path = self.root_directory.clone();
306 nus_title_path.push(format!("{:08x}", title_id.group_id()));
307 nus_title_path.push(format!("{:08x}", title_id.title_id()));
308 nus_title_path.push(".nus");
309 nus_title_path.push(&self.nus_hash);
310 if !nus_title_path.exists() {
311 tokio::fs::create_dir_all(&nus_title_path)
312 .await
313 .map_err(FSError::IO)?;
314 }
315 if let Some(disk_dir) = disk_path.parent()
316 && !disk_dir.exists()
317 {
318 tokio::fs::create_dir_all(&disk_dir)
319 .await
320 .map_err(FSError::IO)?;
321 }
322
323 let (tmd, tmd_raw) = self.get_tmd(&nus_title_path, title_id).await?;
324
325 if file_info.content_idx() == u16::MAX && file_info.file_offset() == u64::MAX {
329 tokio::fs::write(disk_path, &tmd_raw)
330 .await
331 .map_err(FSError::IO)?;
332 } else if file_info.content_idx() == u16::MAX && file_info.file_offset() == u64::MAX - 1 {
333 let (_, fst_raw) = self.get_fst(&nus_title_path, title_id, &tmd).await?;
334 tokio::fs::write(disk_path, &fst_raw)
335 .await
336 .map_err(FSError::IO)?;
337 } else if file_info.content_idx() == u16::MAX && file_info.file_offset() == u64::MAX - 2 {
338 tokio::fs::File::create(disk_path)
339 .await
340 .map_err(FSError::IO)?;
341 } else {
342 let record = tmd
343 .contents()
344 .get(usize::from(file_info.content_idx()))
345 .ok_or_else(|| {
346 NetworkError::NUSInvalidContentID(title_id, file_info.content_idx())
347 })?;
348 let decrypted_content = self
349 .get_and_decrypt_file(&nus_title_path, title_id, record, &file_info)
350 .await?;
351 tokio::fs::write(disk_path, &decrypted_content)
352 .await
353 .map_err(FSError::IO)?;
354 }
355
356 Ok(())
357 }
358
359 async fn get_tmd(
365 &self,
366 base_path: &Path,
367 title_id: TitleID,
368 ) -> Result<(TitleMetadata, Bytes), CatBridgeError> {
369 let mut tmd_path = PathBuf::from(base_path);
370 tmd_path.push("tmd");
371
372 if tmd_path.exists() {
373 match tokio::fs::read(&tmd_path).await {
374 Ok(data) => {
375 let d_as_bytes = Bytes::from(data);
376 match TitleMetadata::try_from(d_as_bytes.clone()) {
377 Ok(tmd) => {
378 debug!(
379 nus_hash = %self.nus_hash,
380 title = format!("{:016x}", title_id.full()),
381 "is using cached tmd for title",
382 );
383 return Ok((tmd, d_as_bytes));
384 }
385 Err(cause) => {
386 warn!(
387 ?cause,
388 lisa.force_combine_fields = true,
389 nus_hash = %self.nus_hash,
390 title = format!("{:016x}", title_id.full()),
391 "Failed to parse existing cached TMD, will download again!",
392 );
393 }
394 }
395 }
396 Err(cause) => {
397 warn!(
398 ?cause,
399 lisa.force_combine_fields = true,
400 nus_hash = %self.nus_hash,
401 title = format!("{:016x}", title_id.full()),
402 "Failed to read existing cached TMD, will download again!",
403 );
404 }
405 }
406 }
407
408 let mut my_url = self.base_url.clone();
409 my_url.set_path(&format!("{}/{:016x}/tmd", my_url.path(), title_id.full()));
410 let resulting_bytes = match self.client.get(my_url).send().await {
411 Ok(resp) => {
412 if !resp.status().is_success() {
413 error!(
414 status = %resp.status(),
415 title = format!("{:016x}", title_id.full()),
416 "Failed to fetch tmd for title got bad status code, will not return TMD",
417 );
418 return Err(NetworkError::HTTPStatusCode(
419 resp.status(),
420 resp.bytes().await.ok(),
421 )
422 .into());
423 }
424
425 match resp.bytes().await {
426 Ok(success) => success,
427 Err(cause) => {
428 error!(
429 ?cause,
430 title = format!("{:016x}", title_id.full()),
431 "Failed to read TMD response from successful NUS server!",
432 );
433 return Err(cause.into());
434 }
435 }
436 }
437 Err(cause) => {
438 error!(
439 ?cause,
440 title = format!("{:016x}", title_id.full()),
441 "Failed to make TMD request to NUS server!",
442 );
443 return Err(cause.into());
444 }
445 };
446
447 let tmd = match TitleMetadata::try_from(resulting_bytes.clone()) {
448 Ok(t) => t,
449 Err(cause) => {
450 error!(
451 ?cause,
452 title = format!("{:016x}", title_id.full()),
453 "NUS responded with invalid TMD, will not use or cache!",
454 );
455 return Err(NetworkError::NUS(cause.into()).into());
456 }
457 };
458 if let Err(cause) = tokio::fs::write(&tmd_path, &resulting_bytes).await {
459 warn!(
460 ?cause,
461 lisa.force_combine_fields = true,
462 path = %tmd_path.display(),
463 "Failed to write TMD to disk! Will need to download again...",
464 );
465 }
466
467 Ok((tmd, resulting_bytes))
468 }
469
470 async fn get_fst(
475 &self,
476 base_path: &Path,
477 title_id: TitleID,
478 tmd: &TitleMetadata,
479 ) -> Result<(CafeContentFilesystemTree, Bytes), CatBridgeError> {
480 let Some(fst_record) = tmd.fst_record() else {
481 error!(
482 title = format!("{:016x}", title_id.full()),
483 "Title does not contain a FST Record, cannot be served!",
484 );
485 return Err(NetworkError::NUSMissingFST(title_id).into());
486 };
487
488 let mut fst_path = PathBuf::from(base_path);
489 fst_path.push("fst");
490 if fst_path.exists() {
491 match tokio::fs::read(&fst_path).await {
492 Ok(data) => {
493 let d_as_bytes = Bytes::from(data);
494 match CafeContentFilesystemTree::try_from(d_as_bytes.clone()) {
495 Ok(t) => return Ok((t, d_as_bytes)),
496 Err(cause) => {
497 warn!(
498 ?cause,
499 lisa.force_combine_fields = true,
500 nus_hash = %self.nus_hash,
501 title = format!("{:016x}", title_id.full()),
502 "Failed to parse existing fst, will download again!",
503 );
504 }
505 }
506 }
507 Err(cause) => {
508 warn!(
509 ?cause,
510 lisa.force_combine_fields = true,
511 nus_hash = %self.nus_hash,
512 title = format!("{:016x}", title_id.full()),
513 "Failed to read existing fst, will download again!",
514 );
515 }
516 }
517 }
518
519 let possible_fst_contents = self
520 .possible_decrypt_contents(base_path, title_id, fst_record)
521 .await?;
522
523 let mut last_error = CatBridgeError::FS(FSError::IO(std::io::Error::other(
524 "zero title keys could be guessed.",
525 )));
526 for (content_possiblity, key) in possible_fst_contents {
527 match CafeContentFilesystemTree::try_from(content_possiblity.clone()) {
528 Ok(content) => {
529 let mut tkeys_path = PathBuf::from(base_path);
530 tkeys_path.push("possible-title-keys");
531 tokio::fs::write(&tkeys_path, key)
532 .await
533 .map_err(FSError::IO)?;
534 tokio::fs::write(&fst_path, &content_possiblity)
535 .await
536 .map_err(FSError::IO)?;
537
538 return Ok((content, content_possiblity));
539 }
540 Err(cause) => {
541 last_error = NetworkError::NUS(cause).into();
542 }
543 }
544 }
545 Err(last_error)
546 }
547
548 async fn get_and_decrypt_file(
553 &self,
554 base_path: &Path,
555 title_id: TitleID,
556 record: &ContentRecord,
557 file_info: &CafeContentFileInformation,
558 ) -> Result<Bytes, CatBridgeError> {
559 let content_id = record.id();
560 let encrypted_content = self
561 .get_encrypted_content_id(base_path, title_id, content_id)
562 .await?;
563
564 let possible_title_keys = self.get_possible_title_keys(base_path, title_id).await;
565 let mut decrypted: Option<Bytes> = None;
566 for key in possible_title_keys {
567 if let Ok(copied) = decrypt(encrypted_content.clone(), key, Some(file_info), record) {
568 decrypted = Some(copied);
569 break;
570 }
571 }
572
573 let Some(final_bytes) = decrypted else {
574 warn!(
575 content_id = format!("{content_id:08x}"),
576 lisa.force_combine_fields = true,
577 nus_hash = %self.nus_hash,
578 title = format!("{:016x}", title_id.full()),
579 "Failed to decrypt content for title ID! will not load file!",
580 );
581 return Err(NetworkError::NUSNoTitleKey(title_id).into());
582 };
583
584 Ok(final_bytes)
585 }
586
587 async fn possible_decrypt_contents(
592 &self,
593 base_path: &Path,
594 title_id: TitleID,
595 record: &ContentRecord,
596 ) -> Result<Vec<(Bytes, [u8; 16])>, CatBridgeError> {
597 let content_id = record.id();
598 let encrypted_content = self
599 .get_encrypted_content_id(base_path, title_id, content_id)
600 .await?;
601 let possible_title_keys = self.get_possible_title_keys(base_path, title_id).await;
602
603 let mut decrypted = Vec::new();
604 for key in possible_title_keys {
605 if let Ok(copied) = decrypt(encrypted_content.clone(), key, None, record) {
606 decrypted.push((copied, key));
607 }
608 }
609
610 Ok(decrypted)
611 }
612
613 async fn get_encrypted_content_id(
617 &self,
618 base_path: &Path,
619 title_id: TitleID,
620 content_id: u32,
621 ) -> Result<Bytes, CatBridgeError> {
622 let content_id_fetchable = format!("{content_id:08x}");
623 let mut encrypted_path = PathBuf::from(base_path);
624 encrypted_path.push("encrypted");
625 tokio::fs::create_dir_all(&encrypted_path)
626 .await
627 .map_err(FSError::IO)?;
628 encrypted_path.push(format!("{content_id_fetchable}.app"));
629
630 if encrypted_path.exists() {
631 match tokio::fs::read(&encrypted_path).await {
632 Ok(data) => {
633 debug!(
634 content_id = format!("{content_id:08x}"),
635 nus_hash = %self.nus_hash,
636 title = format!("{:016x}", title_id.full()),
637 "Using cached encrypted app.",
638 );
639
640 return Ok(Bytes::from(data));
641 }
642 Err(cause) => {
643 warn!(
644 ?cause,
645 content_id = format!("{content_id:08x}"),
646 lisa.force_combine_fields = true,
647 nus_hash = %self.nus_hash,
648 title = format!("{:016x}", title_id.full()),
649 "Failed to read existing encrypted app, will download again!",
650 );
651 }
652 }
653 }
654
655 let mut my_url = self.base_url.clone();
656 my_url.set_path(&format!(
657 "{}/{:016x}/{content_id_fetchable}",
658 my_url.path(),
659 title_id.full()
660 ));
661 let resulting_bytes = match self.client.get(my_url).send().await {
662 Ok(resp) => {
663 if !resp.status().is_success() {
664 warn!(
665 content_id = format!("{content_id:08x}"),
666 status = %resp.status(),
667 title = format!("{:016x}", title_id.full()),
668 "Failed to fetch content for title got bad status code, will not return encrypted app",
669 );
670 return Err(NetworkError::HTTPStatusCode(
671 resp.status(),
672 resp.bytes().await.ok(),
673 )
674 .into());
675 }
676
677 match resp.bytes().await {
678 Ok(success) => success,
679 Err(cause) => {
680 warn!(
681 ?cause,
682 content_id = format!("{content_id:08x}"),
683 title = format!("{:016x}", title_id.full()),
684 "Failed to read content for title response from successful NUS server!",
685 );
686 return Err(cause.into());
687 }
688 }
689 }
690 Err(cause) => {
691 warn!(
692 ?cause,
693 content_id = format!("{content_id:08x}"),
694 title = format!("{:016x}", title_id.full()),
695 "Failed to read content for title request to NUS server!",
696 );
697 return Err(cause.into());
698 }
699 };
700
701 if let Err(cause) = tokio::fs::write(&encrypted_path, &resulting_bytes).await {
702 warn!(
703 ?cause,
704 content_id = format!("{content_id:08x}"),
705 lisa.force_combine_fields = true,
706 path = %encrypted_path.display(),
707 "Failed to write encrypted app file to disk! Will need to fetch again.",
708 );
709 }
710 Ok(resulting_bytes)
711 }
712
713 async fn get_possible_title_keys(
717 &self,
718 base_path: &Path,
719 title_id: TitleID,
720 ) -> Vec<[u8; 0x10]> {
721 let mut tkeys_path = PathBuf::from(base_path);
722 tkeys_path.push("possible-title-keys");
723
724 if tkeys_path.exists() {
725 match tokio::fs::read(&tkeys_path).await {
726 Ok(data) => {
727 debug!(
728 nus_hash = %self.nus_hash,
729 title = format!("{:016x}", title_id.full()),
730 "Using cached title keys",
731 );
732
733 return data
734 .chunks(0x10)
735 .filter_map(|c| {
736 if c.len() == 0x10 {
737 Some([
738 c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9],
739 c[10], c[11], c[12], c[13], c[14], c[15],
740 ])
741 } else {
742 None
743 }
744 })
745 .collect::<Vec<_>>();
746 }
747 Err(cause) => {
748 warn!(
749 ?cause,
750 lisa.force_combine_fields = true,
751 nus_hash = %self.nus_hash,
752 title = format!("{:016x}", title_id.full()),
753 "Failed to read existing cached title keys, will generate again!",
754 );
755 }
756 }
757 }
758
759 let title_keys: Vec<[u8; 0x10]> =
760 if let Ok(tik) = self.fetch_ticket(base_path, title_id).await {
761 if let Ok(title_key) = tik.v0_header().title_key() {
762 vec![title_key]
763 } else {
764 title_key_guesses(title_id)
765 }
766 } else {
767 title_key_guesses(title_id)
768 };
769
770 let mut serialized = BytesMut::with_capacity(title_keys.len() * 0x10);
771 for key in &title_keys {
772 serialized.extend(key);
773 }
774 if let Err(cause) = tokio::fs::write(&tkeys_path, &serialized.freeze()).await {
775 warn!(
776 ?cause,
777 lisa.force_combine_fields = true,
778 path = %tkeys_path.display(),
779 "Failed to write title-keys to disk! Will need to generate again...",
780 );
781 }
782 title_keys
783 }
784
785 async fn fetch_ticket(
796 &self,
797 base_path: &Path,
798 title_id: TitleID,
799 ) -> Result<Ticket, CatBridgeError> {
800 let mut ticket_path = PathBuf::from(base_path);
801 ticket_path.push("title.tik");
802
803 if ticket_path.exists() {
804 match tokio::fs::read(&ticket_path).await {
805 Ok(data) => match Ticket::try_from(Bytes::from(data)) {
806 Ok(tik) => {
807 debug!(
808 nus_hash = %self.nus_hash,
809 title = format!("{:016x}", title_id.full()),
810 "is using cached cetk for title",
811 );
812 return Ok(tik);
813 }
814 Err(cause) => {
815 warn!(
816 ?cause,
817 lisa.force_combine_fields = true,
818 nus_hash = %self.nus_hash,
819 title = format!("{:016x}", title_id.full()),
820 "Failed to parse existing CETK, will download again!",
821 );
822 }
823 },
824 Err(cause) => {
825 warn!(
826 ?cause,
827 lisa.force_combine_fields = true,
828 nus_hash = %self.nus_hash,
829 title = format!("{:016x}", title_id.full()),
830 "Failed to read existing cached CETK, will download again!",
831 );
832 }
833 }
834 }
835
836 let mut my_url = self.base_url.clone();
837 my_url.set_path(&format!("{}/{:016x}/cetk", my_url.path(), title_id.full()));
838 let resulting_bytes = match self.client.get(my_url).send().await {
839 Ok(resp) => {
840 if !resp.status().is_success() {
841 warn!(
842 status = %resp.status(),
843 title = format!("{:016x}", title_id.full()),
844 "Failed to fetch cetk for title got bad status code, will not return cetk",
845 );
846 return Err(NetworkError::HTTPStatusCode(
847 resp.status(),
848 resp.bytes().await.ok(),
849 )
850 .into());
851 }
852
853 match resp.bytes().await {
854 Ok(success) => success,
855 Err(cause) => {
856 warn!(
857 ?cause,
858 title = format!("{:016x}", title_id.full()),
859 "Failed to read CETK response from successful NUS server!",
860 );
861 return Err(cause.into());
862 }
863 }
864 }
865 Err(cause) => {
866 warn!(
867 ?cause,
868 title = format!("{:016x}", title_id.full()),
869 "Failed to make CETK request to NUS server!",
870 );
871 return Err(cause.into());
872 }
873 };
874
875 let tik = match Ticket::try_from(resulting_bytes.clone()) {
876 Ok(t) => t,
877 Err(cause) => {
878 warn!(
879 ?cause,
880 title = format!("{:016x}", title_id.full()),
881 "NUS responded with invalid CETK, will not use or cache!",
882 );
883 return Err(NetworkError::NUS(cause.into()).into());
884 }
885 };
886 if let Err(cause) = tokio::fs::write(&ticket_path, &resulting_bytes).await {
887 warn!(
888 ?cause,
889 lisa.force_combine_fields = true,
890 path = %ticket_path.display(),
891 "Failed to write CETK to disk! Will need to download again...",
892 );
893 }
894 Ok(tik)
895 }
896}
897
898const NUS_FUSE_FIELDS: &[NamedField<'static>] = &[
899 NamedField::new("base_url"),
900 NamedField::new("client"),
901 NamedField::new("nus_hash"),
902 NamedField::new("root_directory"),
903];
904
905impl Structable for NUSFuse {
906 fn definition(&self) -> StructDef<'_> {
907 StructDef::new_static("NUSFuse", Fields::Named(NUS_FUSE_FIELDS))
908 }
909}
910
911impl Valuable for NUSFuse {
912 fn as_value(&self) -> Value<'_> {
913 Value::Structable(self)
914 }
915
916 fn visit(&self, visitor: &mut dyn Visit) {
917 visitor.visit_named_fields(&NamedValues::new(
918 NUS_FUSE_FIELDS,
919 &[
920 Valuable::as_value(&format!("{}", self.base_url)),
921 Valuable::as_value(&format!("{:?}", self.client)),
922 Valuable::as_value(&self.nus_hash),
923 Valuable::as_value(&format!("{}", self.root_directory.display())),
924 ],
925 ));
926 }
927}