Repository: kcl-lang/kcl
Pull request #1908: fix: fix `net.is_IP_in_CIDR` with IPv6
Author: login: He1pa
Created: 2025-04-01T12:36:23Z | Merged: 2025-04-02T02:19:42Z

<!-- Thank you for contributing to KCL!

Note: 

1. With pull requests:

    - Open your pull request against "main"
    - Your pull request should have no more than two commits, if not you should squash them.
    - It should pass all tests in the available continuous integration systems such as GitHub Actions.
    - You should add/modify tests to cover your proposed code changes.
    - If your pull request contains a new feature, please document it on the README.

2. Please create an issue first to describe the problem.

    We recommend that link the issue with the PR in the following question.
    For more info, check https://kcl-lang.io/docs/community/contribute/
-->

#### 1. Does this PR affect any open issues?(Y/N) and add issue references (e.g. "fix #123", "re #123".):

- [ ] N
- [x] Y fix #1905 

<!-- You can add issue references here. 
    e.g. 
    fix #123, re #123, 
    fix https://github.com/XXX/issues/44
-->

#### 2. What is the scope of this PR (e.g. component or file name):

<!-- You can add the scope of this change here. 
    e.g. 
    /src/server/core.rs,
    kcl-lang/kcl/src/parser 
-->

#### 3. Provide a description of the PR(e.g. more details, effects, motivations or doc link):

<!-- You can choose a brief description here -->
- [ ] Affects user behaviors
- [ ] Contains syntax changes
- [ ] Contains variable changes
- [ ] Contains experimental features
- [ ] Performance regression: Consumes more CPU
- [ ] Performance regression: Consumes more Memory
- [x] Other

<!-- You can add more details here.
    e.g. 
    Call method "XXXX" to ..... in order to ....,
    More details: https://XXXX.com/doc......
-->

#### 4. Are there any breaking changes?(Y/N) and describe the breaking changes(e.g. more details, motivations or doc link):

- [x] N
- [ ] Y 

<!-- You can add more details here.
    e.g. 
    Calling method "XXXX" will cause the "XXXX", "XXXX" modules to be affected.
    More details: https://XXXX.com/doc......
-->

#### 5. Are there test cases for these changes?(Y/N) select and add more details, references or doc links:

<!-- You can choose a brief description here -->
- [x] Unit test
- [ ] Integration test
- [ ] Benchmark (add benchmark stats below)
- [ ] Manual test (add detailed scripts or steps below)
- [ ] Other

<!-- You can add more details here.
e.g. 
The test case in XXXX is used to .....
test cases in /src/tests/XXXXX
test cases https://github.com/XXX/pull/44
benchmark stats: time XXX ms
-->



Diff:
diff --git a/kclvm/runtime/src/net/mod.rs b/kclvm/runtime/src/net/mod.rs
index e11b4cea1..811dc99d2 100644
--- a/kclvm/runtime/src/net/mod.rs
+++ b/kclvm/runtime/src/net/mod.rs
@@ -1,5 +1,6 @@
 //! Copyright The KCL Authors. All rights reserved.
 
+use std::net::IpAddr;
 use std::net::Ipv4Addr;
 use std::net::Ipv6Addr;
 use std::str::FromStr;
@@ -509,35 +510,72 @@ pub extern "C-unwind" fn kclvm_net_is_IP_in_CIDR(
     let args = ptr_as_ref(args);
     let kwargs = ptr_as_ref(kwargs);
 
-    if let Some(ip) = get_call_arg_str(args, kwargs, 0, Some("ip")) {
-        if let Some(cidr) = get_call_arg_str(args, kwargs, 1, Some("cidr")) {
-            let parts: Vec<&str> = cidr.split('/').collect();
-            if parts.len() == 2 {
-                let cidr_ip = parts[0];
-                let mask_bits = parts[1];
-                let ip_addr = match Ipv4Addr::from_str(&ip) {
-                    Ok(ip_addr) => ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let cidr_ip_addr = match Ipv4Addr::from_str(cidr_ip) {
-                    Ok(cidr_ip_addr) => cidr_ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let mask_bits = match mask_bits.parse::<u8>() {
-                    Ok(mask_bits) if mask_bits <= 32 => mask_bits,
-                    _ => return kclvm_value_False(ctx),
-                };
-                let mask = !((1 << (32 - mask_bits)) - 1);
-                let ip_u32 = u32::from_be_bytes(ip_addr.octets());
-                let cidr_ip_u32 = u32::from_be_bytes(cidr_ip_addr.octets());
-                let is_in_cidr = (ip_u32 & mask) == (cidr_ip_u32 & mask);
-                return kclvm_value_Bool(ctx, is_in_cidr as i8);
-            }
+    let ip = match get_call_arg_str(args, kwargs, 0, Some("ip")) {
+        Some(ip) => ip,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
         }
-        return kclvm_value_False(ctx);
+    };
+    let cidr = match get_call_arg_str(args, kwargs, 1, Some("cidr")) {
+        Some(cidr) => cidr,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+        }
+    };
+
+    let (cidr_ip, mask_bits) = match _parse_cidr(&cidr) {
+        Some((ip, bits)) => (ip, bits),
+        None => return kclvm_value_False(ctx),
+    };
+
+    match (_parse_ip(&ip), cidr_ip) {
+        (Ok(IpAddr::V4(ip)), IpAddr::V4(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 32 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv4_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        (Ok(IpAddr::V6(ip)), IpAddr::V6(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 128 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv6_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        _ => kclvm_value_False(ctx),
     }
+}
+
+fn _parse_cidr(cidr: &str) -> Option<(IpAddr, Option<u32>)> {
+    let parts: Vec<&str> = cidr.split('/').collect();
+    if parts.len() != 2 {
+        return None;
+    }
+    let ip = IpAddr::from_str(parts[0]).ok()?;
+    let mask_bits = parts[1].parse::<u32>().ok();
+    Some((ip, mask_bits))
+}
 
-    panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+fn _parse_ip(ip: &str) -> Result<IpAddr, ()> {
+    IpAddr::from_str(ip).map_err(|_| ())
+}
+
+fn _check_ipv4_cidr(ip: Ipv4Addr, cidr_ip: Ipv4Addr, mask_bits: u32) -> bool {
+    let mask = !((1u32 << (32 - mask_bits)) - 1);
+    let ip_u32 = u32::from(ip);
+    let cidr_u32 = u32::from(cidr_ip);
+    (ip_u32 & mask) == (cidr_u32 & mask)
+}
+
+fn _check_ipv6_cidr(ip: Ipv6Addr, cidr_ip: Ipv6Addr, mask_bits: u32) -> bool {
+    let mask = match 128 - mask_bits {
+        shift @ 0..=128 => !((1u128 << shift) - 1),
+        _ => return false,
+    };
+    let ip_u128 = u128::from(ip);
+    let cidr_u128 = u128::from(cidr_ip);
+    (ip_u128 & mask) == (cidr_u128 & mask)
 }
 
 #[allow(non_camel_case_types, non_snake_case)]
diff --git a/test/grammar/builtins/net/cidr/main.k b/test/grammar/builtins/net/cidr/main.k
index a565992b0..20aee2280 100644
--- a/test/grammar/builtins/net/cidr/main.k
+++ b/test/grammar/builtins/net/cidr/main.k
@@ -5,3 +5,4 @@ cidr1 = net.hosts_in_CIDR("192.168.1.0/24")
 cidr2 = net.subnets_from_CIDR("192.168.1.0/24")
 cidr3 = net.is_IP_in_CIDR("192.168.1.1", "192.168.1.0/24")
 cidr4 = net.is_IP_in_CIDR("192.168.0.1", "192.168.1.0/24")
+cidr5 = net.is_IP_in_CIDR('2001:db8::1', "2001:db8::/64")
diff --git a/test/grammar/builtins/net/cidr/stdout.golden b/test/grammar/builtins/net/cidr/stdout.golden
index eec869f8a..4d0dba7b3 100644
--- a/test/grammar/builtins/net/cidr/stdout.golden
+++ b/test/grammar/builtins/net/cidr/stdout.golden
@@ -5,3 +5,4 @@ cidr1: []
 cidr2: []
 cidr3: true
 cidr4: false
+cidr5: true


Reviews:
user: login: zong-zhe
body: LGTM 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T13:09:22Z
user: login: johngmyers
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-02T04:31:56Z

Review comments:
diff_hunk: @@ -509,35 +510,72 @@ pub extern "C-unwind" fn kclvm_net_is_IP_in_CIDR(
     let args = ptr_as_ref(args);
     let kwargs = ptr_as_ref(kwargs);
 
-    if let Some(ip) = get_call_arg_str(args, kwargs, 0, Some("ip")) {
-        if let Some(cidr) = get_call_arg_str(args, kwargs, 1, Some("cidr")) {
-            let parts: Vec<&str> = cidr.split('/').collect();
-            if parts.len() == 2 {
-                let cidr_ip = parts[0];
-                let mask_bits = parts[1];
-                let ip_addr = match Ipv4Addr::from_str(&ip) {
-                    Ok(ip_addr) => ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let cidr_ip_addr = match Ipv4Addr::from_str(cidr_ip) {
-                    Ok(cidr_ip_addr) => cidr_ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let mask_bits = match mask_bits.parse::<u8>() {
-                    Ok(mask_bits) if mask_bits <= 32 => mask_bits,
-                    _ => return kclvm_value_False(ctx),
-                };
-                let mask = !((1 << (32 - mask_bits)) - 1);
-                let ip_u32 = u32::from_be_bytes(ip_addr.octets());
-                let cidr_ip_u32 = u32::from_be_bytes(cidr_ip_addr.octets());
-                let is_in_cidr = (ip_u32 & mask) == (cidr_ip_u32 & mask);
-                return kclvm_value_Bool(ctx, is_in_cidr as i8);
-            }
+    let ip = match get_call_arg_str(args, kwargs, 0, Some("ip")) {
+        Some(ip) => ip,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
         }
-        return kclvm_value_False(ctx);
+    };
+    let cidr = match get_call_arg_str(args, kwargs, 1, Some("cidr")) {
+        Some(cidr) => cidr,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+        }
+    };
+
+    let (cidr_ip, mask_bits) = match _parse_cidr(&cidr) {
+        Some((ip, bits)) => (ip, bits),
+        None => return kclvm_value_False(ctx),
+    };
+
+    match (_parse_ip(&ip), cidr_ip) {
+        (Ok(IpAddr::V4(ip)), IpAddr::V4(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 32 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv4_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        (Ok(IpAddr::V6(ip)), IpAddr::V6(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 128 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv6_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        _ => kclvm_value_False(ctx),
     }
+}
+
+fn _parse_cidr(cidr: &str) -> Option<(IpAddr, Option<u32>)> {
+    let parts: Vec<&str> = cidr.split('/').collect();
+    if parts.len() != 2 {
+        return None;
+    }
+    let ip = IpAddr::from_str(parts[0]).ok()?;
+    let mask_bits = parts[1].parse::<u32>().ok();
path: kclvm/runtime/src/net/mod.rs
user: login: johngmyers
body: This incorrectly succeeds if `ip` has bits set beyond `mask_bits`. For example, `10.11.0.0/8`.
created_at: 2025-04-02T04:23:38Z
author_association: NONE
diff_hunk: @@ -509,35 +510,72 @@ pub extern "C-unwind" fn kclvm_net_is_IP_in_CIDR(
     let args = ptr_as_ref(args);
     let kwargs = ptr_as_ref(kwargs);
 
-    if let Some(ip) = get_call_arg_str(args, kwargs, 0, Some("ip")) {
-        if let Some(cidr) = get_call_arg_str(args, kwargs, 1, Some("cidr")) {
-            let parts: Vec<&str> = cidr.split('/').collect();
-            if parts.len() == 2 {
-                let cidr_ip = parts[0];
-                let mask_bits = parts[1];
-                let ip_addr = match Ipv4Addr::from_str(&ip) {
-                    Ok(ip_addr) => ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let cidr_ip_addr = match Ipv4Addr::from_str(cidr_ip) {
-                    Ok(cidr_ip_addr) => cidr_ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let mask_bits = match mask_bits.parse::<u8>() {
-                    Ok(mask_bits) if mask_bits <= 32 => mask_bits,
-                    _ => return kclvm_value_False(ctx),
-                };
-                let mask = !((1 << (32 - mask_bits)) - 1);
-                let ip_u32 = u32::from_be_bytes(ip_addr.octets());
-                let cidr_ip_u32 = u32::from_be_bytes(cidr_ip_addr.octets());
-                let is_in_cidr = (ip_u32 & mask) == (cidr_ip_u32 & mask);
-                return kclvm_value_Bool(ctx, is_in_cidr as i8);
-            }
+    let ip = match get_call_arg_str(args, kwargs, 0, Some("ip")) {
+        Some(ip) => ip,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
         }
-        return kclvm_value_False(ctx);
+    };
+    let cidr = match get_call_arg_str(args, kwargs, 1, Some("cidr")) {
+        Some(cidr) => cidr,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+        }
+    };
+
+    let (cidr_ip, mask_bits) = match _parse_cidr(&cidr) {
+        Some((ip, bits)) => (ip, bits),
+        None => return kclvm_value_False(ctx),
+    };
+
+    match (_parse_ip(&ip), cidr_ip) {
+        (Ok(IpAddr::V4(ip)), IpAddr::V4(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 32 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv4_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        (Ok(IpAddr::V6(ip)), IpAddr::V6(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 128 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv6_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        _ => kclvm_value_False(ctx),
     }
+}
+
+fn _parse_cidr(cidr: &str) -> Option<(IpAddr, Option<u32>)> {
+    let parts: Vec<&str> = cidr.split('/').collect();
+    if parts.len() != 2 {
+        return None;
+    }
+    let ip = IpAddr::from_str(parts[0]).ok()?;
+    let mask_bits = parts[1].parse::<u32>().ok();
+    Some((ip, mask_bits))
+}
 
-    panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+fn _parse_ip(ip: &str) -> Result<IpAddr, ()> {
+    IpAddr::from_str(ip).map_err(|_| ())
+}
+
+fn _check_ipv4_cidr(ip: Ipv4Addr, cidr_ip: Ipv4Addr, mask_bits: u32) -> bool {
path: kclvm/runtime/src/net/mod.rs
user: login: johngmyers
body: Why not use the `cidr` crate? I was just working on reimplementing `net.parse_cidr` using it.
created_at: 2025-04-02T04:24:49Z
author_association: NONE
diff_hunk: @@ -509,35 +510,72 @@ pub extern "C-unwind" fn kclvm_net_is_IP_in_CIDR(
     let args = ptr_as_ref(args);
     let kwargs = ptr_as_ref(kwargs);
 
-    if let Some(ip) = get_call_arg_str(args, kwargs, 0, Some("ip")) {
-        if let Some(cidr) = get_call_arg_str(args, kwargs, 1, Some("cidr")) {
-            let parts: Vec<&str> = cidr.split('/').collect();
-            if parts.len() == 2 {
-                let cidr_ip = parts[0];
-                let mask_bits = parts[1];
-                let ip_addr = match Ipv4Addr::from_str(&ip) {
-                    Ok(ip_addr) => ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let cidr_ip_addr = match Ipv4Addr::from_str(cidr_ip) {
-                    Ok(cidr_ip_addr) => cidr_ip_addr,
-                    Err(_) => return kclvm_value_False(ctx),
-                };
-                let mask_bits = match mask_bits.parse::<u8>() {
-                    Ok(mask_bits) if mask_bits <= 32 => mask_bits,
-                    _ => return kclvm_value_False(ctx),
-                };
-                let mask = !((1 << (32 - mask_bits)) - 1);
-                let ip_u32 = u32::from_be_bytes(ip_addr.octets());
-                let cidr_ip_u32 = u32::from_be_bytes(cidr_ip_addr.octets());
-                let is_in_cidr = (ip_u32 & mask) == (cidr_ip_u32 & mask);
-                return kclvm_value_Bool(ctx, is_in_cidr as i8);
-            }
+    let ip = match get_call_arg_str(args, kwargs, 0, Some("ip")) {
+        Some(ip) => ip,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
         }
-        return kclvm_value_False(ctx);
+    };
+    let cidr = match get_call_arg_str(args, kwargs, 1, Some("cidr")) {
+        Some(cidr) => cidr,
+        None => {
+            panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+        }
+    };
+
+    let (cidr_ip, mask_bits) = match _parse_cidr(&cidr) {
+        Some((ip, bits)) => (ip, bits),
+        None => return kclvm_value_False(ctx),
+    };
+
+    match (_parse_ip(&ip), cidr_ip) {
+        (Ok(IpAddr::V4(ip)), IpAddr::V4(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 32 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv4_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        (Ok(IpAddr::V6(ip)), IpAddr::V6(cidr_ip)) => {
+            let mask_bits = match mask_bits {
+                Some(bits) if bits <= 128 => bits,
+                _ => return kclvm_value_False(ctx),
+            };
+            kclvm_value_Bool(ctx, _check_ipv6_cidr(ip, cidr_ip, mask_bits) as i8)
+        }
+        _ => kclvm_value_False(ctx),
     }
+}
+
+fn _parse_cidr(cidr: &str) -> Option<(IpAddr, Option<u32>)> {
+    let parts: Vec<&str> = cidr.split('/').collect();
+    if parts.len() != 2 {
+        return None;
+    }
+    let ip = IpAddr::from_str(parts[0]).ok()?;
+    let mask_bits = parts[1].parse::<u32>().ok();
+    Some((ip, mask_bits))
+}
 
-    panic!("is_IP_in_CIDR() missing 2 required positional arguments: 'ip' and 'cidr'");
+fn _parse_ip(ip: &str) -> Result<IpAddr, ()> {
+    IpAddr::from_str(ip).map_err(|_| ())
+}
+
+fn _check_ipv4_cidr(ip: Ipv4Addr, cidr_ip: Ipv4Addr, mask_bits: u32) -> bool {
+    let mask = !((1u32 << (32 - mask_bits)) - 1);
path: kclvm/runtime/src/net/mod.rs
user: login: johngmyers
body: Does this not overflow for `0.0.0.0/0`?
created_at: 2025-04-02T04:27:56Z
author_association: NONE
diff_hunk: @@ -5,3 +5,4 @@ cidr1 = net.hosts_in_CIDR("192.168.1.0/24")
 cidr2 = net.subnets_from_CIDR("192.168.1.0/24")
 cidr3 = net.is_IP_in_CIDR("192.168.1.1", "192.168.1.0/24")
 cidr4 = net.is_IP_in_CIDR("192.168.0.1", "192.168.1.0/24")
+cidr5 = net.is_IP_in_CIDR('2001:db8::1', "2001:db8::/64")
path: test/grammar/builtins/net/cidr/main.k
user: login: johngmyers
body: There should be a LOT more tests:
* First in CIDR
* Last in CIDR
* One before first in CIDR
* One after last in CIDR
* `0.0.0.0/0` and `::/0`
* IPv4 in `::/0`
* IPv4 in IPv6 CIDR and vice versa
* Invalid CIDRs
* Invalid IPs
* IPv4 /32 and IPv6 /128
created_at: 2025-04-02T04:31:40Z
author_association: NONE

PR comments:
user: login: zong-zhe
created_at: 2025-04-01T13:10:50Z
body: cc @johngmyers 😄 
author_association: CONTRIBUTOR
user: login: coveralls
created_at: 2025-04-01T13:19:34Z
body: ## Pull Request Test Coverage Report for [Build 14196324909](https://coveralls.io/builds/73021290)


### Details

* **0** of **56**   **(0.0%)**  changed or added relevant lines in **1** file are covered.
* **5** unchanged lines in **1** file lost coverage.
* Overall coverage decreased (**-0.01%**) to **70.353%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [kclvm/runtime/src/net/mod.rs](https://coveralls.io/builds/73021290/source?filename=kclvm%2Fruntime%2Fsrc%2Fnet%2Fmod.rs#L513) | 0 | 56 | 0.0%
<!-- | **Total:** | **0** | **56** | **0.0%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [kclvm/runtime/src/net/mod.rs](https://coveralls.io/builds/73021290/source?filename=kclvm%2Fruntime%2Fsrc%2Fnet%2Fmod.rs#L580) | 5 | 27.82% |
<!-- | **Total:** | **5** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/73021290/badge)](https://coveralls.io/builds/73021290) |
| :-- | --: |
| Change from base [Build 14185628298](https://coveralls.io/builds/73010697): |  -0.01% |
| Covered Lines: | 52870 |
| Relevant Lines: | 75150 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: COLLABORATOR