diff --git a/Cargo.lock b/Cargo.lock index 195ab27..99a92be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "opaque-debug 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -69,6 +69,11 @@ dependencies = [ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "autocfg" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "backtrace" version = "0.3.9" @@ -90,6 +95,16 @@ dependencies = [ "libc 0.2.49 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bincode" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "bit_field" version = "0.10.0" @@ -153,7 +168,7 @@ name = "blz-nx" version = "0.1.0" source = "git+https://github.com/Thog/blz-nx-rs#7e2dccf2a3e1ccb52408e1db995e8f3971de4f2c" dependencies = [ - "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -178,7 +193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "byteorder" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -417,10 +432,11 @@ name = "linkle" version = "0.2.7" dependencies = [ "aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "bit_field 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "block-modes 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "blz-nx 0.1.0 (git+https://github.com/Thog/blz-nx-rs)", - "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "cargo_metadata 0.6.0 (git+https://github.com/roblabla/cargo_metadata)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "cmac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -865,8 +881,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd1479b7c29641adbd35ff3b5c293922d696a92f25c8c975da3e0acbc87258f" "checksum arrayvec 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f405cc4c21cd8b784f6c8fc2adf9bc00f59558f0049b5ec21517f875963040cc" "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +"checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" "checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" "checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" +"checksum bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9f04a5e50dc80b3d5d35320889053637d15011aed5e66b66b37ae798c65da6f7" "checksum bit_field 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a165d606cf084741d4ac3a28fb6e9b1eb0bd31f6cd999098cfddb0b2ab381dc0" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" @@ -880,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum byte-tools 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "980479e6fde23246dfb54d47580d66b4e99202e7579c5eaa9fe10ecb5ebd2182" "checksum bytecount 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8" "checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" -"checksum byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "74c0b906e9446b0a2e4f760cdb3fa4b2c48cdc6db8766a845c54b6ff063fd2e9" +"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum cargo_metadata 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1efca0b863ca03ed4c109fb1c55e0bc4bbeb221d3e103d86251046b06a526bd0" "checksum cargo_metadata 0.6.0 (git+https://github.com/roblabla/cargo_metadata)" = "" "checksum cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "d01c69d08ff207f231f07196e30f84c70f1c815b04f980f8b7b01ff01f05eb92" diff --git a/Cargo.toml b/Cargo.toml index 8de395c..f704faf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ derive_more = "0.13" cmac = "0.2.0" blz-nx = { git = "https://github.com/Thog/blz-nx-rs" } bit_field = "0.10.0" +bincode = "1.1.4" [features] binaries = ["structopt", "cargo_metadata", "scroll", "goblin", "clap"] diff --git a/src/bin/linkle_clap.rs b/src/bin/linkle_clap.rs index 843a0cb..487bbe3 100644 --- a/src/bin/linkle_clap.rs +++ b/src/bin/linkle_clap.rs @@ -94,6 +94,16 @@ enum Opt { /// Key file to use #[structopt(parse(from_os_str), short = "k", long = "keyset")] keyfile: Option, + }, + /// Create an NPDM from a JSON-NPDM formatted file. + #[structopt(name = "npdm")] + Npdm { + /// Sets the input JSON file to use. + #[structopt(parse(from_os_str))] + input_file: PathBuf, + /// Sets the output NPDM file to use. + #[structopt(parse(from_os_str))] + output_file: PathBuf, } } @@ -134,7 +144,7 @@ fn create_kip(input_file: &str, npdm_file: &str, output_file: &str) -> Result<() let mut option = OpenOptions::new(); let output_option = option.write(true).create(true).truncate(true); output_option.open(output_file)?; - + nxo.write_kip1(&mut output_option.open(output_file).map_err(|err| (err, output_file))?, &npdm).map_err(|err| (err, output_file))?; Ok(()) } @@ -193,6 +203,15 @@ fn print_keys(is_dev: bool, key_path: Option<&Path>) -> Result<(), linkle::error Ok(()) } +fn create_npdm(input_file: &Path, output_file: &Path) -> Result<(), linkle::error::Error> { + let mut npdm = linkle::format::npdm::NpdmJson::from_file(&input_file)?; + let mut option = OpenOptions::new(); + let output_option = option.write(true).create(true).truncate(true); + let mut out_file = output_option.open(output_file).map_err(|err| (err, output_file))?; + npdm.into_npdm(&mut out_file, false)?; + Ok(()) +} + fn to_opt_ref>(s: &Option) -> Option<&U> { s.as_ref().map(AsRef::as_ref) } @@ -207,6 +226,7 @@ fn process_args(app: &Opt) { Opt::Nacp { ref input_file, ref output_file } => create_nacp(input_file, output_file), Opt::Romfs { ref input_directory, ref output_file } => create_romfs(input_directory, output_file), Opt::Keygen { dev, ref keyfile } => print_keys(*dev, to_opt_ref(keyfile)), + Opt::Npdm { ref input_file, ref output_file } => create_npdm(input_file, output_file) }; if let Err(e) = res { diff --git a/src/error.rs b/src/error.rs index 9e2195e..2654b4e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,7 @@ use failure::Backtrace; use block_modes::BlockModeError; use failure::Fail; use derive_more::Display; +use std::borrow::Cow; #[derive(Debug, Fail, Display)] pub enum Error { @@ -32,6 +33,10 @@ pub enum Error { RomFsSymlink(PathBuf, Backtrace), #[display(fmt = "Unknown file type at {}", "_0.display()")] RomFsFiletype(PathBuf, Backtrace), + #[display(fmt = "Invalid NPDM value for field {}", "_0")] + InvalidNpdmValue(Cow<'static, str>, Backtrace), + #[display(fmt = "Failed to serialize NPDM.")] + BincodeError(#[cause] Box, Backtrace), } impl Error { @@ -96,3 +101,9 @@ impl From<(usize, cmac::crypto_mac::MacError)> for Error { Error::MacError(err, id, Backtrace::new()) } } + +impl From> for Error { + fn from(err: Box) -> Error { + Error::BincodeError(err, Backtrace::new()) + } +} diff --git a/src/format/mod.rs b/src/format/mod.rs index e921d5c..6ec7630 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -2,5 +2,5 @@ pub mod nacp; pub mod nxo; pub mod pfs0; pub mod romfs; -mod npdm; +pub mod npdm; mod utils; diff --git a/src/format/npdm.rs b/src/format/npdm.rs index f301a7d..3201d74 100644 --- a/src/format/npdm.rs +++ b/src/format/npdm.rs @@ -1,15 +1,15 @@ -use byteorder::{LittleEndian, WriteBytesExt}; -use crate::format::utils; use std; -use std::fmt; -use std::fs::File; use std::io::Write; use std::collections::HashMap; -use crate::format::utils::HexOrNum; +use crate::format::utils::{SigOrPubKey, Reserved64, HexOrNum}; +use crate::error::Error; use serde_derive::{Serialize, Deserialize}; use serde_json; use bit_field::BitField; use std::convert::TryFrom; +use std::path::Path; +use std::mem::size_of; +use failure::Backtrace; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", content = "value")] @@ -60,7 +60,7 @@ impl KernelCapability { for (idx, mask) in masks.iter_mut().enumerate() { mask.set_bits(29..32, idx as u32); } - for (syscall_name, syscall_val) in syscalls { + for (_syscall_name, syscall_val) in syscalls { masks[syscall_val.0 as usize / 24].set_bit(usize::try_from((syscall_val.0 % 24) + 5).unwrap(), true); used[syscall_val.0 as usize / 24] = true; } @@ -118,3 +118,306 @@ impl KernelCapability { } } } + +fn sac_encoded_len(sacs: &[String]) -> usize { + sacs.iter().map(|v| 1 + v.len()).sum() +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NPDMFilesystemAccess { + permissions: HexOrNum, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NpdmJson { + // META fields. + name: String, + main_thread_stack_size: HexOrNum, + main_thread_priority: u8, + default_cpu_id: u8, + // We thought this field was the process_category. We were wrong. 🤦 + #[serde(alias = "process_category")] + version: u32, + address_space_type: u8, + is_64_bit: bool, + + // ACID fields + is_retail: bool, + pool_partition: u32, + title_id_range_min: HexOrNum, + title_id_range_max: HexOrNum, + developer_key: Option, + + // ACI0 + title_id: HexOrNum, + + // FAC + filesystem_access: NPDMFilesystemAccess, + + // SAC + service_access: Vec, + service_host: Vec, + + // KAC + kernel_capabilities: Vec, +} + +enum ACIDBehavior<'a> { + Sign, + Empty, + Use(&'a [u8]) +} + +impl NpdmJson { + pub fn from_file(file: &Path) -> Result { + let file = std::fs::File::open(file)?; + match serde_json::from_reader(file) { + Ok(res) => Ok(res), + Err(error) => Err(Error::from(error)), + } + } + + // TODO: Optionally pass a (signed) ACID here. + pub fn into_npdm(&self, mut file: W, _signed: bool) -> Result<(), Error> { + let mut meta: RawMeta = RawMeta::default(); + + meta.magic = *b"META"; + + if self.address_space_type & !3 != 0 { + return Err(Error::InvalidNpdmValue("address_space_type".into(), Backtrace::new())); + } + meta.mmu_flags = (self.address_space_type & 3) << 1; + if self.is_64_bit { meta.mmu_flags |= 1; } + + meta.main_thread_prio = self.main_thread_priority; + meta.main_thread_core_num = self.default_cpu_id; + + meta.system_resources = 0; + meta.version = 0; + + meta.main_thread_stack_size = self.main_thread_stack_size.0 as _; + + let title_name_len = std::cmp::min(self.name.as_bytes().len(), 12); + meta.title_name = [0; 16]; + meta.title_name[..title_name_len].copy_from_slice(&self.name.as_bytes()[..title_name_len]); + + meta.product_code = [0; 0x10]; + + meta.aci_offset = (size_of::() + size_of::() + + size_of::() + + sac_encoded_len(&self.service_host) + sac_encoded_len(&self.service_access) + + self.kernel_capabilities.iter().map(|v| v.encode().len()).sum::()) as u32; + meta.aci_size = (size_of::() + size_of::() + + sac_encoded_len(&self.service_host) + sac_encoded_len(&self.service_access) + + self.kernel_capabilities.iter().map(|v| v.encode().len()).sum::()) as u32; + + meta.acid_offset = 0x80; + meta.acid_size = (size_of::() + size_of::() + + sac_encoded_len(&self.service_host) + sac_encoded_len(&self.service_access) + + self.kernel_capabilities.iter().map(|v| v.encode().len()).sum::()) as u32; + + bincode::config().little_endian().serialize_into(&mut file, &meta)?; + + let mut acid = RawAcid::default(); + acid.rsa_acid_sig = SigOrPubKey([0; 0x100]); + acid.rsa_nca_pubkey = SigOrPubKey([0; 0x100]); + acid.magic = *b"ACID"; + acid.signed_size = meta.acid_size - 0x100; + + acid.flags = 0u32; + if self.is_retail { acid.flags |= 1; } + + if self.pool_partition & !3 != 0 { + return Err(Error::InvalidNpdmValue("pool_partition".into(), Backtrace::new())); + } + acid.flags |= (self.pool_partition & 3) << 2; + // TODO: Unqualified approval. Zefuk is this? + + acid.titleid_range_min = self.title_id_range_min.0; + acid.titleid_range_max = self.title_id_range_max.0; + + acid.fs_access_control_offset = meta.acid_offset + size_of::() as u32; + acid.fs_access_control_size = size_of::() as u32; + + acid.service_access_control_offset = acid.fs_access_control_offset + acid.fs_access_control_size; + acid.service_access_control_size = (sac_encoded_len(&self.service_host) + sac_encoded_len(&self.service_access)) as u32; + + acid.kernel_access_control_offset = acid.service_access_control_offset + acid.service_access_control_size; + acid.kernel_access_control_size = self.kernel_capabilities.iter().map(|v| v.encode().len()).sum::() as u32; + + + bincode::config().little_endian().serialize_into(&mut file, &acid)?; + + let mut fac = RawFileSystemAccessControl::default(); + fac.version = 1; + fac.padding = [0; 3]; + fac.permissions_bitmask = self.filesystem_access.permissions.0; + + bincode::config().little_endian().serialize_into(&mut file, &fac)?; + + for elem in &self.service_access { + if elem.len() & !7 != 0 || elem.len() == 0 { + return Err(Error::InvalidNpdmValue(format!("service_access.{}", elem).into(), Backtrace::new())) + } + file.write_all(&[elem.len() as u8 - 1])?; + file.write_all(elem.as_bytes())?; + } + + for elem in &self.service_host { + if elem.len() & !7 != 0 || elem.len() == 0 { + return Err(Error::InvalidNpdmValue(format!("service_host.{}", elem).into(), Backtrace::new())) + } + file.write_all(&[0x80 | (elem.len() as u8 - 1)])?; + file.write_all(elem.as_bytes())?; + } + + for elem in &self.kernel_capabilities { + bincode::config().little_endian().serialize_into(&mut file, &elem)?; + } + + // ACI0 + let mut aci0 = RawAci::default(); + aci0.magic = *b"ACI0"; + aci0.titleid = self.title_id.0; + aci0.fs_access_header_offset = meta.aci_offset + size_of::() as u32; + aci0.fs_access_header_size = size_of::() as u32; + aci0.service_access_control_offset = aci0.fs_access_header_offset + aci0.fs_access_header_size; + aci0.service_access_control_size = (sac_encoded_len(&self.service_host) + sac_encoded_len(&self.service_access)) as u32; + aci0.service_access_control_offset = aci0.service_access_control_offset + aci0.service_access_control_size; + aci0.kernel_access_control_size = self.kernel_capabilities.iter().map(|v| v.encode().len()).sum::() as u32; + + bincode::config().little_endian().serialize_into(&mut file, &aci0)?; + + let mut fah = RawFileSystemAccessHeader::default(); + fah.version = 1; + fah.padding = [0; 3]; + fah.permissions_bitmask = self.filesystem_access.permissions.0; + fah.data_size = 0x1C; // Always 0x1C + fah.size_of_content_owner_id = 0; + fah.data_size_plus_content_owner_size = 0x1C; + fah.size_of_save_data_owners = 0; + + bincode::config().little_endian().serialize_into(&mut file, &fac)?; + + for elem in &self.service_access { + if elem.len() & !7 != 0 || elem.len() == 0 { + return Err(Error::InvalidNpdmValue(format!("service_access.{}", elem).into(), Backtrace::new())) + } + file.write_all(&[elem.len() as u8 - 1])?; + file.write_all(elem.as_bytes())?; + } + + for elem in &self.service_host { + if elem.len() & !7 != 0 || elem.len() == 0 { + return Err(Error::InvalidNpdmValue(format!("service_host.{}", elem).into(), Backtrace::new())) + } + file.write_all(&[0x80 | (elem.len() as u8 - 1)])?; + file.write_all(elem.as_bytes())?; + } + + for elem in &self.kernel_capabilities { + bincode::config().little_endian().serialize_into(&mut file, &elem)?; + } + + + Ok(()) + } +} + +#[repr(C)] +#[derive(Default, Clone, Copy, Serialize)] +struct RawFileSystemAccessControl { + version: u8, + padding: [u8; 3], + permissions_bitmask: u64, + reserved: [u8; 0x20] +} + +#[repr(C)] +#[derive(Default, Clone, Copy, Serialize)] +struct RawFileSystemAccessHeader { + version: u8, + padding: [u8; 3], + permissions_bitmask: u64, + data_size: u32, // Always 0x1C + size_of_content_owner_id: u32, + data_size_plus_content_owner_size: u32, + size_of_save_data_owners: u32, + // TODO: there's more optional stuff afterwards. +} + +#[repr(C)] +#[derive(Default, Clone, Copy, Serialize)] +struct RawMeta { + magic: [u8; 4], + #[doc(hidden)] + reserved4: u32, + reserved8: u32, + mmu_flags: u8, + #[doc(hidden)] + reserved13: u8, + main_thread_prio: u8, + main_thread_core_num: u8, + #[doc(hidden)] + reserved16: u32, + system_resources: u32, + version: u32, + main_thread_stack_size: u32, + title_name: [u8; 16], + product_code: [u8; 16], + #[doc(hidden)] + reserved64: Reserved64, + aci_offset: u32, + aci_size: u32, + acid_offset: u32, + acid_size: u32, +} + +/// Restriced Access Controls, signed by Nintendo. +#[repr(C)] +#[derive(Default, Clone, Copy, Serialize)] +struct RawAcid { + /// RSA-2048 Signature starting from `rsa_nca_pubkey` and spanning + /// `signed_size` bytes, using a fixed key owned by Nintendo. The pubkey + /// part can be found in hactool, `acid_fixed_key_modulus`. + rsa_acid_sig: SigOrPubKey, // [u8; 0x100], + /// RSA-2048 public key for the second NCA signature + rsa_nca_pubkey: SigOrPubKey, // [u8; 0x100], + /// Magic identifying a valid ACID. Should be `b"ACID"`. + magic: [u8; 4], + signed_size: u32, + #[doc(hidden)] + reserved: u32, + flags: u32, + titleid_range_min: u64, + titleid_range_max: u64, + fs_access_control_offset: u32, + fs_access_control_size: u32, + service_access_control_offset: u32, + service_access_control_size: u32, + kernel_access_control_offset: u32, + kernel_access_control_size: u32, + #[doc(hidden)] + reserved38: u64 +} + +/// Access Control Information. +/// +/// Protected by the NCA signature, which devs control via their pubkey. +#[repr(C)] +#[derive(Default, Clone, Copy, Serialize)] +struct RawAci { + /// Magic identifying a valid ACI. Should be `ACI0`. + magic: [u8; 4], + #[doc(hidden)] + reserved4: [u8; 0xC], + titleid: u64, + #[doc(hidden)] + reserved24: u64, + fs_access_header_offset: u32, + fs_access_header_size: u32, + service_access_control_offset: u32, + service_access_control_size: u32, + kernel_access_control_offset: u32, + kernel_access_control_size: u32, +} \ No newline at end of file diff --git a/src/format/utils.rs b/src/format/utils.rs index 94246ad..ee880c3 100644 --- a/src/format/utils.rs +++ b/src/format/utils.rs @@ -6,6 +6,7 @@ use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use sha2::{Sha256, Digest}; use serde::{Serialize, Serializer, Deserialize, Deserializer}; +use serde::ser::SerializeTuple; use serde::de::{Visitor, Unexpected}; pub fn align(size: usize, padding: usize) -> usize { @@ -85,7 +86,7 @@ impl<'de> Deserialize<'de> for HexOrNum { where E: serde::de::Error { - if (v.starts_with("0x")) { + if v.starts_with("0x") { u64::from_str_radix(&v[2..], 16).map_err(|_| { E::invalid_value(Unexpected::Str(v), &"a hex-encoded string") }) @@ -108,3 +109,40 @@ impl Serialize for HexOrNum { serializer.collect_str(&format_args!("{:#010x}", self.0)) } } + +macro_rules! array_impls { + ($($ty:ident: $len:literal),+) => { + $( + #[derive(Clone, Copy)] + pub struct $ty(pub [u8; $len]); + + impl Default for $ty { + fn default() -> Self { + $ty([0; $len]) + } + } + + impl fmt::Debug for $ty { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_fmt(format_args!("{:02x?}", &self.0[..])) + } + } + + impl Serialize for $ty { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_tuple($len)?; + for e in &self.0[..] { + seq.serialize_element(e)?; + } + seq.end() + } + } + )+ + } +} + +array_impls!(SigOrPubKey: 0x100, Reserved64: 0x30); \ No newline at end of file