pem

PEM (Privacy-Enhanced Mail) encoding and decoding library for MoonBit

pem
encoding
decoding
base64
crypto
Download zip
Author
Version
0.1.2
License
MIT
Last updated
11 months ago
Downloads
23

#PEM (Privacy-Enhanced Mail) Library

A MoonBit library for encoding and decoding PEM format data, commonly used for cryptographic keys and certificates.

#Basic Encoding

test "basic_encoding_example" {
let data = b"Hello, World!"
let block = PemBlock::new(label="MESSAGE", data~)
let encoded = encode(block)

inspect(encoded, content=(
#|-----BEGIN MESSAGE-----
#|SGVsbG8sIFdvcmxkIQ==
#|-----END MESSAGE-----
))
}

#Encoding with Headers

test "encoding_with_headers_example" {
let cert_data = b"Certificate data"
let headers : Map[String, String] = { "Version": "1.0", "Algorithm": "RSA" }
let block = PemBlock::new(label="CERTIFICATE", data=cert_data, headers~)
let encoded = encode(block)

// Verify the structure
inspect(string_starts_with(encoded, "-----BEGIN CERTIFICATE-----"), content="true")
inspect(encoded.contains("Version: 1.0"), content="true")
inspect(encoded.contains("Algorithm: RSA"), content="true")
inspect(string_ends_with(encoded, "-----END CERTIFICATE-----"), content="true")
}

#Basic Decoding

test "basic_decoding_example" {
let pem_data =
#|-----BEGIN MESSAGE-----
#|SGVsbG8sIFdvcmxkIQ==
#|-----END MESSAGE-----

let result = decode(pem_data)
match result {
Ok(block) => {
assert_eq(block.label, "MESSAGE")
assert_eq(block.data, "Hello, World!")
assert_eq(block.headers.size(), 0)
}
Err(error) => fail("Unexpected error: \{error}")
}
}

#Decoding with Headers

test "decoding_with_headers_example" {
let pem_data =
#|-----BEGIN CERTIFICATE-----
#|Version: 1.0
#|Algorithm: RSA
#|
#|Q2VydGlmaWNhdGUgZGF0YQ==
#|-----END CERTIFICATE-----

let result = decode(pem_data)
match result {
Ok(block) => {
assert_eq(block.label, "CERTIFICATE")
assert_eq(block.data, "Certificate data")
assert_eq(block.headers.get("Version"), Some("1.0"))
assert_eq(block.headers.get("Algorithm"), Some("RSA"))
}
Err(error) => fail("Unexpected error: \{error}")
}
}

#Decoding Multiple Blocks

test "multiple_blocks_example" {
let multi_pem =
#|-----BEGIN FIRST-----
#|Zmlyc3Q=
#|-----END FIRST-----
#|-----BEGIN SECOND-----
#|c2Vjb25k
#|-----END SECOND-----

let result = decode_many(multi_pem)
match result {
Ok(blocks) => {
assert_eq(blocks.length(), 2)
assert_eq(blocks[0].label, "FIRST")
assert_eq(blocks[0].data, "first")
assert_eq(blocks[1].label, "SECOND")
assert_eq(blocks[1].data, "second")
}
Err(error) => fail("Unexpected error: \{error}")
}
}

#Error Handling

test "error_handling_examples" {
// Test invalid format
let invalid_pem = "not a pem file"
let result1 = decode(invalid_pem)
match result1 {
Err(PemError::InvalidFormat(msg)) => {
inspect(msg, content="PEM data too short")
}
_ => fail("Should have failed with InvalidFormat")
}

// Test missing BEGIN marker
let no_begin = "SGVsbG8=\n-----END MESSAGE-----"
let result2 = decode(no_begin)
match result2 {
Err(PemError::InvalidFormat(_)) => () // Expected
_ => fail("Should detect missing BEGIN")
}

// Test mismatched labels
let mismatched = "-----BEGIN MESSAGE-----\nSGVsbG8=\n-----END CERTIFICATE-----"
let result3 = decode(mismatched)
match result3 {
Err(PemError::MismatchedLabels(begin, end)) => {
inspect(begin, content="MESSAGE")
inspect(end, content="CERTIFICATE")
}
_ => fail("Should detect label mismatch")
}

// Test invalid base64
let bad_base64 = "-----BEGIN MESSAGE-----\nInvalid@Base64!\n-----END MESSAGE-----"
let result4 = decode(bad_base64)
match result4 {
Err(PemError::InvalidBase64(_)) => () // Expected
_ => fail("Should detect invalid base64")
}
}

#Roundtrip Example

test "roundtrip_example" {
let original_data = b"Any binary data can be encoded in PEM format"
let headers : Map[String, String] = { "Type": "Example", "Version": "1.0" }

// Encode
let block = PemBlock::new(label="DATA", data=original_data, headers~)
let encoded = encode(block)

// Decode
let result = decode(encoded)
match result {
Ok(decoded) => {
assert_eq(decoded.label, "DATA")
assert_eq(decoded.data.to_string(), original_data.to_string())
assert_eq(decoded.headers.get("Type"), Some("Example"))
assert_eq(decoded.headers.get("Version"), Some("1.0"))
}
Err(error) => fail("Roundtrip failed: \{error}")
}
}

#Certificate Example

test "certificate_example" {
let cert_data = b"Mock X.509 certificate data"
let cert_block = PemBlock::new(label="CERTIFICATE", data=cert_data)
let pem_string = encode(cert_block)

// Verify certificate format
assert_true(string_starts_with(pem_string, "-----BEGIN CERTIFICATE-----"))
assert_true(string_ends_with(pem_string, "-----END CERTIFICATE-----"))

// Verify it can be decoded back
let result = decode(pem_string)
match result {
Ok(decoded) => {
assert_eq(decoded.label, "CERTIFICATE")
assert_eq(decoded.data, "Mock X.509 certificate data")
}
Err(error) => fail("Certificate roundtrip failed: \{error}")
}
}

#Private Key Example

test "private_key_example" {
let key_data = b"Mock RSA private key data"
let headers : Map[String, String] = {
"Proc-Type": "4,ENCRYPTED",
"DEK-Info": "AES-256-CBC,1234567890ABCDEF"
}
let key_block = PemBlock::new(label="RSA PRIVATE KEY", data=key_data, headers~)
let pem_string = encode(key_block)

// Verify private key format
inspect(string_starts_with(pem_string, "-----BEGIN RSA PRIVATE KEY-----"), content="true")
inspect(pem_string.contains("Proc-Type: 4,ENCRYPTED"), content="true")
inspect(pem_string.contains("DEK-Info: AES-256-CBC"), content="true")

// Verify headers are preserved
let result = decode(pem_string)
match result {
Ok(decoded) => {
assert_eq(decoded.headers.get("Proc-Type"), Some("4,ENCRYPTED"))
inspect(decoded.headers.get("DEK-Info") |> Option::map(fn(s) { string_starts_with(s, "AES-256-CBC") }), content="Some(true)")
}
Err(error) => fail("Private key roundtrip failed: \{error}")
}
}

#Certificate Chain Example

test "certificate_chain_example" {
// Create a certificate chain
let root_cert = PemBlock::new(label="CERTIFICATE", data=b"Root CA certificate")
let intermediate_cert = PemBlock::new(label="CERTIFICATE", data=b"Intermediate certificate")
let leaf_cert = PemBlock::new(label="CERTIFICATE", data=b"Leaf certificate")

// Combine into a chain
let chain_pem = encode(root_cert) + "\n" + encode(intermediate_cert) + "\n" + encode(leaf_cert)

// Parse the chain
let result = decode_many(chain_pem)
match result {
Ok(certificates) => {
assert_eq(certificates.length(), 3)
assert_eq(certificates[0].data, "Root CA certificate")
assert_eq(certificates[1].data, "Intermediate certificate")
assert_eq(certificates[2].data, "Leaf certificate")

// All should be certificates
for cert in certificates {
assert_eq(cert.label, "CERTIFICATE")
}
}
Err(error) => fail("Certificate chain parsing failed: \{error}")
}
}

#Long Data with Line Wrapping

test "long_data_line_wrapping_example" {
// Create data that will require line wrapping (more than 64 base64 chars)
let long_data = Bytes::from_array(Array::make(150, 65)) // 150 'A' bytes
let block = PemBlock::new(label="LONG DATA", data=long_data)
let encoded = encode(block)

// Verify line wrapping occurred
let lines = encoded.split("\n") |> Iter::to_array
inspect(lines.length() > 4, content="true") // Should have multiple data lines

// Verify roundtrip works with long data
let result = decode(encoded)
match result {
Ok(decoded) => {
inspect(decoded.data.length(), content="150")
inspect(decoded.label, content="LONG DATA")
// Verify all bytes are 'A' (65)
for i = 0; i < decoded.data.length(); i = i + 1 {
if decoded.data[i].to_int() != 65 {
fail("Unexpected byte at position \{i}")
}
}
}
Err(error) => fail("Long data roundtrip failed: \{error}")
}
}

#Mixed Block Types

test "mixed_block_types_example" {
// Create different types of PEM blocks
let cert = PemBlock::new(label="CERTIFICATE", data=b"Certificate data")
let pubkey = PemBlock::new(label="PUBLIC KEY", data=b"Public key data")
let privkey = PemBlock::new(label="PRIVATE KEY", data=b"Private key data")

// Combine different block types
let mixed_pem = encode(cert) + "\n" + encode(pubkey) + "\n" + encode(privkey)

// Parse all blocks
let result = decode_many(mixed_pem)
match result {
Ok(blocks) => {
inspect(blocks.length(), content="3")

// Verify each block type
assert_eq(blocks[0].label, "CERTIFICATE")
assert_eq(blocks[0].data, "Certificate data")

assert_eq(blocks[1].label, "PUBLIC KEY")
assert_eq(blocks[1].data, "Public key data")

assert_eq(blocks[2].label, "PRIVATE KEY")
assert_eq(blocks[2].data, "Private key data")
}
Err(error) => fail("Mixed blocks parsing failed: \{error}")
}
}

#Empty and Edge Cases

test "edge_cases_example" {
// Test empty data
let empty_block = PemBlock::new(label="EMPTY", data=b"")
let empty_encoded = encode(empty_block)

inspect(string_starts_with(empty_encoded, "-----BEGIN EMPTY-----"), content="true")
inspect(string_ends_with(empty_encoded, "-----END EMPTY-----"), content="true")

// Test minimal valid PEM
let minimal_pem =
#|-----BEGIN TEST-----
#|QQ==
#|-----END TEST-----

let result = decode(minimal_pem)
match result {
Ok(block) => {
assert_eq(block.label, "TEST")
assert_eq(block.data, "A")
}
Err(error) => fail("Minimal PEM failed: \{error}")
}

// Test PEM with only headers and no data should fail
let headers_only =
#|-----BEGIN HEADERS-----
#|Key: Value
#|-----END HEADERS-----

let result2 = decode(headers_only)
match result2 {
Err(PemError::EmptyData) => () // Expected
_ => fail("Should detect empty data")
}
}

PemBlock

pub(all) struct PemBlock {
label : String
data : Bytes
headers : Map[String, String]
}

A PEM block represents a single PEM-encoded data block
impl Eq for PemBlock
impl Show for PemBlock

PemBlock::new

fn PemBlock::new(label~ : String, data~ : Bytes, headers? : Map[String, String]) -> PemBlock

Create a new PEM block

PemError

pub(all) enum PemError {
InvalidFormat(String)
InvalidBase64(String)
MismatchedLabels(String, String)
EmptyData
InvalidLabel(String)
}

Error types for PEM operations
impl Eq for PemError
impl Show for PemError
impl ToJson for PemError

decode

fn decode(pem_data : String) -> Result[PemBlock, PemError]

Decode a PEM string to a PEM block

decode_many

fn decode_many(pem_data : String) -> Result[Array[PemBlock], PemError]

Decode multiple PEM blocks from a string

encode

fn encode(block : PemBlock) -> String

Encode a PEM block to string format

string_ends_with

fn string_ends_with(s : String, suffix : String) -> Bool

Helper function to check if string ends with suffix

string_starts_with

fn string_starts_with(s : String, prefix : String) -> Bool

Helper function to check if string starts with prefix

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io