use std::fmt::{Display, Formatter};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use cargo_metadata::{Message, Package};
use clap::{Parser, Subcommand, ValueEnum};
use serde_derive::Deserialize;
use serde_json::json;
use setup::install_targets;
use utils::*;
mod setup;
mod utils;
#[derive(Debug, Deserialize)]
struct NanosMetadata {
curve: Vec<String>,
path: Vec<String>,
flags: String,
icon: String,
icon_small: String,
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct LedgerMetadata {
curve: Vec<String>,
path: Vec<String>,
flags: String,
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct DeviceMetadata {
icon: String,
}
#[derive(Parser, Debug)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[clap(name = "Ledger devices build and load commands")]
#[clap(version = "0.0")]
#[clap(about = "Builds the project and emits a JSON manifest for ledgerctl.")]
enum Cli {
Ledger(CliArgs),
}
#[derive(clap::Args, Debug)]
struct CliArgs {
#[clap(long)]
#[clap(value_name = "prebuilt ELF exe")]
use_prebuilt: Option<PathBuf>,
#[clap(long)]
#[clap(help = concat!(
"Should the app.hex be placed next to the app.json, or next to the input exe?",
" ",
"Typically used with --use-prebuilt when the input exe is in a read-only location.",
))]
hex_next_to_json: bool,
#[clap(subcommand)]
command: MainCommand,
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
enum Device {
Nanos,
Nanox,
Nanosplus,
}
impl Display for Device {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_ref())
}
}
impl AsRef<str> for Device {
fn as_ref(&self) -> &str {
match self {
Device::Nanos => "nanos",
Device::Nanox => "nanox",
Device::Nanosplus => "nanosplus",
}
}
}
#[derive(Subcommand, Debug)]
enum MainCommand {
#[clap(about = "install custom target files")]
Setup,
#[clap(about = "build the project for a given device")]
Build {
#[clap(value_enum)]
#[clap(help = "device to build for")]
device: Device,
#[clap(short, long)]
#[clap(help = "load on a device")]
load: bool,
#[clap(last = true)]
remaining_args: Vec<String>,
},
}
fn main() {
let Cli::Ledger(cli) = Cli::parse();
match cli.command {
MainCommand::Setup => install_targets(),
MainCommand::Build {
device: d,
load: a,
remaining_args: r,
} => {
build_app(d, a, cli.use_prebuilt, cli.hex_next_to_json, r);
}
}
}
fn retrieve_metadata(
device: Device,
manifest_path: Option<&str>,
) -> (Package, LedgerMetadata, DeviceMetadata) {
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(manifestpath) = manifest_path {
cmd = cmd.manifest_path(manifestpath).clone();
}
let res = cmd
.no_deps()
.exec()
.expect("Could not execute `cargo metadata`");
let this_pkg = res.packages.last().unwrap();
let metadata_section = this_pkg.metadata.get("ledger");
if let Some(metadatasection) = metadata_section {
let metadata_device = metadata_section
.unwrap()
.clone()
.get(device.as_ref())
.unwrap()
.clone();
let ledger_metadata: LedgerMetadata =
serde_json::from_value(metadatasection.clone())
.expect("Could not deserialize medatada.ledger");
let device_metadata: DeviceMetadata =
serde_json::from_value(metadata_device)
.expect("Could not deserialize device medatada");
(this_pkg.clone(), ledger_metadata, device_metadata)
} else {
println!("WARNING: 'package.metadata.ledger' section is missing in Cargo.toml, trying 'package.metadata.nanos'");
let nanos_section = this_pkg.metadata.get("nanos").expect(
"No appropriate [package.metadata.<ledger|nanos>] section found.",
);
let nanos_metadata: NanosMetadata =
serde_json::from_value(nanos_section.clone())
.expect("Could not deserialize medatada.nanos");
let ledger_metadata = LedgerMetadata {
curve: nanos_metadata.curve,
path: nanos_metadata.path,
flags: nanos_metadata.flags,
name: nanos_metadata.name,
};
let device_metadata = DeviceMetadata {
icon: match device {
Device::Nanos => nanos_metadata.icon,
_ => nanos_metadata.icon_small,
},
};
(this_pkg.clone(), ledger_metadata, device_metadata)
}
}
fn build_app(
device: Device,
is_load: bool,
use_prebuilt: Option<PathBuf>,
hex_next_to_json: bool,
remaining_args: Vec<String>,
) {
let exe_path = match use_prebuilt {
None => {
let mut cargo_cmd = Command::new("cargo")
.args([
"build",
"--release",
format!("--target={}", device.as_ref()).as_str(),
"--message-format=json-diagnostic-rendered-ansi",
])
.args(&remaining_args)
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut exe_path = PathBuf::new();
let out = cargo_cmd.stdout.take().unwrap();
let reader = std::io::BufReader::new(out);
for message in Message::parse_stream(reader) {
match message.as_ref().unwrap() {
Message::CompilerArtifact(artifact) => {
if let Some(n) = &artifact.executable {
exe_path = n.to_path_buf();
}
}
Message::CompilerMessage(message) => {
println!("{message}");
}
_ => (),
}
}
cargo_cmd.wait().expect("Couldn't get cargo's exit status");
exe_path
}
Some(prebuilt) => prebuilt.canonicalize().unwrap(),
};
let (this_pkg, metadata_ledger, metadata_device) =
retrieve_metadata(device, None);
let current_dir = this_pkg
.manifest_path
.parent()
.expect("Could not find package's parent path");
let hex_file_abs = if hex_next_to_json {
current_dir
} else {
exe_path.parent().unwrap()
}
.join("app.hex");
export_binary(&exe_path, &hex_file_abs);
let app_json_name = format!("app_{}.json", device.as_ref());
let app_json = current_dir.join(app_json_name);
let hex_file = hex_file_abs.strip_prefix(current_dir).unwrap();
let infos = retrieve_infos(&exe_path).unwrap();
let flags = match device {
Device::Nanos | Device::Nanosplus => metadata_ledger.flags,
Device::Nanox => {
let base = u32::from_str_radix(metadata_ledger.flags.as_str(), 16)
.unwrap_or(0);
format!("0x{:x}", base | 0x200)
}
};
let backup_targetid : String = match device {
Device::Nanos => String::from("0x31100004"),
Device::Nanox => String::from("0x33000004"),
Device::Nanosplus => String::from("0x33100004"),
};
let file = fs::File::create(&app_json).unwrap();
let mut json = json!({
"name": metadata_ledger.name.as_ref().unwrap_or(&this_pkg.name),
"version": &this_pkg.version,
"icon": metadata_device.icon,
"targetId": infos.target_id.unwrap_or(backup_targetid),
"flags": flags,
"derivationPath": {
"curves": metadata_ledger.curve,
"paths": metadata_ledger.path
},
"binary": hex_file,
"dataSize": infos.size
});
match device {
Device::Nanos => (),
_ => {
json["apiLevel"] = infos.api_level.into();
}
}
serde_json::to_writer_pretty(file, &json).unwrap();
let output_dir: Option<PathBuf> = remaining_args
.iter()
.position(|arg| arg == "--out-dir" || arg.starts_with("--out-dir="))
.and_then(|index| {
let out_dir_arg = &remaining_args[index];
if out_dir_arg.contains('=') {
Some(out_dir_arg.split('=').nth(1).unwrap().to_string())
} else {
remaining_args
.get(index + 1)
.map(|path_str| path_str.to_string())
}
})
.map(|path_str| PathBuf::from(path_str));
let exe_filename = exe_path.file_name().unwrap().to_str();
let exe_parent = exe_path.parent().unwrap().to_path_buf();
let apdu_file_path = output_dir.unwrap_or(exe_parent).join(exe_filename.unwrap()).with_extension("apdu");
dump_with_ledgerctl(current_dir, &app_json, apdu_file_path.to_str().unwrap());
if is_load {
install_with_ledgerctl(current_dir, &app_json);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_metadata() {
let (_, metadata_ledger, metadata_nanos) =
retrieve_metadata(Device::Nanos, Some("./tests/valid/Cargo.toml"));
assert_eq!(metadata_ledger.name, Some("TestApp".to_string()));
assert_eq!(metadata_ledger.curve, ["secp256k1"]);
assert_eq!(metadata_ledger.flags, "0x38");
assert_eq!(metadata_ledger.path, ["'44/123"]);
assert_eq!(metadata_nanos.icon, "./assets/nanos.gif")
}
#[test]
fn valid_metadata_variant() {
let (_, metadata_ledger, metadata_nanos) = retrieve_metadata(
Device::Nanos,
Some("./tests/valid_variant/Cargo.toml"),
);
assert_eq!(metadata_ledger.name, Some("TestApp".to_string()));
assert_eq!(metadata_ledger.curve, ["secp256k1"]);
assert_eq!(metadata_ledger.flags, "0x38");
assert_eq!(metadata_ledger.path, ["'44/123"]);
assert_eq!(metadata_nanos.icon, "./assets/nanos.gif")
}
#[test]
fn valid_outdated_metadata() {
let (_, metadata_ledger, metadata_nanos) = retrieve_metadata(
Device::Nanos,
Some("./tests/valid_outdated/Cargo.toml"),
);
assert_eq!(metadata_ledger.name, Some("TestApp".to_string()));
assert_eq!(metadata_ledger.curve, ["secp256k1"]);
assert_eq!(metadata_ledger.flags, "0");
assert_eq!(metadata_ledger.path, ["'44/123"]);
assert_eq!(metadata_nanos.icon, "nanos.gif")
}
}