From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <dietmar@druiddev.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 0D706B9AC9
 for <pbs-devel@lists.proxmox.com>; Fri, 15 Mar 2024 12:28:06 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id E03221B800
 for <pbs-devel@lists.proxmox.com>; Fri, 15 Mar 2024 12:27:35 +0100 (CET)
Received: from druiddev.proxmox.com (unknown [94.136.29.99])
 by firstgate.proxmox.com (Proxmox) with ESMTP
 for <pbs-devel@lists.proxmox.com>; Fri, 15 Mar 2024 12:27:34 +0100 (CET)
Received: by druiddev.proxmox.com (Postfix, from userid 1000)
 id B51088BCF4; Fri, 15 Mar 2024 12:27:34 +0100 (CET)
From: Dietmar Maurer <dietmar@proxmox.com>
To: pbs-devel@lists.proxmox.com
Date: Fri, 15 Mar 2024 12:27:31 +0100
Message-Id: <20240315112732.368831-3-dietmar@proxmox.com>
X-Mailer: git-send-email 2.39.2
In-Reply-To: <20240315112732.368831-1-dietmar@proxmox.com>
References: <20240315112732.368831-1-dietmar@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL -0.547 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery
 methods
 RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_NONE                0.001 SPF: sender does not publish an SPF Record
 T_SCC_BODY_TEXT_LINE    -0.01 -
Subject: [pbs-devel] [PATCH proxmox 3/4] proxmox-schema: add IP address
 regex/api-types
X-BeenThere: pbs-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Backup Server development discussion
 <pbs-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/>
List-Post: <mailto:pbs-devel@lists.proxmox.com>
List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Fri, 15 Mar 2024 11:28:06 -0000

Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
---
 proxmox-schema/src/api_types.rs | 84 +++++++++++++++++++++++++++++++++
 1 file changed, 84 insertions(+)

diff --git a/proxmox-schema/src/api_types.rs b/proxmox-schema/src/api_types.rs
index 4e10ebb..381d4cb 100644
--- a/proxmox-schema/src/api_types.rs
+++ b/proxmox-schema/src/api_types.rs
@@ -3,10 +3,68 @@ use const_format::concatcp;
 
 use crate::{ApiStringFormat, Schema, StringSchema};
 
+#[rustfmt::skip]
+const IPV4OCTET: &str = r"(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9])?[0-9])";
+
+#[rustfmt::skip]
+const IPV6H16: &str = r"(?:[0-9a-fA-F]{1,4})";
+
+/// Returns the regular expression string to match IPv4 addresses
+#[rustfmt::skip]
+pub const IPV4RE_STR: &str = concatcp!(r"(?:(?:", IPV4OCTET, r"\.){3}", IPV4OCTET, ")");
+
+#[rustfmt::skip]
+const IPV6LS32: &str = concatcp!(r"(?:(?:", IPV4RE_STR, "|", IPV6H16, ":", IPV6H16, "))" );
+
+/// Returns the regular expression string to match IPv6 addresses
+#[rustfmt::skip]
+pub const IPV6RE_STR: &str = concatcp!(r"(?:",
+    r"(?:(?:",                                         r"(?:", IPV6H16, r":){6})", IPV6LS32, r")|",
+    r"(?:(?:",                                       r"::(?:", IPV6H16, r":){5})", IPV6LS32, r")|",
+    r"(?:(?:(?:",                         IPV6H16, r")?::(?:", IPV6H16, r":){4})", IPV6LS32, r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,1}", IPV6H16, r")?::(?:", IPV6H16, r":){3})", IPV6LS32, r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,2}", IPV6H16, r")?::(?:", IPV6H16, r":){2})", IPV6LS32, r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,3}", IPV6H16, r")?::(?:", IPV6H16, r":){1})", IPV6LS32, r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,4}", IPV6H16, r")?::",                   ")", IPV6LS32, r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,5}", IPV6H16, r")?::",                   ")", IPV6H16,  r")|",
+    r"(?:(?:(?:(?:", IPV6H16, r":){0,6}", IPV6H16, r")?::",                                  ")))");
+
+/// Returns the regular expression string to match IP addresses (v4 or v6)
+#[rustfmt::skip]
+pub const IPRE_STR: &str = concatcp!(r"(?:", IPV4RE_STR, "|", IPV6RE_STR, ")");
+
+/// Regular expression string to match IP addresses where IPv6 addresses require brackets around
+/// them, while for IPv4 they are forbidden.
+#[rustfmt::skip]
+pub const IPRE_BRACKET_STR: &str = concatcp!(r"(?:", IPV4RE_STR, r"|\[(?:", IPV6RE_STR, r")\]", r")");
+
+#[rustfmt::skip]
+pub const CIDR_V4_REGEX_STR: &str = concatcp!(r"(?:", IPV4RE_STR, r"/\d{1,2})$");
+
+#[rustfmt::skip]
+pub const CIDR_V6_REGEX_STR: &str = concatcp!(r"(?:", IPV6RE_STR, r"/\d{1,3})$");
+
 #[rustfmt::skip]
 const SAFE_ID_REGEX_STR: &str = r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)";
 
 const_regex! {
+    /// IPv4 regular expression.
+    pub IP_V4_REGEX = concatcp!(r"^", IPV4RE_STR, r"$");
+
+    /// IPv6 regular expression.
+    pub IP_V6_REGEX = concatcp!(r"^", IPV6RE_STR, r"$");
+
+    /// Regex to match IP addresses (V4 or V6)
+    pub IP_REGEX = concatcp!(r"^", IPRE_STR, r"$");
+
+    /// Regex to match IP addresses where IPv6 addresses require brackets around
+    /// them, while for IPv4 they are forbidden.
+    pub IP_BRACKET_REGEX = concatcp!(r"^", IPRE_BRACKET_STR, r"$");
+
+    pub CIDR_V4_REGEX = concatcp!(r"^", CIDR_V4_REGEX_STR, r"$");
+    pub CIDR_V6_REGEX = concatcp!(r"^", CIDR_V6_REGEX_STR, r"$");
+    pub CIDR_REGEX = concatcp!(r"^(?:", CIDR_V4_REGEX_STR, "|",  CIDR_V6_REGEX_STR, r")$");
+
     /// Regex for safe identifiers.
     ///
     /// This
@@ -36,3 +94,29 @@ pub const COMMENT_SCHEMA: Schema = StringSchema::new("Comment.")
     .format(&SINGLE_LINE_COMMENT_FORMAT)
     .max_length(128)
     .schema();
+
+#[test]
+fn test_regexes() {
+    assert!(IP_REGEX.is_match("127.0.0.1"));
+    assert!(IP_V4_REGEX.is_match("127.0.0.1"));
+    assert!(!IP_V6_REGEX.is_match("127.0.0.1"));
+
+    assert!(CIDR_V4_REGEX.is_match("127.0.0.1/24"));
+    assert!(CIDR_REGEX.is_match("127.0.0.1/24"));
+
+    assert!(IP_REGEX.is_match("::1"));
+    assert!(IP_REGEX.is_match("2014:b3a::27"));
+    assert!(IP_REGEX.is_match("2014:b3a::192.168.0.1"));
+    assert!(IP_REGEX.is_match("2014:b3a:0102:adf1:1234:4321:4afA:BCDF"));
+    assert!(!IP_V4_REGEX.is_match("2014:b3a:0102:adf1:1234:4321:4afA:BCDF"));
+    assert!(IP_V6_REGEX.is_match("2014:b3a:0102:adf1:1234:4321:4afA:BCDF"));
+
+    assert!(CIDR_V6_REGEX.is_match("2014:b3a:0102:adf1:1234:4321:4afA:BCDF/60"));
+    assert!(CIDR_REGEX.is_match("2014:b3a:0102:adf1:1234:4321:4afA:BCDF/60"));
+
+    assert!(IP_BRACKET_REGEX.is_match("127.0.0.1"));
+    assert!(IP_BRACKET_REGEX.is_match("[::1]"));
+    assert!(IP_BRACKET_REGEX.is_match("[2014:b3a::27]"));
+    assert!(IP_BRACKET_REGEX.is_match("[2014:b3a::192.168.0.1]"));
+    assert!(IP_BRACKET_REGEX.is_match("[2014:b3a:0102:adf1:1234:4321:4afA:BCDF]"));
+}
-- 
2.39.2