Skip to content
← Back to Examples

IP Address Parser

RustRubyurls networkintermediate

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:hhhh

Examples

192.168.1.1VALID

Private IPv4 address

0.0.0.0VALID

Wildcard IPv4 address

255.255.255.255VALID

Broadcast IPv4 address

127.0.0.1VALID

Loopback IPv4 address

::1VALID

Loopback IPv6 address

2001:db8::1VALID

IPv6 with :: compression

fe80::1VALID

Link-local IPv6 address

256.1.1.1INVALID

Invalid: octet > 255

1.2.3INVALID

Invalid: incomplete IPv4

Output Format

The parser returns an IpAddress enum distinguishing between IPv4 and IPv6, with optional component extraction for IPv4 octets.

Structure

type

IpAddress enum with variants: Ipv4(String), Ipv6(String)

Ipv4

Can be decomposed into [u8; 4] octets

Ipv6

Can be decomposed into hex groups

Visual Overview

IP Address Parser diagram

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

Rust
//! 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);
        }
    }
}
Ruby
# 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