// .proto file parser for proto3 syntax.
// Supports: message, enum, service, rpc, oneof, map fields, nested types,
// block and line comments.
///|
/// Represents a protobuf field type.
pub enum FieldType {
Int32
Int64
Uint32
Uint64
Sint32
Sint64
Fixed32
Fixed64
Sfixed32
Sfixed64
Float
Double
Bool
String
Bytes
Map(FieldType, FieldType)
Message(String)
Enum(String)
} derive(Debug, Eq)
///|
/// Represents a field label.
pub enum FieldLabel {
Optional
Required
Repeated
} derive(Debug, Eq)
///|
/// Represents a single field in a message.
pub struct ProtoField {
name : String
number : UInt
field_type : FieldType
label : FieldLabel
} derive(Debug, Eq)
///|
/// Represents an enum value.
pub struct ProtoEnumValue {
name : String
number : Int
} derive(Debug, Eq)
///|
/// Represents an enum definition.
pub struct ProtoEnum {
name : String
values : Array[ProtoEnumValue]
} derive(Debug, Eq)
///|
/// Represents a oneof group inside a message.
pub struct ProtoOneof {
name : String
fields : Array[ProtoField]
} derive(Debug, Eq)
///|
/// Represents an RPC method inside a service.
pub struct ProtoRpc {
name : String
input_type : String
output_type : String
client_streaming : Bool
server_streaming : Bool
} derive(Debug, Eq)
///|
/// Represents a service definition.
pub struct ProtoService {
name : String
rpcs : Array[ProtoRpc]
} derive(Debug, Eq)
///|
/// Represents a message definition.
pub struct ProtoMessage {
name : String
fields : Array[ProtoField]
oneofs : Array[ProtoOneof]
nested_messages : Array[ProtoMessage]
nested_enums : Array[ProtoEnum]
} derive(Debug, Eq)
///|
/// A parse error with a line position and message.
pub struct ProtoError {
line : Int
message : String
} derive(Debug, Eq)
///|
/// A parsed .proto file containing all definitions.
pub struct ProtoFile {
messages : Array[ProtoMessage]
enums : Array[ProtoEnum]
services : Array[ProtoService]
} derive(Debug, Eq)
///|
/// Format a parse error as a readable string.
pub fn format_proto_error(err : ProtoError) -> String {
"line \{err.line}: \{err.message}"
}
///|
/// Parse a .proto file content string into a ProtoFile.
pub fn parse_proto(content : String) -> Result[ProtoFile, ProtoError] {
let cleaned = strip_block_comments(content)
let lines = split_lines(cleaned)
let enum_names = collect_enum_names(lines)
let messages : Array[ProtoMessage] = []
let enums : Array[ProtoEnum] = []
let services : Array[ProtoService] = []
let mut i = 0
while i < lines.length() {
let line = trim_line(lines[i])
if line == "" || is_comment(line) || is_top_level_decl(line) {
i = i + 1
continue
}
if starts_with(line, "message ") {
match parse_message(lines, i, enum_names) {
None => return Err({ line: i + 1, message: "failed to parse message" })
Some((msg, next_i)) => {
messages.push(msg)
i = next_i
}
}
} else if starts_with(line, "enum ") {
match parse_enum(lines, i) {
None => return Err({ line: i + 1, message: "failed to parse enum" })
Some((e, next_i)) => {
enums.push(e)
i = next_i
}
}
} else if starts_with(line, "service ") {
match parse_service(lines, i) {
None => return Err({ line: i + 1, message: "failed to parse service" })
Some((s, next_i)) => {
services.push(s)
i = next_i
}
}
} else {
i = i + 1
}
}
Ok({ messages, enums, services })
}
// --- Comment stripping ---
///|
/// Remove `/* ... */` block comments (non-nested) from the source.
fn strip_block_comments(content : String) -> String {
let out = StringBuilder()
let mut i = 0
let mut seg_start = 0
while i < content.length() {
if i + 1 < content.length() && content[i:i + 2].to_owned() == "/*" {
out.write_string(content[seg_start:i].to_owned())
let close = find_str_from(content, i + 2, "*/")
if close < 0 {
return out.to_string()
}
i = close + 2
seg_start = i
} else {
i = i + 1
}
}
out.write_string(content[seg_start:].to_owned())
out.to_string()
}
///|
/// Find the first occurrence of `substr` in `s` at or after `start`.
fn find_str_from(s : String, start : Int, substr : String) -> Int {
if s.length() - start < substr.length() {
return -1
}
let end = s.length() - substr.length() + 1
for i in start.. Array[String] {
let names : Array[String] = []
for line in lines {
let t = trim_left(line)
if starts_with(t, "enum ") {
let rest = t[5:].to_owned()
let mut name = trim(rest)
if ends_with(name, "{") {
name = trim(name[:name.length() - 1].to_owned())
}
if name.length() > 0 && !contains_string(names, name) {
names.push(name)
}
}
}
names
}
// --- String helpers ---
///|
fn starts_with(s : String, prefix : String) -> Bool {
if s.length() < prefix.length() {
return false
}
s[:prefix.length()] == prefix
}
///|
fn ends_with(s : String, suffix : String) -> Bool {
if s.length() < suffix.length() {
return false
}
s[s.length() - suffix.length():] == suffix
}
///|
fn find_str(s : String, substr : String) -> Int {
find_str_from(s, 0, substr)
}
///|
fn find_char(s : String, target : Char) -> Int {
let mut idx = 0
for c in s {
if c == target {
return idx
}
idx = idx + 1
}
-1
}
///|
fn contains_string(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
///|
fn is_comment(line : String) -> Bool {
let t = trim_left(line)
starts_with(t, "//")
}
///|
fn is_top_level_decl(line : String) -> Bool {
let t = trim_left(line)
starts_with(t, "syntax ") ||
starts_with(t, "package ") ||
starts_with(t, "import ") ||
starts_with(t, "option ")
}
///|
fn trim_line(line : String) -> String {
let mut s = line
let comment_pos = find_str(s, "//")
if comment_pos >= 0 {
s = s[:comment_pos].to_owned()
}
trim(s)
}
///|
fn trim(s : String) -> String {
trim_right(trim_left(s))
}
///|
fn trim_left(s : String) -> String {
let mut start = 0
while start < s.length() && is_whitespace_char(s, start) {
start = start + 1
}
if start >= s.length() {
return ""
}
s[start:].to_owned()
}
///|
fn trim_right(s : String) -> String {
let mut end = s.length()
while end > 0 && is_whitespace_char(s, end - 1) {
end = end - 1
}
if end <= 0 {
return ""
}
s[:end].to_owned()
}
///|
fn is_whitespace_char(s : String, idx : Int) -> Bool {
let c = s[idx]
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
///|
fn split_lines(content : String) -> Array[String] {
let result : Array[String] = []
let mut start = 0
for i in 0.. (ProtoMessage, Int)? {
let header = trim_line(lines[start])
if !starts_with(header, "message ") {
return None
}
let rest = header[8:].to_owned()
let mut msg_name = trim(rest)
if ends_with(msg_name, "{") {
msg_name = trim(msg_name[:msg_name.length() - 1].to_owned())
}
let mut i = start
if !ends_with(header, "{") {
i = i + 1
while i < lines.length() && trim_line(lines[i]) != "{" {
i = i + 1
}
if i >= lines.length() {
return None
}
}
i = i + 1 // skip opening brace
let fields : Array[ProtoField] = []
let oneofs : Array[ProtoOneof] = []
let nested_msgs : Array[ProtoMessage] = []
let nested_enums : Array[ProtoEnum] = []
while i < lines.length() {
let line = trim_line(lines[i])
if line == "" || is_comment(line) {
i = i + 1
continue
}
if line == "}" {
i = i + 1
break
}
if starts_with(line, "message ") {
match parse_message(lines, i, enum_names) {
None => return None
Some((nested, next_i)) => {
nested_msgs.push(nested)
i = next_i
}
}
} else if starts_with(line, "enum ") {
match parse_enum(lines, i) {
None => return None
Some((e, next_i)) => {
nested_enums.push(e)
i = next_i
}
}
} else if starts_with(line, "oneof ") {
match parse_oneof(lines, i, enum_names) {
None => return None
Some((oneof, next_i)) => {
oneofs.push(oneof)
i = next_i
}
}
} else if find_str(line, "=") >= 0 {
match parse_field(line, enum_names) {
None => i = i + 1
Some(f) => {
fields.push(f)
i = i + 1
}
}
} else {
i = i + 1
}
}
Some(
(
{
name: msg_name,
fields,
oneofs,
nested_messages: nested_msgs,
nested_enums,
},
i,
),
)
}
///|
fn parse_oneof(
lines : Array[String],
start : Int,
enum_names : Array[String],
) -> (ProtoOneof, Int)? {
let header = trim_line(lines[start])
let rest = header[5:].to_owned() // "oneof " is 5 chars
let mut oneof_name = trim(rest)
if ends_with(oneof_name, "{") {
oneof_name = trim(oneof_name[:oneof_name.length() - 1].to_owned())
}
let mut i = start
if !ends_with(header, "{") {
i = i + 1
while i < lines.length() && trim_line(lines[i]) != "{" {
i = i + 1
}
if i >= lines.length() {
return None
}
}
i = i + 1
let fields : Array[ProtoField] = []
while i < lines.length() {
let line = trim_line(lines[i])
if line == "" || is_comment(line) {
i = i + 1
continue
}
if line == "}" {
i = i + 1
break
}
if find_str(line, "=") >= 0 {
match parse_field(line, enum_names) {
None => i = i + 1
Some(f) => {
fields.push(f)
i = i + 1
}
}
} else {
i = i + 1
}
}
Some(({ name: oneof_name, fields }, i))
}
///|
fn parse_enum(lines : Array[String], start : Int) -> (ProtoEnum, Int)? {
let header = trim_line(lines[start])
let rest = header[5:].to_owned() // "enum " is 5 chars
let mut enum_name = trim(rest)
if ends_with(enum_name, "{") {
enum_name = trim(enum_name[:enum_name.length() - 1].to_owned())
}
let mut i = start
if !ends_with(header, "{") {
i = i + 1
while i < lines.length() && trim_line(lines[i]) != "{" {
i = i + 1
}
if i >= lines.length() {
return None
}
}
i = i + 1
let values : Array[ProtoEnumValue] = []
while i < lines.length() {
let line = trim_line(lines[i])
if line == "" || is_comment(line) {
i = i + 1
continue
}
if line == "}" {
i = i + 1
break
}
let eq_pos = find_str(line, "=")
if eq_pos >= 0 {
let val_name = trim(line[:eq_pos].to_owned())
let after_eq = trim(line[eq_pos + 1:].to_owned())
let mut num_str = after_eq
if ends_with(num_str, ";") {
num_str = num_str[:num_str.length() - 1].to_owned()
}
num_str = trim(num_str)
match parse_int(num_str) {
None => i = i + 1
Some(n) => {
values.push({ name: val_name, number: n })
i = i + 1
}
}
} else {
i = i + 1
}
}
Some(({ name: enum_name, values }, i))
}
///|
fn parse_service(lines : Array[String], start : Int) -> (ProtoService, Int)? {
let header = trim_line(lines[start])
let rest = header[8:].to_owned() // "service " is 8 chars
let mut svc_name = trim(rest)
if ends_with(svc_name, "{") {
svc_name = trim(svc_name[:svc_name.length() - 1].to_owned())
}
let mut i = start
if !ends_with(header, "{") {
i = i + 1
while i < lines.length() && trim_line(lines[i]) != "{" {
i = i + 1
}
if i >= lines.length() {
return None
}
}
i = i + 1
let rpcs : Array[ProtoRpc] = []
while i < lines.length() {
let line = trim_line(lines[i])
if line == "" || is_comment(line) {
i = i + 1
continue
}
if line == "}" {
i = i + 1
break
}
if starts_with(line, "rpc ") {
match parse_rpc(line) {
None => ()
Some(rpc) => rpcs.push(rpc)
}
}
i = i + 1
}
Some(({ name: svc_name, rpcs }, i))
}
///|
fn parse_rpc(line : String) -> ProtoRpc? {
let t = trim_left(line)
let rest = t[4:].to_owned() // "rpc " is 4 chars
let open_paren = find_char(rest, '(')
if open_paren < 0 {
return None
}
let name = trim(rest[:open_paren].to_owned())
let close_paren = find_char(rest, ')')
if close_paren < 0 {
return None
}
let input_part = trim(rest[open_paren + 1:close_paren].to_owned())
let (input_type, client_streaming) = parse_rpc_type(input_part)
let after_close = rest[close_paren + 1:].to_owned()
let returns_pos = find_str(after_close, "returns")
if returns_pos < 0 {
return None
}
let ret_part = after_close[returns_pos + 7:].to_owned()
let ropen = find_char(ret_part, '(')
let rclose = find_char(ret_part, ')')
if ropen < 0 || rclose < 0 {
return None
}
let output_part = trim(ret_part[ropen + 1:rclose].to_owned())
let (output_type, server_streaming) = parse_rpc_type(output_part)
Some({ name, input_type, output_type, client_streaming, server_streaming })
}
///|
fn parse_rpc_type(part : String) -> (String, Bool) {
let t = trim_left(part)
if starts_with(t, "stream ") {
(trim(t[7:].to_owned()), true)
} else {
(t, false)
}
}
///|
fn parse_field(line : String, enum_names : Array[String]) -> ProtoField? {
let t = trim_left(line)
if starts_with(t, "map<") {
return parse_map_field(t)
}
let words = split_words(line)
let mut idx = 0
if words.length() < 3 {
return None
}
let mut label = FieldLabel::Optional
let first = words[0]
if first == "repeated" {
label = FieldLabel::Repeated
idx = 1
} else if first == "required" {
label = FieldLabel::Required
idx = 1
} else if first == "optional" {
idx = 1
}
if idx + 3 >= words.length() {
return None
}
let type_str = words[idx]
let name = words[idx + 1]
if words[idx + 2] != "=" {
return None
}
let num_str = words[idx + 3]
let number = parse_uint(num_str)
match number {
None => None
Some(n) => {
let ftype = parse_field_type(type_str, enum_names)
match ftype {
None => None
Some(ft) => Some({ name, number: n, field_type: ft, label })
}
}
}
}
///|
fn parse_map_field(line : String) -> ProtoField? {
let gt = find_char(line, '>')
if gt < 0 {
return None
}
let inner = line[4:gt].to_owned() // between "map<" and ">"
let comma = find_char(inner, ',')
if comma < 0 {
return None
}
let key_type_str = trim(inner[:comma].to_owned())
let value_type_str = trim(inner[comma + 1:].to_owned())
let key_type = parse_scalar_type(key_type_str)
let value_type = parse_field_type(value_type_str, [])
if key_type is None || value_type is None {
return None
}
let rest = line[gt + 1:].to_owned()
let words = split_words(rest)
if words.length() < 3 {
return None
}
let name = words[0]
if words[1] != "=" {
return None
}
match parse_uint(words[2]) {
None => None
Some(n) => {
let ft = FieldType::Map(key_type.unwrap(), value_type.unwrap())
Some({ name, number: n, field_type: ft, label: FieldLabel::Optional })
}
}
}
///|
fn split_words(line : String) -> Array[String] {
let words : Array[String] = []
let mut start = -1
for i in 0..= 0 {
words.push(line[start:i].to_owned())
start = -1
}
if c == '=' {
words.push("=")
}
} else if start < 0 {
start = i
}
}
if start >= 0 {
words.push(line[start:].to_owned())
}
words
}
///|
fn parse_field_type(s : String, enum_names : Array[String]) -> FieldType? {
match parse_scalar_type(s) {
Some(ft) => Some(ft)
None =>
if contains_string(enum_names, s) {
Some(FieldType::Enum(s))
} else {
Some(FieldType::Message(s))
}
}
}
///|
fn parse_scalar_type(s : String) -> FieldType? {
match s {
"int32" => Some(FieldType::Int32)
"int64" => Some(FieldType::Int64)
"uint32" => Some(FieldType::Uint32)
"uint64" => Some(FieldType::Uint64)
"sint32" => Some(FieldType::Sint32)
"sint64" => Some(FieldType::Sint64)
"fixed32" => Some(FieldType::Fixed32)
"fixed64" => Some(FieldType::Fixed64)
"sfixed32" => Some(FieldType::Sfixed32)
"sfixed64" => Some(FieldType::Sfixed64)
"float" => Some(FieldType::Float)
"double" => Some(FieldType::Double)
"bool" => Some(FieldType::Bool)
"string" => Some(FieldType::String)
"bytes" => Some(FieldType::Bytes)
_ => None
}
}
///|
fn parse_int(s : String) -> Int? {
if s.length() == 0 {
return None
}
let mut result = 0
let mut neg = false
let mut start = 0
if s[0] == '-' {
neg = true
start = 1
}
for i in start..= '0' && ch <= '9' {
result = result * 10 + (ch.to_int() - 48)
} else {
break
}
}
Some(if neg { -result } else { result })
}
///|
fn parse_uint(s : String) -> UInt? {
if s.length() == 0 {
return None
}
let mut result : UInt = 0
for i in 0..= '0' && ch <= '9' {
result = result * 10 + (ch.to_int() - 48).reinterpret_as_uint()
} else {
break
}
}
Some(result)
}