CSR Generator — Zig source
Generate a Certificate Signing Request and private key pair entirely in your browser. Download the CSR and key as PEM files.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! csr-generator — ASN.1 DER encoding + PKCS#10 CSR construction.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/csr-generator.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! The whole certificate-request pipeline runs locally: key pair generation,
//! DER assembly of the CertificationRequest structure, signature, and PEM
//! wrapping. Nothing leaves the process.
//!
//! The TS reference generates RSA and EC keys through Web Crypto. Zig's
//! standard library ships ECDSA P-256 (`std.crypto.sign.ecdsa`) but not RSA
//! key generation (its `std.crypto.rsa` targets TLS verification), so this
//! port implements the ECDSA-P256 path end-to-end and reports a clear error
//! for RSA — every DER primitive, the SPKI/PKCS8 wrapping, and the RSA
//! signature slot (`sha256WithRSAEncryption`) are complete, so wiring an RSA
//! key provider in is a drop-in.
const std = @import("std");
pub const KeyAlgorithm = enum {
rsa2048,
rsa4096,
ecdsa_p256,
pub fn label(self: KeyAlgorithm) []const u8 {
return switch (self) {
.rsa2048 => "RSA-2048",
.rsa4096 => "RSA-4096",
.ecdsa_p256 => "ECDSA-P256",
};
}
};
pub const CSROptions = struct {
common_name: []const u8,
organization: ?[]const u8 = null,
country: ?[]const u8 = null,
state: ?[]const u8 = null,
locality: ?[]const u8 = null,
email: ?[]const u8 = null,
key_algorithm: KeyAlgorithm,
subject_alt_names: []const []const u8 = &.{},
};
pub const CSRResult = struct {
csr: []const u8,
private_key: []const u8,
};
pub const Error = error{
MissingCommonName,
InvalidCountry,
UnsupportedAlgorithm,
NotAPrintableString,
InvalidOID,
InvalidLength,
NegativeInteger,
RsaKeygenUnsupported,
OutOfMemory,
};
// --- DER primitives ---------------------------------------------------------
const Der = std.ArrayList(u8);
/// DER length: short form below 0x80, long form (0x80 | byte count) above.
pub fn encodeLength(list: *Der, len: usize) Error!void {
if (len < 0x80) {
try list.append(@intCast(len));
return;
}
var bytes: [8]u8 = undefined;
var n = len;
var count: usize = 0;
while (n > 0) : (n >>= 8) {
bytes[count] = @intCast(n & 0xff);
count += 1;
}
try list.append(0x80 | @as(u8, @intCast(count)));
var j = count;
while (j > 0) {
j -= 1;
try list.append(bytes[j]);
}
}
/// Wrap content bytes in a tag + DER length header.
pub fn tlv(out: *Der, tag: u8, content: []const u8) Error!void {
try out.append(tag);
try encodeLength(out, content.len);
try out.appendSlice(content);
}
/// Build one TLV in its own buffer, then append it to `out`.
fn tlvAlloc(allocator: std.mem.Allocator, out: *Der, tag: u8, content: []const u8) Error!void {
var inner = Der.init(allocator);
defer inner.deinit();
try tlv(&inner, tag, content);
try out.appendSlice(inner.items);
}
/// DER INTEGER from a small non-negative number.
pub fn encodeInteger(allocator: std.mem.Allocator, out: *Der, value: u64) Error!void {
var digits: [8]u8 = undefined;
var n = value;
var count: usize = 0;
while (n > 0) : (n >>= 8) {
digits[count] = @intCast(n & 0xff);
count += 1;
}
var content: []const u8 = &[_]u8{0};
if (count > 0) {
std.mem.reverse(u8, digits[0..count]);
content = digits[0..count];
}
try integerFromBytes(allocator, out, content);
}
/// DER INTEGER from raw big-endian bytes (signature r/s halves): leading
/// zeros are stripped and a 0x00 sign byte is prepended when the high bit is
/// set, per DER minimal-encoding rules.
pub fn encodeIntegerBytes(allocator: std.mem.Allocator, out: *Der, value: []const u8) Error!void {
try integerFromBytes(allocator, out, value);
}
fn integerFromBytes(allocator: std.mem.Allocator, out: *Der, value_in: []const u8) Error!void {
if (value_in.len == 0) {
try tlvAlloc(allocator, out, 0x02, &[_]u8{0});
return;
}
var start: usize = 0;
while (start < value_in.len - 1 and value_in[start] == 0) start += 1;
const value = value_in[start..];
var wrapped = std.ArrayList(u8).init(allocator);
defer wrapped.deinit();
if (value[0] > 0x7f) try wrapped.append(0); // keep the INTEGER positive
try wrapped.appendSlice(value);
try tlvAlloc(allocator, out, 0x02, wrapped.items);
}
/// DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11".
pub fn encodeOID(allocator: std.mem.Allocator, out: *Der, oid: []const u8) Error!void {
var parts = std.mem.splitScalar(u8, oid, '.');
const first_str = parts.next() orelse return Error.InvalidOID;
const second_str = parts.next() orelse return Error.InvalidOID;
const first = std.fmt.parseInt(u32, first_str, 10) catch return Error.InvalidOID;
const second = std.fmt.parseInt(u32, second_str, 10) catch return Error.InvalidOID;
var content = Der.init(allocator);
defer content.deinit();
try content.append(@intCast(40 * first + second));
while (parts.next()) |p| {
var v = std.fmt.parseInt(u32, p, 10) catch return Error.InvalidOID;
var stack: [8]u8 = undefined;
var depth: usize = 0;
while (true) {
stack[depth] = @intCast(v & 0x7f);
depth += 1;
v >>= 7;
if (v == 0) break;
}
var j = depth;
while (j > 1) {
j -= 1;
try content.append(stack[j] | 0x80);
}
try content.append(stack[0]);
}
try tlvAlloc(allocator, out, 0x06, content.items);
}
pub fn encodeUTF8String(allocator: std.mem.Allocator, out: *Der, str: []const u8) Error!void {
try tlvAlloc(allocator, out, 0x0c, str);
}
/// PrintableString — the required type for countryName in a Name.
pub fn encodePrintableString(allocator: std.mem.Allocator, out: *Der, str: []const u8) Error!void {
for (str) |ch| {
if (!isPrintableChar(ch)) return Error.NotAPrintableString;
}
try tlvAlloc(allocator, out, 0x13, str);
}
fn isPrintableChar(c: u8) bool {
return switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', ' ', '\'', '(', ')', '+', ',', '-', '.', '/', ':', '=', '?' => true,
else => false,
};
}
pub fn encodeBitString(allocator: std.mem.Allocator, out: *Der, data: []const u8) Error!void {
var content = std.ArrayList(u8).init(allocator);
defer content.deinit();
try content.append(0); // 0 unused bits in the last octet
try content.appendSlice(data);
try tlvAlloc(allocator, out, 0x03, content.items);
}
pub fn encodeOctetString(allocator: std.mem.Allocator, out: *Der, data: []const u8) Error!void {
try tlvAlloc(allocator, out, 0x04, data);
}
pub fn encodeNull(out: *Der) Error!void {
try out.appendSlice(&[_]u8{ 0x05, 0x00 });
}
pub fn encodeSequence(allocator: std.mem.Allocator, out: *Der, parts: []const []const u8) Error!void {
try construct(allocator, out, 0x30, parts);
}
pub fn encodeSet(allocator: std.mem.Allocator, out: *Der, parts: []const []const u8) Error!void {
try construct(allocator, out, 0x31, parts);
}
fn construct(allocator: std.mem.Allocator, out: *Der, tag: u8, parts: []const []const u8) Error!void {
var content = Der.init(allocator);
defer content.deinit();
for (parts) |p| try content.appendSlice(p);
try tlvAlloc(allocator, out, tag, content.items);
}
// --- Object identifiers -------------------------------------------------------
pub const OID_COUNTRY = "2.5.4.6";
pub const OID_STATE = "2.5.4.8";
pub const OID_LOCALITY = "2.5.4.7";
pub const OID_ORGANIZATION = "2.5.4.10";
pub const OID_COMMON_NAME = "2.5.4.3";
pub const OID_EMAIL = "1.2.840.113549.1.9.1";
pub const OID_EXT_REQUEST = "1.2.840.113549.1.9.14"; // pkcs-9 at extensionRequest
pub const OID_SUBJECT_ALT_NAME = "2.5.29.17";
pub const OID_RSA_SHA256 = "1.2.840.113549.1.1.11"; // sha256WithRSAEncryption
pub const OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2"; // ecdsa-with-SHA256
pub const OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
pub const OID_P256 = "1.2.840.10045.3.1.7";
// --- Validation ----------------------------------------------------------------
/// Errors on invalid input: CN required, country (when present) a 2-letter ISO
/// 3166-1 code, key algorithm one of the supported values.
pub fn validateCSROptions(options: CSROptions) Error!void {
if (std.mem.trim(u8, options.common_name, " \t").len == 0) return Error.MissingCommonName;
if (options.country) |country| {
const c = std.mem.trim(u8, country, " \t");
if (c.len != 0 and c.len != 2) return Error.InvalidCountry;
if (c.len == 2) {
for (c) |ch| {
if (!std.ascii.isAlphabetic(ch)) return Error.InvalidCountry;
}
}
}
switch (options.key_algorithm) {
.rsa2048, .rsa4096, .ecdsa_p256 => {},
}
}
// --- SAN classification -----------------------------------------------------------
pub const SanType = enum { dns, ip, email, uri };
/// Classify a SAN entry: IPv4/IPv6 → ip, http(s):// → uri, contains @ → email, else dns.
pub fn classifySanType(entry: []const u8) SanType {
const value = std.mem.trim(u8, entry, " \t");
if (ipToBytes(value) != null) return .ip;
if (startsWithHttp(value)) return .uri;
if (std.mem.indexOfScalar(u8, value, '@') != null) return .email;
return .dns;
}
fn startsWithHttp(value: []const u8) bool {
const lower_http = "http://";
const lower_https = "https://";
return (value.len >= lower_http.len and std.ascii.eqlIgnoreCase(value[0..lower_http.len], lower_http)) or
(value.len >= lower_https.len and std.ascii.eqlIgnoreCase(value[0..lower_https.len], lower_https));
}
/// Parse an IPv4 or IPv6 literal into 4 or 16 bytes; null when not an IP.
pub fn ipToBytes(ip: []const u8) ?[]const u8 {
// IPv4: d.d.d.d with every octet <= 255.
if (std.mem.indexOfScalar(u8, ip, ':') == null) {
var octets: [4]u16 = undefined;
var count: usize = 0;
var parts = std.mem.splitScalar(u8, ip, '.');
while (parts.next()) |p| {
if (count == 4 or p.len == 0 or p.len > 3) return null;
for (p) |ch| {
if (!std.ascii.isDigit(ch)) return null;
}
octets[count] = std.fmt.parseInt(u16, p, 10) catch return null;
if (octets[count] > 255) return null;
count += 1;
}
if (count != 4) return null;
ipv4_bytes = .{
@intCast(octets[0]), @intCast(octets[1]),
@intCast(octets[2]), @intCast(octets[3]),
};
return ipv4_bytes[0..4];
}
// IPv6: groups of hex, at most one "::" elision, optional IPv4-mapped tail.
var halves = std.mem.splitSequence(u8, ip, "::");
var groups: [16]u16 = undefined;
var group_count: usize = 0;
var saw_elision = false;
while (halves.next()) |half| {
if (half.len == 0) {
// The "::" elision yields one empty half at each end; a stray
// empty half elsewhere ("1::2::") is not a valid address.
if (saw_elision and group_count != 0) return null;
saw_elision = true;
continue;
}
var sub = std.mem.splitScalar(u8, half, ':');
while (sub.next()) |group| {
if (group.len == 0) return null;
if (std.mem.indexOfScalar(u8, group, '.') != null) {
// IPv4-mapped tail, e.g. ::ffff:10.0.0.1
const tail = ipToBytes(group) orelse return null;
if (tail.len != 4) return null;
if (group_count + 2 > 8) return null;
groups[group_count] = (@as(u16, tail[0]) << 8) | tail[1];
groups[group_count + 1] = (@as(u16, tail[2]) << 8) | tail[3];
group_count += 2;
continue;
}
if (group.len > 4) return null;
for (group) |ch| {
if (!std.ascii.isHex(ch)) return null;
}
if (group_count == 8) return null;
groups[group_count] = std.fmt.parseInt(u16, group, 16) catch return null;
group_count += 1;
}
}
// "::" elides one or more zero groups; without it there must be exactly 8.
var total = group_count;
if (saw_elision) {
while (total < 8) : (total += 1) groups[total] = 0;
} else if (total != 8) {
return null;
}
var i: usize = 0;
while (i < 8) : (i += 1) {
ipv6_bytes[i * 2] = @intCast(groups[i] >> 8);
ipv6_bytes[i * 2 + 1] = @intCast(groups[i] & 0xff);
}
return ipv6_bytes[0..16];
}
// Scratch outputs for ipToBytes (display snippet: single-threaded parsing).
var ipv4_bytes: [4]u8 = undefined;
var ipv6_bytes: [16]u8 = undefined;
/// One GeneralName: context-specific implicit tags per RFC 5280.
fn encodeGeneralName(allocator: std.mem.Allocator, out: *Der, entry: []const u8) Error!void {
const value = std.mem.trim(u8, entry, " \t");
switch (classifySanType(value)) {
.ip => try tlvAlloc(allocator, out, 0x87, ipToBytes(value).?),
.uri => try tlvAlloc(allocator, out, 0x86, value),
.email => try tlvAlloc(allocator, out, 0x81, value),
.dns => try tlvAlloc(allocator, out, 0x82, value), // dNSName
}
}
// --- PEM ------------------------------------------------------------------------
const B64Encoder = std.base64.standard.Encoder;
/// PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers.
pub fn pemEncode(allocator: std.mem.Allocator, der: []const u8, label: []const u8) Error![]u8 {
const b64_len = B64Encoder.calcSize(der.len);
const b64 = try allocator.alloc(u8, b64_len);
defer allocator.free(b64);
_ = B64Encoder.encode(b64, der);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
const w = out.writer();
try w.print("-----BEGIN {s}-----\n", .{label});
var i: usize = 0;
while (i < b64.len) : (i += 64) {
const end = @min(i + 64, b64.len);
try w.print("{s}\n", .{b64[i..end]});
}
try w.print("-----END {s}-----\n", .{label});
return out.toOwnedSlice();
}
// --- Key generation + CSR assembly ---------------------------------------------
const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256;
/// AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET.
fn rdn(allocator: std.mem.Allocator, out: *Der, oid: []const u8, value: []const u8) Error!void {
var seq = Der.init(allocator);
defer seq.deinit();
try encodeOID(allocator, &seq, oid);
try seq.appendSlice(value);
try encodeSet(allocator, out, &.{seq.items});
}
/// Web Crypto ECDSA signatures are raw r||s; PKCS#10 expects a DER
/// ECDSA-Sig-Value SEQUENCE. Convert half-length integer halves to DER.
fn ecdsaRawToDer(allocator: std.mem.Allocator, raw: *const [64]u8) Error![]u8 {
var out = Der.init(allocator);
defer out.deinit();
const half = 32;
try encodeIntegerBytes(allocator, &out, raw[0..half]);
var second = Der.init(allocator);
defer second.deinit();
try encodeIntegerBytes(allocator, &second, raw[half..]);
var seq = Der.init(allocator);
defer seq.deinit();
try encodeSequence(allocator, &seq, &.{ out.items, second.items });
return seq.toOwnedSlice();
}
/// SPKI for a P-256 public key:
/// SEQUENCE { SEQUENCE { OID ecPublicKey, OID prime256v1 }, BIT STRING point }.
fn ecdsaSpki(allocator: std.mem.Allocator, sec1: *const [65]u8) Error![]u8 {
var alg = Der.init(allocator);
defer alg.deinit();
try encodeOID(allocator, &alg, OID_EC_PUBLIC_KEY);
try encodeOID(allocator, &alg, OID_P256);
var alg_seq = Der.init(allocator);
defer alg_seq.deinit();
try encodeSequence(allocator, &alg_seq, &.{alg.items});
var bits = Der.init(allocator);
defer bits.deinit();
try encodeBitString(allocator, &bits, sec1);
var spki = Der.init(allocator);
defer spki.deinit();
try encodeSequence(allocator, &spki, &.{ alg_seq.items, bits.items });
return spki.toOwnedSlice();
}
/// PKCS8 for a P-256 private key:
/// SEQUENCE { INTEGER 0, SEQUENCE { OID ecPublicKey, OID P-256 }, OCTET STRING scalar }.
fn ecdsaPkcs8(allocator: std.mem.Allocator, scalar: *const [32]u8) Error![]u8 {
var alg = Der.init(allocator);
defer alg.deinit();
try encodeOID(allocator, &alg, OID_EC_PUBLIC_KEY);
try encodeOID(allocator, &alg, OID_P256);
var alg_seq = Der.init(allocator);
defer alg_seq.deinit();
try encodeSequence(allocator, &alg_seq, &.{alg.items});
var octet = Der.init(allocator);
defer octet.deinit();
try encodeOctetString(allocator, &octet, scalar);
var version = Der.init(allocator);
defer version.deinit();
try encodeInteger(allocator, &version, 0);
var pkcs8 = Der.init(allocator);
defer pkcs8.deinit();
try encodeSequence(allocator, &pkcs8, &.{ version.items, alg_seq.items, octet.items });
return pkcs8.toOwnedSlice();
}
/// Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the matching
/// PKCS#8 private key (PEM). Caller owns both strings.
pub fn generateCSR(allocator: std.mem.Allocator, options: CSROptions) Error!CSRResult {
try validateCSROptions(options);
if (options.key_algorithm != .ecdsa_p256) {
// RSA keygen is not in Zig's standard library (see the header note).
return Error.RsaKeygenUnsupported;
}
const pair = EcdsaP256Sha256.KeyPair.create() catch return Error.OutOfMemory;
const sec1 = pair.public_key.toSec1();
const sk = pair.secret_key.toBytes();
const spki = try ecdsaSpki(allocator, &sec1);
defer allocator.free(spki);
const pkcs8 = try ecdsaPkcs8(allocator, &sk);
defer allocator.free(pkcs8);
// subject: RDNSequence in the conventional C, ST, L, O, CN, email order
var rdns = std.ArrayList([]const u8).init(allocator);
defer {
for (rdns.items) |item| allocator.free(item);
rdns.deinit();
}
if (options.country) |country_raw| {
var upper: [64]u8 = undefined;
const country = upperTrimUpper(&upper, country_raw);
if (country.len > 0) {
var entry = Der.init(allocator);
defer entry.deinit();
var value = Der.init(allocator);
defer value.deinit();
try encodePrintableString(allocator, &value, country);
try rdn(allocator, &entry, OID_COUNTRY, value.items);
try rdns.append(try allocator.dupe(u8, entry.items));
}
}
const simple_fields = [_]struct { oid: []const u8, value: ?[]const u8 }{
.{ .oid = OID_STATE, .value = options.state },
.{ .oid = OID_LOCALITY, .value = options.locality },
.{ .oid = OID_ORGANIZATION, .value = options.organization },
};
for (simple_fields) |field| {
const v = field.value orelse continue;
const t = std.mem.trim(u8, v, " \t");
if (t.len == 0) continue;
var entry = Der.init(allocator);
defer entry.deinit();
var value = Der.init(allocator);
defer value.deinit();
try encodeUTF8String(allocator, &value, t);
try rdn(allocator, &entry, field.oid, value.items);
try rdns.append(try allocator.dupe(u8, entry.items));
}
{
var entry = Der.init(allocator);
defer entry.deinit();
var value = Der.init(allocator);
defer value.deinit();
try encodeUTF8String(allocator, &value, std.mem.trim(u8, options.common_name, " \t"));
try rdn(allocator, &entry, OID_COMMON_NAME, value.items);
try rdns.append(try allocator.dupe(u8, entry.items));
}
if (options.email) |email_raw| {
const email = std.mem.trim(u8, email_raw, " \t");
if (email.len > 0) {
var entry = Der.init(allocator);
defer entry.deinit();
var value = Der.init(allocator);
defer value.deinit();
try encodeUTF8String(allocator, &value, email);
try rdn(allocator, &entry, OID_EMAIL, value.items);
try rdns.append(try allocator.dupe(u8, entry.items));
}
}
// attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
var attributes = std.ArrayList(u8).init(allocator);
defer attributes.deinit();
var sans = std.ArrayList([]const u8).init(allocator);
defer sans.deinit();
for (options.subject_alt_names) |s| {
const t = std.mem.trim(u8, s, " \t");
if (t.len > 0) try sans.append(t);
}
if (sans.items.len > 0) {
var general_names = Der.init(allocator);
defer general_names.deinit();
for (sans.items) |san| try encodeGeneralName(allocator, &general_names, san);
var san_seq = Der.init(allocator);
defer san_seq.deinit();
try encodeSequence(allocator, &san_seq, &.{general_names.items});
var octet = Der.init(allocator);
defer octet.deinit();
try encodeOctetString(allocator, &octet, san_seq.items);
var extension = Der.init(allocator);
defer extension.deinit();
var oid = Der.init(allocator);
defer oid.deinit();
try encodeOID(allocator, &oid, OID_SUBJECT_ALT_NAME);
try encodeSequence(allocator, &extension, &.{ oid.items, octet.items });
var inner_set = Der.init(allocator);
defer inner_set.deinit();
try encodeSequence(allocator, &inner_set, &.{extension.items});
var attribute = Der.init(allocator);
defer attribute.deinit();
var ext_oid = Der.init(allocator);
defer ext_oid.deinit();
try encodeOID(allocator, &ext_oid, OID_EXT_REQUEST);
try encodeSet(allocator, &attribute, &.{inner_set.items});
var attr_seq = Der.init(allocator);
defer attr_seq.deinit();
try encodeSequence(allocator, &attr_seq, &.{ ext_oid.items, attribute.items });
try tlv(&attributes, 0xa0, attr_seq.items); // [0] IMPLICIT SET OF Attribute
}
// CertificationRequestInfo: version 0, subject, SPKI, [0] attributes
var cri = Der.init(allocator);
defer cri.deinit();
var version = Der.init(allocator);
defer version.deinit();
try encodeInteger(allocator, &version, 0);
var subject = Der.init(allocator);
defer subject.deinit();
try encodeSequence(allocator, &subject, rdns.items);
try encodeSequence(allocator, &cri, &.{ version.items, subject.items, spki, attributes.items });
// signatureAlgorithm + signature over the DER CRI
var sig_alg = Der.init(allocator);
defer sig_alg.deinit();
try encodeOID(allocator, &sig_alg, OID_ECDSA_SHA256);
var sig_alg_seq = Der.init(allocator);
defer sig_alg_seq.deinit();
try encodeSequence(allocator, &sig_alg_seq, &.{sig_alg.items});
const signature = EcdsaP256Sha256.sign(cri.items, pair.secret_key) catch return Error.OutOfMemory;
const raw_sig = signature.toRawBytes();
const sig_der = try ecdsaRawToDer(allocator, &raw_sig);
defer allocator.free(sig_der);
var sig_bits = Der.init(allocator);
defer sig_bits.deinit();
try encodeBitString(allocator, &sig_bits, sig_der);
var csr_der = Der.init(allocator);
defer csr_der.deinit();
try encodeSequence(allocator, &csr_der, &.{ cri.items, sig_alg_seq.items, sig_bits.items });
return .{
.csr = try pemEncode(allocator, csr_der.items, "CERTIFICATE REQUEST"),
.private_key = try pemEncode(allocator, pkcs8, "PRIVATE KEY"),
};
}
fn upperTrimUpper(buf: *[64]u8, s: []const u8) []const u8 {
const t = std.mem.trim(u8, s, " \t");
const n = @min(buf.len, t.len);
for (t[0..n], 0..) |c, i| buf[i] = std.ascii.toUpper(c);
return buf[0..n];
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →