Skip to content

Commit 577d5a0

Browse files
committed
Added rustii CLI command to replace content in a WAD
Also added required library features to make this possible. Rust makes the whole "getting content's index from its CID" thing so much easier.
1 parent 96ace71 commit 577d5a0

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
lines changed

src/bin/rustii/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ fn main() -> Result<()> {
127127
},
128128
title::wad::Commands::Unpack { input, output } => {
129129
title::wad::unpack_wad(input, output)?
130+
},
131+
title::wad::Commands::Set { input, content, output, identifier, r#type} => {
132+
title::wad::set_wad(input, content, output, identifier, r#type)?
130133
}
131134
}
132135
},

src/bin/rustii/title/wad.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ pub enum Commands {
3737
input: String,
3838
/// The directory to extract the WAD to
3939
output: String
40+
},
41+
/// Replace existing content in a WAD file with new data
42+
Set {
43+
/// The path to the WAD file to modify
44+
input: String,
45+
/// The new WAD content
46+
content: String,
47+
/// An optional output path; defaults to overwriting input WAD file
48+
#[arg(short, long)]
49+
output: Option<String>,
50+
/// An optional new type for the content, can be "Normal", "Shared", or "DLC"
51+
#[arg(short, long)]
52+
r#type: Option<String>,
53+
#[command(flatten)]
54+
identifier: ContentIdentifier,
4055
}
4156
}
4257

@@ -55,6 +70,18 @@ pub struct ConvertTargets {
5570
vwii: bool,
5671
}
5772

73+
#[derive(Args)]
74+
#[clap(next_help_heading = "Content Identifier")]
75+
#[group(multiple = false, required = true)]
76+
pub struct ContentIdentifier {
77+
/// The index of the content to replace
78+
#[arg(short, long)]
79+
index: Option<usize>,
80+
/// The Content ID of the content to replace
81+
#[arg(short, long)]
82+
cid: Option<String>,
83+
}
84+
5885
enum Target {
5986
Retail,
6087
Dev,
@@ -94,7 +121,7 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
94121
Target::Vwii => PathBuf::from(format!("{}_vWii.wad", in_path.file_stem().unwrap().to_str().unwrap())),
95122
}
96123
};
97-
let mut title = title::Title::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
124+
let mut title = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
98125
// Bail if the WAD is already using the selected encryption.
99126
if matches!(target, Target::Dev) && title.ticket.is_dev() {
100127
bail!("This is already a development WAD!");
@@ -243,3 +270,56 @@ pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
243270
println!("WAD file unpacked!");
244271
Ok(())
245272
}
273+
274+
pub fn set_wad(input: &str, content: &str, output: &Option<String>, identifier: &ContentIdentifier, ctype: &Option<String>) -> Result<()> {
275+
let in_path = Path::new(input);
276+
if !in_path.exists() {
277+
bail!("Source WAD \"{}\" could not be found.", in_path.display());
278+
}
279+
let content_path = Path::new(content);
280+
if !content_path.exists() {
281+
bail!("New content \"{}\" could not be found.", content_path.display());
282+
}
283+
// Get the output name now that we know the target, if one wasn't passed.
284+
let out_path = if output.is_some() {
285+
PathBuf::from(output.clone().unwrap()).with_extension("wad")
286+
} else {
287+
in_path.to_path_buf()
288+
};
289+
// Load the WAD and parse the new type, if one was specified.
290+
let mut title = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?;
291+
let new_content = fs::read(content_path)?;
292+
let mut target_type: Option<tmd::ContentType> = None;
293+
if ctype.is_some() {
294+
target_type = match ctype.clone().unwrap().to_ascii_lowercase().as_str() {
295+
"normal" => Some(tmd::ContentType::Normal),
296+
"shared" => Some(tmd::ContentType::Shared),
297+
"dlc" => Some(tmd::ContentType::DLC),
298+
_ => bail!("The specified content type \"{}\" is invalid!", ctype.clone().unwrap()),
299+
};
300+
}
301+
// Parse the identifier passed to choose how to do the find and replace.
302+
if identifier.index.is_some() {
303+
match title.set_content(&new_content, identifier.index.unwrap(), None, target_type) {
304+
Err(title::TitleError::Content(content::ContentError::IndexOutOfRange { index, max })) => {
305+
bail!("The specified index {} does not exist in this WAD! The maximum index is {}.", index, max)
306+
},
307+
Err(e) => bail!("An unknown error occurred while setting the new content: {e}"),
308+
Ok(_) => (),
309+
}
310+
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
311+
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
312+
println!("Successfully replaced content at index {} in WAD file \"{}\".", identifier.index.unwrap(), out_path.display());
313+
} else if identifier.cid.is_some() {
314+
let cid = u32::from_str_radix(identifier.cid.clone().unwrap().as_str(), 16).with_context(|| "The specified Content ID is invalid!")?;
315+
let index = match title.content.get_index_from_cid(cid) {
316+
Ok(index) => index,
317+
Err(_) => bail!("The specified Content ID \"{}\" ({}) does not exist in this WAD!", identifier.cid.clone().unwrap(), cid),
318+
};
319+
title.set_content(&new_content, index, None, target_type).with_context(|| "An unknown error occurred while setting the new content.")?;
320+
title.fakesign().with_context(|| "An unknown error occurred while fakesigning the modified WAD.")?;
321+
fs::write(&out_path, title.to_wad()?.to_bytes()?).with_context(|| "Could not open output file for writing.")?;
322+
println!("Successfully replaced content with Content ID \"{}\" ({}) in WAD file \"{}\".", identifier.cid.clone().unwrap(), cid, out_path.display());
323+
}
324+
Ok(())
325+
}

src/title/content.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ impl ContentRegion {
110110
}
111111
Ok(buf)
112112
}
113+
114+
/// Gets the index of content using its Content ID.
115+
pub fn get_index_from_cid(&self, cid: u32) -> Result<usize, ContentError> {
116+
// Use fancy Rust find and map methods to find the index matching the provided CID. Take
117+
// that libWiiPy!
118+
let content_index = self.content_records.iter()
119+
.find(|record| record.content_id == cid)
120+
.map(|record| record.index);
121+
if let Some(index) = content_index {
122+
Ok(index as usize)
123+
} else {
124+
Err(ContentError::CIDNotFound(cid))
125+
}
126+
}
113127

114128
/// Gets the encrypted content file from the ContentRegion at the specified index.
115129
pub fn get_enc_content_by_index(&self, index: usize) -> Result<Vec<u8>, ContentError> {

src/title/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ impl Title {
135135
/// Sets the content at the specified index to the provided decrypted content. This content will
136136
/// have its size and hash saved into the matching record. Optionally, a new Content ID or
137137
/// content type can be provided, with the existing values being preserved by default.
138-
pub fn set_content(&mut self, content: &[u8], index: usize) -> Result<(), TitleError> {
139-
self.content.set_content(content, index, None, None, self.ticket.dec_title_key())?;
138+
pub fn set_content(&mut self, content: &[u8], index: usize, cid: Option<u32>, content_type: Option<tmd::ContentType>) -> Result<(), TitleError> {
139+
self.content.set_content(content, index, cid, content_type, self.ticket.dec_title_key())?;
140140
self.tmd.content_records = self.content.content_records.clone();
141141
Ok(())
142142
}

0 commit comments

Comments
 (0)