IP Address Parser
Parse and validate IPv4 and IPv6 addresses, supporting various formats including compressed IPv6 notation and IPv4-mapped IPv6 addresses.
Key Concepts
- octet validation
- hexadecimal parsing
- zero compression
- address families
- range validation
Why Parse This Format?
IP address parsing is fundamental for network applications, security tools, log analysis, and access control systems. Proper validation prevents security issues from malformed addresses.
Use Cases
- Network security and firewall rules
- Log file analysis and IP extraction
- Geolocation services
- Access control and rate limiting
- Network configuration validation
Input Format
IPv4 addresses use dotted decimal notation (four octets 0-255). IPv6 addresses use eight groups of hexadecimal digits, with :: compression for consecutive zero groups.
IPv4: n.n.n.n | IPv6: hhhh:hhhh:hhhh:hhhh:hhhh:hhhh:hhhh:hhhhExamples
192.168.1.1VALIDPrivate IPv4 address
0.0.0.0VALIDWildcard IPv4 address
255.255.255.255VALIDBroadcast IPv4 address
127.0.0.1VALIDLoopback IPv4 address
::1VALIDLoopback IPv6 address
2001:db8::1VALIDIPv6 with :: compression
fe80::1VALIDLink-local IPv6 address
256.1.1.1INVALIDInvalid: octet > 255
1.2.3INVALIDInvalid: incomplete IPv4
Output Format
The parser returns an IpAddress enum distinguishing between IPv4 and IPv6, with optional component extraction for IPv4 octets.
Structure
typeIpAddress enum with variants: Ipv4(String), Ipv6(String)
Ipv4Can be decomposed into [u8; 4] octets
Ipv6Can be decomposed into hex groups
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
IPv4 Octet Parsing
.rule("octet", re(r#"[0-9]{1,3}"#))
.rule("ipv4", seq(vec![
dynamic(octet), dynamic(str(".")),
dynamic(octet), dynamic(str(".")),
dynamic(octet), dynamic(str(".")),
dynamic(octet),
]))Each IPv4 octet is 0-255 The regex matches 1-3 digits, with post-parse validation ensuring 0-255 range.
IPv4 Validation
fn validate_ipv4(octets: &[u8; 4]) -> Result<(), String> {
for &octet in octets {
if *octet > 255 {
return Err(format!("Octet {} exceeds 255", octet));
}
}
Ok(())
}After parsing, octets are validated: Leading zeros are preserved in parsing but cause ambiguity (e.g., `01` vs `1`).
IPv6 Full Format
.rule("ipv6_full", seq(vec![
dynamic(re("[0-9a-fA-F]{4}")),
dynamic(seq(vec![dynamic(str(":")), dynamic(re("[0-9a-fA-F]{4}"))]).repeat(7)),
]))IPv6 has 8 groups of 4 hex digits: Each group represents 16 bits, totaling 128 bits for the address.
IPv6 Compressed Format
.rule("ipv6_compressed", seq(vec![
dynamic(re("[0-9a-fA-F]{0,4}")),
dynamic(str("::")),
dynamic(re("[0-9a-fA-F]{0,4}")),
]))The `::` notation compresses consecutive zero groups `::1` is the loopback (equivalent to `0:0:0:0:0:0:0:0:0:1`), `::` alone is the unspecified address.
IPv6 Expansion
fn expand_ipv6(compressed: &str) -> [u16; 8] {
let parts: Vec<&str> = compressed.split("::").collect();
let left: Vec<u16> = parts[0].split(':').filter_map(|p| u16::from_str_radix(p) 16).ok()).collect();
let right: Vec<u16> = parts[1].split(':').filter_map(|p| u16::from_str_radix(p) 16).ok()).collect();
let zeros = 8 - left.len() - right.len();
let mut result = left;
result.extend(vec![0; zeros / 2]);
result.extend(right);
result
}Compressed addresses are expanded for comparison Expansion normalizes addresses for consistent comparison.
IPv6 Expansion
fn expand_ipv6(compressed: &str) -> [u16; 8] {
let parts: Vec<&str> = compressed.split("::").collect();
let left: Vec<u16> = parts[0].split(':').filter_map(|p| u16::from_str_radix(p) 16).ok()).collect();
let right: Vec<u16> = parts[1].split(':').filter_map(|p| u16::from_str_radix(p) 16).ok()).collect();
let zeros = 8 - left.len() - right.len();
let mut result = left;
result.extend(vec![0; zeros / 2]);
result.extend(right);
result
}Compressed addresses are expanded for comparison Expansion normalizes addresses for consistent comparison.
Source Code
//! IP Address Parser Example
//!
//! This example demonstrates parsing IPv4 and IPv6 addresses.
//! Based on the Parslet ip_address.rb example.
//!
//! Run with: cargo run --example ip_address --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder},
AstArena, Grammar, PortableParser,
};
/// Build an IPv4 grammar
fn build_ipv4_grammar() -> Grammar {
GrammarBuilder::new()
// Single digit
.rule("digit", re("[0-9]"))
// Dec octet: 0-255
// 25[0-5] | 2[0-4][0-9] | 1[0-9][0-9] | [1-9][0-9] | [0-9]
.rule(
"dec_octet",
choice(vec![
dynamic(re("25[0-5]")), // 250-255
dynamic(re("2[0-4][0-9]")), // 200-249
dynamic(re("1[0-9][0-9]")), // 100-199
dynamic(re("[1-9][0-9]")), // 10-99
dynamic(re("[0-9]")), // 0-9
]),
)
// IPv4: dec_octet.dec_octet.dec_octet.dec_octet
.rule(
"ipv4",
seq(vec![
dynamic(re("(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])")),
dynamic(str(".")),
dynamic(re("(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])")),
dynamic(str(".")),
dynamic(re("(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])")),
dynamic(str(".")),
dynamic(re("(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])")),
]),
)
.build()
}
/// Build an IPv6 grammar (simplified)
fn build_ipv6_grammar() -> Grammar {
GrammarBuilder::new()
// Hex digit
.rule("hexdigit", re("[0-9a-fA-F]"))
// h16: 1-4 hex digits
.rule("h16", re("[0-9a-fA-F]{1,4}"))
// ls32: h16:h16 or IPv4
.rule("ls32", choice(vec![
dynamic(re("[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}")),
dynamic(re("(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])")),
]))
// IPv6 patterns (simplified - covers most common cases)
.rule("ipv6", choice(vec![
// Full form: 8 groups of h16
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}")),
// With :: compression
dynamic(re("::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,6})?")),
dynamic(re("[0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,5})?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,4})?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,2}::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,3})?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,3}::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,2})?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,4}::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})?)?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,5}::[0-9a-fA-F]{1,4}?")),
dynamic(re("[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,6}::")),
]))
.build()
}
/// Parsed IP address
#[derive(Debug, Clone)]
pub enum IpAddress {
Ipv4(String),
Ipv6(String),
}
/// Parse an IP address (IPv4 or IPv6)
pub fn parse_ip(input: &str) -> Result<IpAddress, String> {
let input = input.trim();
// Try IPv4 first
let ipv4_grammar = build_ipv4_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&ipv4_grammar, input, &mut arena);
if parser.parse().is_ok() {
return Ok(IpAddress::Ipv4(input.to_string()));
}
// Try IPv6
let ipv6_grammar = build_ipv6_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&ipv6_grammar, input, &mut arena);
if parser.parse().is_ok() {
return Ok(IpAddress::Ipv6(input.to_string()));
}
Err(format!("Invalid IP address: {}", input))
}
/// Validate an IPv4 address octet
pub fn validate_ipv4_octet(octet: &str) -> bool {
octet.parse::<u8>().is_ok()
}
/// Parse and validate IPv4 address
pub fn parse_ipv4_components(input: &str) -> Option<[u8; 4]> {
let parts: Vec<&str> = input.split('.').collect();
if parts.len() != 4 {
return None;
}
let mut octets = [0u8; 4];
for (i, part) in parts.iter().enumerate() {
octets[i] = part.parse::<u8>().ok()?;
}
Some(octets)
}
fn main() {
println!("IP Address Parser Example");
println!("========================\n");
let addresses = [
// Valid IPv4
"0.0.0.0",
"192.168.1.1",
"255.255.255.255",
"10.0.0.1",
"127.0.0.1",
// Invalid IPv4
"255.255.255",
"256.1.1.1",
"1.2.3.4.5",
// Valid IPv6
"::1",
"::",
"2001:db8::1",
"fe80::1",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"2001:db8:85a3::8a2e:370:7334",
"::ffff:192.168.1.1",
// Invalid IPv6
"1:2",
"gggg::1",
];
println!("{:<40} | {:<10} | {}", "Address", "Type", "Components");
println!("{}", "-".repeat(70));
for addr in addresses {
match parse_ip(addr) {
Ok(IpAddress::Ipv4(ip)) => {
let components = parse_ipv4_components(&ip);
println!("{:<40} | {:<10} | {:?}", addr, "IPv4", components);
}
Ok(IpAddress::Ipv6(_ip)) => {
println!("{:<40} | {:<10} | {}", addr, "IPv6", "(hex groups)");
}
Err(e) => {
println!("{:<40} | {:<10} | {}", addr, "INVALID", e);
}
}
}
// Demonstrate IPv4 component parsing
println!("\nIPv4 Component Parsing:");
println!("-----------------------");
for ip in ["192.168.1.1", "10.0.0.1", "127.0.0.1"] {
if let Some(octets) = parse_ipv4_components(ip) {
println!("{} -> {:?}", ip, octets);
}
}
}
# frozen_string_literal: true
# This example is heavily inspired by citrus' ip.citrus. Have a look at both
# of these to get some choice!
# The grammars in this file conform to the ABNF given in Appendix A of RFC 3986
# Uniform Resource Identifier (URI): Generic Syntax.
#
# See http://tools.ietf.org/html/rfc3986#appendix-A for more information.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "pp"
require "parsanol/parslet"
module IPv4
include Parsanol::Parslet
# A host identified by an IPv4 literal address is represented in
# dotted-decimal notation (a sequence of four decimal numbers in the range 0
# to 255, separated by "."), as described in [RFC1123] by reference to
# [RFC0952]. Note that other forms of dotted notation may be interpreted on
# some platforms, as described in Section 7.4, but only the dotted-decimal
# form of four octets is allowed by this grammar.
rule(:ipv4) do
(dec_octet >> str(".") >> dec_octet >> str(".") >>
dec_octet >> str(".") >> dec_octet).as(:ipv4)
end
rule(:dec_octet) do
(str("25") >> match("[0-5]")) |
(str("2") >> match("[0-4]") >> digit) |
(str("1") >> digit >> digit) |
(match("[1-9]") >> digit) |
digit
end
rule(:digit) do
match("[0-9]")
end
end
# Must be used in concert with IPv4
module IPv6
include Parsanol::Parslet
rule(:colon) { str(":") }
rule(:dcolon) { colon >> colon }
# h16 :
def h16r(times)
(h16 >> colon).repeat(times, times)
end
# : h16
def h16l(times)
(colon >> h16).repeat(0, times)
end
# A 128-bit IPv6 address is divided into eight 16-bit pieces. Each piece is
# represented numerically in case-insensitive hexadecimal, using one to four
# hexadecimal digits (leading zeroes are permitted). The eight encoded
# pieces are given most-significant first, separated by colon characters.
# Optionally, the least-significant two pieces may instead be represented in
# IPv4 address textual format. A sequence of one or more consecutive
# zero-valued 16-bit pieces within the address may be elided, omitting all
# their digits and leaving exactly two consecutive colons in their place to
# mark the elision.
rule(:ipv6) do
(
((
h16r(6) |
(dcolon >> h16r(5)) |
(h16.maybe >> dcolon >> h16r(4)) |
((h16 >> h16l(1)).maybe >> dcolon >> h16r(3)) |
((h16 >> h16l(2)).maybe >> dcolon >> h16r(2)) |
((h16 >> h16l(3)).maybe >> dcolon >> h16r(1)) |
((h16 >> h16l(4)).maybe >> dcolon)
) >> ls32) |
((h16 >> h16l(5)).maybe >> dcolon >> h16) |
((h16 >> h16l(6)).maybe >> dcolon)
).as(:ipv6)
end
rule(:h16) do
hexdigit.repeat(1, 4)
end
rule(:ls32) do
(h16 >> colon >> h16) |
ipv4
end
rule(:hexdigit) do
digit | match("[a-fA-F]")
end
end
class Parser
include IPv4
include IPv6
def parse(str)
(ipv4 | ipv6).parse(str)
end
end
%w[
0.0.0.0
255.255.255.255
255.255.255
1:2:3:4:5:6:7:8
12AD:34FC:A453:1922::
12AD::34FC
12AD::
::
1:2
].each do |address|
parser = Parser.new
printf "%30s -> ", address
begin
result = parser.parse(address)
puts result.inspect
rescue Parsanol::ParseFailed => e
puts "Failed: #{e}"
end
end