Skip to content

Commit ae13397

Browse files
rayketchamclaude
andcommitted
feat: PEM paste in shell, stdin pipe, raw base64 detection (#35, #36)
Three input methods now work: 1. Interactive shell PEM paste — paste any PEM block (cert, CSR, key, CRL, PKCS#7) and it auto-detects type and displays it 2. Stdin pipe — `cat cert.pem | pki show -` reads from stdin 3. Raw base64 — paste "MIIFqj..." without headers and it tries to decode as a certificate Fixed the PEM multi-line buffer to work inside the newline split loop so pasted PEM blocks are properly buffered until -----END. Closes #35 Closes #36 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 95aa098 commit ae13397

4 files changed

Lines changed: 98 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ jobs:
272272
set -euo pipefail
273273
VERSION_OUT=$(target/release/pki --version)
274274
echo "Version output: $VERSION_OUT"
275-
if ! echo "$VERSION_OUT" | grep -qF "0.6.0"; then
276-
echo "ERROR: version output did not contain expected '0.6.0'"
275+
if ! echo "$VERSION_OUT" | grep -qF "0.6.1"; then
276+
echo "ERROR: version output did not contain expected '0.6.1'"
277277
echo "Got: $VERSION_OUT"
278278
exit 1
279279
fi

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010
]
1111

1212
[workspace.package]
13-
version = "0.6.0"
13+
version = "0.6.1"
1414
edition = "2021"
1515
license = "Apache-2.0"
1616
rust-version = "1.88.0"

crates/pki-client/src/commands/show.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,51 @@ use crate::config::GlobalConfig;
2020
/// - `Ok(None)` if file type is Certificate (caller should handle with full options)
2121
/// - `Err(...)` on error
2222
pub fn auto_show(path: &Path, config: &GlobalConfig) -> Result<Option<CmdResult>> {
23-
let data =
24-
std::fs::read(path).with_context(|| format!("Failed to read file: {}", path.display()))?;
23+
// Support reading from stdin with "-"
24+
let data = if path == Path::new("-") {
25+
use std::io::Read;
26+
let mut buf = Vec::new();
27+
std::io::stdin()
28+
.read_to_end(&mut buf)
29+
.context("Failed to read from stdin")?;
30+
buf
31+
} else {
32+
std::fs::read(path)
33+
.with_context(|| format!("Failed to read file: {}", path.display()))?
34+
};
2535

2636
// Smart detection - analyzes content, tries parsing
2737
let detection = DetectedFileType::detect_with_confidence(&data, path);
2838
let file_type = detection.file_type;
2939

30-
show_by_type(path, &data, file_type, config)
40+
let result = show_by_type(path, &data, file_type, config);
41+
42+
// For stdin ("-"), never return None — write to temp file and show as cert
43+
if path == Path::new("-") {
44+
if let Ok(None) = result {
45+
let tmp = std::env::temp_dir().join(format!("pki-stdin-{}.pem", std::process::id()));
46+
std::fs::write(&tmp, &data)?;
47+
let show_result = super::cert::run(
48+
super::cert::CertCommands::Show(super::cert::ShowArgs {
49+
file: tmp.clone(),
50+
subject: false,
51+
san: false,
52+
issuer: false,
53+
check: false,
54+
issuer_cert: None,
55+
lint: false,
56+
interactive: false,
57+
all: false,
58+
no_chain: false,
59+
}),
60+
config,
61+
);
62+
let _ = std::fs::remove_file(&tmp);
63+
return show_result.map(Some);
64+
}
65+
}
66+
67+
result
3168
}
3269

3370
/// Get human-readable name for file type.

crates/pki-client/src/shell.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,44 @@ pub fn run(config: &GlobalConfig) -> Result<CmdResult> {
299299
continue;
300300
}
301301

302+
// If we're buffering PEM, keep adding lines
303+
if let Some(ref mut buffer) = multiline_buffer {
304+
buffer.push('\n');
305+
buffer.push_str(single_line);
306+
if single_line.contains("-----END") {
307+
let full_pem = buffer.clone();
308+
multiline_buffer = None;
309+
// Auto-show the pasted PEM
310+
if let Err(e) = show_inline_pem(&full_pem, config) {
311+
eprintln!("{}: {}", "error".red().bold(), e);
312+
}
313+
}
314+
continue;
315+
}
316+
302317
// Check if this line starts a multi-line PEM input
303-
if single_line.contains("-----BEGIN") && !single_line.contains("-----END") {
304-
multiline_buffer = Some(single_line.to_string());
318+
if single_line.contains("-----BEGIN") {
319+
if single_line.contains("-----END") {
320+
// Single-line PEM (unlikely but handle it)
321+
if let Err(e) = show_inline_pem(single_line, config) {
322+
eprintln!("{}: {}", "error".red().bold(), e);
323+
}
324+
} else {
325+
multiline_buffer = Some(single_line.to_string());
326+
}
327+
continue;
328+
}
329+
330+
// Detect raw base64 DER paste (e.g. "MIIFqj..." without PEM headers)
331+
if single_line.len() > 40
332+
&& single_line.starts_with("MII")
333+
&& single_line.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
334+
{
335+
// Wrap in PEM and show
336+
let pem = format!("-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----", single_line);
337+
if let Err(e) = show_inline_pem(&pem, config) {
338+
eprintln!("{}: {}", "error".red().bold(), e);
339+
}
305340
continue;
306341
}
307342

@@ -889,6 +924,24 @@ fn run_cli_command(args: &[String], config: &GlobalConfig) -> Result<()> {
889924

890925
/// Passthrough commands to the full CLI dispatcher for commands not
891926
/// explicitly handled in the shell (probe, diff, convert, scep, acme, est, etc.)
927+
/// Show inline PEM content — auto-detects type (cert, CSR, key, CRL, PKCS#7).
928+
fn show_inline_pem(pem_text: &str, config: &GlobalConfig) -> Result<()> {
929+
use std::io::Write;
930+
931+
// Write PEM to a temp file, show it, clean up
932+
let tmp_path = std::env::temp_dir().join(format!("pki-paste-{}.pem", std::process::id()));
933+
let mut f = std::fs::File::create(&tmp_path)?;
934+
f.write_all(pem_text.as_bytes())?;
935+
f.flush()?;
936+
drop(f);
937+
938+
let args = vec!["show".to_string(), tmp_path.display().to_string()];
939+
let result = run_cli_command(&args, config);
940+
941+
let _ = std::fs::remove_file(&tmp_path);
942+
result
943+
}
944+
892945
/// Split a command line respecting double and single quotes.
893946
fn shell_split(input: &str) -> Vec<String> {
894947
let mut args = Vec::new();

0 commit comments

Comments
 (0)