use std::{
ffi::OsString,
fs::File,
io::{BufRead, BufReader, Seek, SeekFrom, Write},
path::{Path, PathBuf},
process::ExitCode,
};
use clap::Parser;
use jam_program_blob_common::{
read_metadata, write_metadata, ConventionalMetadata, CrateInfo, ProgramBlob,
};
#[derive(Parser)]
#[clap(about, arg_required_else_help = true)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand, Clone, Debug)]
enum Commands {
ShowMeta {
#[clap(value_name = "FILE")]
path: PathBuf,
},
SetMeta {
#[clap(long)]
name: Option<String>,
#[clap(long)]
version: Option<String>,
#[clap(long)]
license: Option<String>,
#[clap(long = "author")]
authors: Option<Vec<String>>,
#[clap(value_name = "FILE")]
path: PathBuf,
},
GetPvm {
#[clap(value_name = "FILE")]
path: PathBuf,
#[clap()]
output_file: PathBuf,
},
BuildNullAuthorizer {
output: PathBuf,
},
}
fn main() -> ExitCode {
do_main().unwrap_or_else(|e| {
eprintln!("{e}");
ExitCode::FAILURE
})
}
const NULL_AUTHORIZER_EXPECTED_HASH: &str =
"f8d86b97d65319a078e5840f1614c296a5254217794dcc910e72ca174e3c2e86";
fn build_null_authorizer() -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let assembly = r"
pub @is_authorized_ext:
// Return empty auth output.
//
// Since the same registers are used for the inputs and the outputs
// we need to clear these to zero.
a0 = 0
a1 = 0
ret
";
let polkavm_blob = polkavm_common::assembler::assemble(assembly)
.map_err(|err| format!("PolkaVM assembler failed: {err}"))?;
let parts = polkavm_common::program::ProgramParts::from_bytes(polkavm_blob.into())
.map_err(|err| format!("failed to split PolkaVM blob into parts: {err}"))?;
let metadata = ConventionalMetadata::Info(CrateInfo {
name: "NULL Authorizer".into(),
version: "1".into(),
license: "CC0".into(),
authors: vec![],
});
let metadata = codec::Encode::encode(&metadata);
ProgramBlob::from_pvm(&parts, metadata.into())
.to_vec()
.map_err(|err| format!("failed to serialize JAM blob: {err}").into())
}
fn do_main() -> Result<ExitCode, Box<dyn std::error::Error + Send + Sync>> {
let args = Args::parse();
match args.command {
Commands::ShowMeta { path } => {
let file = File::open(&path).map_err(|e| format!("Failed to open {path:?}: {e}"))?;
let mut reader = BufReader::with_capacity(MAX_METADATA_LEN, file);
reader.fill_buf()?;
let ConventionalMetadata::Info(CrateInfo { name, version, license, authors }) =
read_metadata(&mut reader.buffer())
.map_err(|e| format!("Failed to read metadata: {e}"))?;
println!("Name: {name}");
println!("Version: {version}");
println!("License: {license}");
println!("Authors: {}", authors.join(", "));
},
Commands::SetMeta { name, version, license, authors, path } => {
let input_file =
File::open(&path).map_err(|e| format!("Failed to open {path:?}: {e}"))?;
let mut reader = BufReader::with_capacity(MAX_METADATA_LEN, input_file);
reader.fill_buf()?;
let mut slice = reader.buffer();
let old_len = slice.len();
let (metadata, metadata_len) = match read_metadata(&mut slice) {
Ok(ConventionalMetadata::Info(mut old_metadata)) => {
let new_len = slice.len();
let metadata_len = old_len - new_len;
if let Some(name) = name {
old_metadata.name = name;
}
if let Some(version) = version {
old_metadata.version = version;
}
if let Some(license) = license {
old_metadata.license = license;
}
if let Some(authors) = authors {
old_metadata.authors = authors;
}
(ConventionalMetadata::Info(old_metadata), metadata_len)
},
Err(_) => {
let metadata = ConventionalMetadata::Info(CrateInfo {
name: name.unwrap_or_default(),
version: version.unwrap_or_default(),
license: license.unwrap_or_default(),
authors: authors.unwrap_or_default(),
});
(metadata, 0)
},
};
let mut input_file = reader.into_inner();
input_file.seek(SeekFrom::Start(metadata_len as u64))?;
let tmp_file_name = get_tmp_file_name(&path).expect("File name exists");
let mut tmp_file = File::create(&tmp_file_name)
.map_err(|e| format!("Failed to create {tmp_file_name:?}: {e}"))?;
let mut metadata_bytes = Vec::new();
write_metadata(&metadata, &mut metadata_bytes)?;
tmp_file.write_all(&metadata_bytes)?;
std::io::copy(&mut input_file, &mut tmp_file)?;
std::fs::rename(&tmp_file_name, path)?;
},
Commands::GetPvm { path, output_file } => {
if ProgramBlob::from_bytes(
&std::fs::read(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?,
)
.is_some()
{
return Err("Can't get PVM blob from JAM binary".into());
}
let file = File::open(&path).map_err(|e| format!("Failed to open {path:?}: {e}"))?;
let mut reader = BufReader::with_capacity(MAX_METADATA_LEN, file);
reader.fill_buf()?;
let slice = &mut reader.buffer();
let old_len = slice.len();
let _meta =
read_metadata(slice).map_err(|e| format!("Failed to read metadata: {e}"))?;
let new_len = slice.len();
let metadata_len = old_len - new_len;
let mut file = reader.into_inner();
file.seek(SeekFrom::Start(metadata_len as u64))?;
let mut out_file = File::create(&output_file)
.map_err(|e| format!("Failed to create {output_file:?}: {e}"))?;
std::io::copy(&mut file, &mut out_file)?;
},
Commands::BuildNullAuthorizer { output } => {
let jam_blob = build_null_authorizer()?;
let hash = blake2b_simd::Params::new().hash_length(32).hash(&jam_blob);
eprintln!("Built canonical NULL authorizer; hash: 0x{}", hash.to_hex());
if hash.to_hex().to_string() != NULL_AUTHORIZER_EXPECTED_HASH {
eprintln!("ERROR: Hash mismatch: expected hash 0x{NULL_AUTHORIZER_EXPECTED_HASH}. This should never happen; please report this!");
return Ok(ExitCode::FAILURE);
}
std::fs::write(&output, jam_blob)
.map_err(|err| format!("failed to write to {}: {err}", output.display()))?;
eprintln!("Canonical NULL authorizer successfully written to '{}'.", output.display());
},
}
Ok(ExitCode::SUCCESS)
}
fn get_tmp_file_name(path: &Path) -> Option<PathBuf> {
let mut buf = PathBuf::new();
if let Some(dir) = path.parent() {
buf.push(dir);
}
let mut file_name = OsString::new();
file_name.push(".");
file_name.push(path.file_name()?);
file_name.push(".tmp");
Some(file_name.into())
}
const MAX_METADATA_LEN: usize = 2 * 4096;
#[test]
fn test_build_null_authorizer() {
let jam_blob = build_null_authorizer().unwrap();
let hash = blake2b_simd::Params::new().hash_length(32).hash(&jam_blob);
assert_eq!(hash.to_hex().to_string(), NULL_AUTHORIZER_EXPECTED_HASH);
}