inet 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd1407cb6b63aedfbf3e934ab952f7a89e74b19660aabb97d13eaa2ca63365d9
4
- data.tar.gz: 8a47da2e0262b952cc8b085b1551c057585a1ff8f2e48039dd16e71fccc038b4
3
+ metadata.gz: 441ae2bd4745812fbc1181d5a430b3eb065225670418c41f252e25907efe4117
4
+ data.tar.gz: 1831047c063ad8e090b1a184b4bd8dd0e530e1302f711b0ba5abf26627bdf6ee
5
5
  SHA512:
6
- metadata.gz: 22881dab447cf39c8468b8ef3053fb5276b373f35034c0a95dcb115c72f47d777448793e55007f9bc572979196e1a7aa1531e784c969aa8fb401a87dd766df79
7
- data.tar.gz: 1de282eccadcf51c4ebd15929f3eee88e90f0aec16617df644e900894461eb0157ce4de807b4de0a87251cbb6720c8699d36de91bab9f4a8bf7fee21650480de
6
+ metadata.gz: 2e2f7f9e1f5904cfa0db29842c513e3b7b01a1a987b672b19cea5e91bb6d47e3e15a344c93cde983bc8da2bd447938e0cc674b1823dea27d2cb391ddd734d6e4
7
+ data.tar.gz: c0d4867be92ee239c51666d27f442b56ca6071469cb36d1810f525acc28f317817ba29edb42dda798ad2881684c70eaaa22d0ce9a16a07f86473887412ee26a7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.2.0
4
+
5
+ 1. **地址与服务运算**:从 algosec-rb 迁入 IPv4、IPv6 地址规范化、范围解析、CIDR 拆分、端口及协议服务编码,公共入口为 `Inet::IPAddr`、`Inet::IPv6`。
6
+ 2. **区间集合**:提供不可变 `Inet::Set`,支持合并、交集、差集、包含、重叠、关系判断和 Marshal 持久化;无需 ActiveSupport 或其他运行时依赖。
7
+ 3. **验证**:补充边界、非法输入、集合运算对照测试和真实 gem 隔离安装验证。
8
+
9
+ ## 0.1.1
10
+
11
+ 1. **自动发布**:接通 RubyGems Trusted Publishing,由 GitHub Actions 发布通过全部检查的同一份 gem,并提供对应的 GitHub Release 和 SHA256 校验文件。
12
+
3
13
  ## 0.1.0
4
14
 
5
15
  1. **基础骨架**:提供可安装的 `inet` gem、`Inet` 命名空间和版本信息,要求 Ruby 3.4 及以上版本。
data/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 inet contributors
4
+ Copyright (c) 2026 Gatework contributors
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Release](https://github.com/gatework/inet/actions/workflows/release.yml/badge.svg)](https://github.com/gatework/inet/actions/workflows/release.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/inet.svg)](https://rubygems.org/gems/inet)
5
5
 
6
- `inet` 的 Ruby gem 基础骨架。当前仅提供 `Inet` 命名空间和版本信息,尚未实现网络功能。
6
+ `inet` 提供 IPv4、IPv6 地址与服务范围运算,以及不可变整数区间集合。加载时不发起网络请求或启动后台任务。
7
7
 
8
8
  要求 Ruby 3.4 或更高版本,无运行时依赖,采用 MIT 许可证。
9
9
 
@@ -16,15 +16,38 @@ gem install inet
16
16
  也可以在 Gemfile 中添加:
17
17
 
18
18
  ```ruby
19
- gem "inet", "~> 0.1"
19
+ gem "inet", "~> 0.2"
20
20
  ```
21
21
 
22
22
  ```ruby
23
23
  require "inet"
24
24
 
25
- puts Inet::VERSION # => "0.1.0"
25
+ Inet::IPAddr.host_cidr("192.0.2.9/24") # => "192.0.2.9/24"
26
+ Inet::IPAddr.network_cidr("192.0.2.9/24") # => "192.0.2.0/24"
27
+ Inet::IPAddr.cidrs(Inet::IPAddr.parse("192.0.2.0/24")) # => [["192.0.2.0", 24]]
28
+ Inet::IPv6.canonical("2001:0db8::9/64") # => "2001:db8::/64"
29
+ Inet::IPv6.canonical("2001:db8::9/64", host: true) # => "2001:db8::9/64"
30
+
31
+ allowed = Inet::Set.new(1..10) - Inet::Set.new(3..7)
32
+ allowed.ranges # => [1..2, 8..10]
33
+ allowed.cardinality # => 5
26
34
  ```
27
35
 
36
+ ## 接口约定
37
+
38
+ - `Inet::IPAddr` 继承标准库 `IPAddr`,只接受 IPv4 字符串或整数;设备格式中的前导零按十进制处理,`any` 表示 `0.0.0.0`,全部 IPv4 地址使用 `any/0`。
39
+ - `host_cidr` 保留主机位,`network_cidr` 归一化网络地址;可传第二个参数指定前缀长度或连续网络掩码。`host` 返回主机文本,`to_i` / `from_i` 转换整数,`prefix_length` / `netmask` / `wildcard_mask` 转换掩码。
40
+ - `parse` 接受主机、CIDR、点分掩码或 `起点-终点`,返回 `Inet::Set`。IPv4 的范围端点自动排序;`between` 接受两个端点,`subnet` / `network` 接受地址及掩码。`cidrs` 接受合法 IPv4 整数集合并返回 `[地址, 前缀]` 数组;`format_range` 接受合法闭区间并返回单个 CIDR 或端点表达式。
41
+ - `ports` 接受端口整数、区间、单个端口文本或以连字符/空格分隔的范围,限制为 `0..65535`。`protocol_number` 接受名称、符号或 `0..255` 的编号;`icmp?` 判断 ICMP/ICMPv6。`service("tcp/80-90")` 返回服务整数集合,高八位为协议号,低十六位为端口;省略端口使用零,`any` 或协议 `0` 表示整个 24 位服务空间。
42
+ - `Inet::IPv6.address` 返回标准库 `IPAddr`;`canonical` 规范化主机或 CIDR,`parse` 返回整数集合,`normalize` 输出规范文本。范围必须按从小到大排列且不带前缀;拒绝带区域标识 `%` 的地址。`from_i` 转换 128 位非负整数,`ranges` 把合法 IPv6 整数集合转换为地址文本端点对。
43
+ - `Inet::Set.new(*ranges)` 接受非负整数区间,自动合并重叠及相邻区间;半开区间转换为闭区间,拒绝空或逆序区间。`each` 遍历区间,`size` 是区间段数,`cardinality` 是整数元素总数。空集合的 `min` / `max` 返回 `nil`。
44
+ - 集合支持 `|`、`&`、`-`、`include?(整数)`、`cover?(整数或集合)`、`subset?`、`overlap?`、`relation`。关系返回 `:equal`、`:contains`、`:within` 或 `:other`。集合和值数组冻结,`to_a` 返回数组副本;相等性、哈希和 Marshal 恢复保持值语义。
45
+
46
+ 非法地址、超范围值和不支持的类型抛出 `ArgumentError`(包括标准库 `IPAddr` 的对应子类)。集合自身不区分 IPv4、IPv6、端口或服务,调用方须保持同一数值空间,避免跨类型混算。
47
+ 可分别 `require "inet/ipaddr"`、`require "inet/ipv6"` 或 `require "inet/set"`。
48
+
49
+ 这三个模块迁自 Gatework 的 algosec-rb,保留其公共方法与运算行为;最低 Ruby 版本自带的 `Range#overlap?` 替代原 ActiveSupport 加载。源代码采用 MIT 许可证,版权声明见 [LICENSE](LICENSE)。
50
+
28
51
  ## 开发
29
52
 
30
53
  ```sh
data/docs/releasing.md CHANGED
@@ -36,7 +36,8 @@
36
36
  4. 上传该 gem,回读 RubyGems 版本信息和下载内容,确认 SHA256 一致。
37
37
  5. 创建对应标签和 GitHub Release,上传同一份 gem 与 `SHA256SUMS`,下载比对后公开发布。
38
38
 
39
- 首次推送 `main` 会发布 `0.1.0`。同版本的后续提交会跳过发布;后续更改需要提升版本号。
39
+ `0.1.0` 的首次上传使用了本地构建包;从 `0.1.1` 开始使用上述自动发布流程。
40
+ 同版本的后续提交会跳过发布;后续更改需要提升版本号。
40
41
  发布只在 `main` 上运行,标签由工作流自动创建。
41
42
 
42
43
  ## 失败恢复
@@ -49,4 +50,4 @@ RubyGems 上传结果不明时,先查看版本页面和 SHA256,等待索引
49
50
  同版本存在不同 SHA256 时停止发布,需要检查冲突并使用新版本。
50
51
 
51
52
  `script/release.rb` 是 Actions 内部入口,要求 `gatework/inet` 的 `main`、干净工作区和对应的触发提交。
52
- 本地可以运行 `bundle exec ruby script/verify_package.rb pkg/inet-0.1.0.gem` 验证已有构件。
53
+ 本地可以运行 `bundle exec ruby script/verify_package.rb pkg/inet-0.1.1.gem` 验证已有构件。
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "set"
5
+
6
+ module Inet
7
+ # 在标准 IPAddr 能力上补充防火墙所需的 IPv4 和服务范围运算。
8
+ class IPAddr < ::IPAddr
9
+ IPV4_MAX = ::IPAddr::IN4MASK
10
+ PORT_MAX = (1 << 16) - 1
11
+ SERVICE_MAX = (1 << 24) - 1
12
+ # 服务编码中反复使用的协议号集中命名,避免上层散落裸数字。
13
+ ANY_PROTOCOL = 0
14
+ ICMP = 1
15
+ ICMPV6 = 58
16
+ TCP = 6
17
+ UDP = 17
18
+ PROTOCOL_NUMBERS = { "tcp" => TCP, "udp" => UDP, "icmp" => ICMP, "icmpv6" => ICMPV6,
19
+ "ms-rpc-tcp" => TCP, "ms-rpc-udp" => UDP }.freeze
20
+
21
+ # 规范化 IPv4 输入,并拒绝 IPv6、非法前缀及非十进制设备写法。
22
+ def initialize(address = "0.0.0.0", family = Socket::AF_INET)
23
+ unless [Socket::AF_INET, Socket::AF_UNSPEC].include?(family)
24
+ raise ArgumentError, "Firewall IP helpers require IPv4"
25
+ end
26
+
27
+ if address.is_a?(String)
28
+ address = address.sub(%r{\Aany(?=/|\z)}i, "0.0.0.0")
29
+ unless %r{\A[0-9]+(?:\.[0-9]+){3}(?:/(?:[0-9]+|[0-9]+(?:\.[0-9]+){3}))?\z}.match?(address)
30
+ raise ArgumentError, "Invalid IPv4 expression: #{address.inspect}"
31
+ end
32
+
33
+ # 设备格式把带前导零的八位组和前缀都按十进制解释。
34
+ address = address.gsub(/\b0+(?=[0-9])/, "")
35
+ elsif !address.is_a?(Integer)
36
+ raise ArgumentError, "IPv4 input must be a string or integer"
37
+ end
38
+ super(address, Socket::AF_INET)
39
+ end
40
+
41
+ # 把地址转换成包含首尾整数的标准集合,供范围运算复用。
42
+ def to_set
43
+ raise ArgumentError, "Firewall IP helpers require IPv4" unless ipv4?
44
+
45
+ range = to_range
46
+ Set.new(range.begin.to_i..range.end.to_i)
47
+ end
48
+
49
+ class << self
50
+ # 接口地址保留主机位;不能直接用 IPAddr.new("host/prefix").to_s。
51
+ def host_cidr(expression, mask = nil)
52
+ host, prefix = string(expression, label: "Interface address").split("/", 2)
53
+ "#{from_i(to_i(host))}/#{prefix_length(mask || prefix || 32)}"
54
+ end
55
+
56
+ # 路由目的统一归一化为网络地址和前缀。
57
+ def network_cidr(expression, mask = nil)
58
+ address = new(host_cidr(expression, mask))
59
+ "#{address}/#{address.prefix}"
60
+ end
61
+
62
+ # 取接口 CIDR 中的主机地址,供 NAT 出口使用。
63
+ def host(expression) = host_cidr(expression).split("/", 2).first
64
+
65
+ # 把不带前缀的 IPv4 文本转换为整数。
66
+ def to_i(address)
67
+ unless address.is_a?(String) && !address.include?("/")
68
+ raise ArgumentError, "Invalid IPv4 address: #{address.inspect}"
69
+ end
70
+
71
+ new(address).to_i
72
+ end
73
+
74
+ # 把 IPv4 整数转换为点分十进制文本。
75
+ def from_i(number)
76
+ value = integer(number, max: IPV4_MAX, label: "IPv4 integer")
77
+ new(value).to_s
78
+ end
79
+
80
+ # 把前缀长度或掩码转换为规范前缀长度。
81
+ def prefix_length(mask)
82
+ mask = integer(mask, max: 32, label: "Prefix length") unless mask.is_a?(String)
83
+ new("0.0.0.0/#{mask}").prefix
84
+ end
85
+
86
+ # 根据前缀长度生成点分十进制网络掩码。
87
+ def netmask(mask) = new(IPV4_MAX).mask(prefix_length(mask)).to_s
88
+
89
+ # 把通配符掩码转换为网络掩码。
90
+ def wildcard_mask(wildcard)
91
+ unless wildcard.is_a?(String) && /\A[0-9]+(?:\.[0-9]+){3}\z/.match?(wildcard)
92
+ raise ArgumentError, "Invalid IPv4 wildcard"
93
+ end
94
+
95
+ (~new(wildcard)).to_s
96
+ end
97
+
98
+ # 根据地址和掩码计算覆盖的整数地址集合。
99
+ def subnet(address, mask = 32)
100
+ new(to_i(address)).mask(prefix_length(mask.nil? ? 32 : mask)).to_set
101
+ end
102
+
103
+ # 返回子网的网络地址文本。
104
+ def network(address, mask = 32) = from_i(subnet(address, mask).min)
105
+
106
+ # 解析主机、CIDR 或网络掩码表达式,也支持两个 IPv4 端点。
107
+ def parse(expression)
108
+ text = string(expression, label: "IPv4 expression")
109
+ parts = text.split("-", -1)
110
+ return between(*parts) if parts.size == 2
111
+ raise ArgumentError, "Invalid IPv4 expression: #{text}" unless parts.size == 1
112
+
113
+ new(text).to_set
114
+ end
115
+
116
+ # 用两个端点创建有序的闭区间。
117
+ def between(first, last)
118
+ low, high = [to_i(first), to_i(last)].minmax
119
+ Set.new(low..high)
120
+ end
121
+
122
+ # 将地址范围精确拆成 CIDR,供持久化与命令生成共用。
123
+ def cidrs(set)
124
+ set.flat_map do |range|
125
+ cursor = range.begin
126
+ last = range.end
127
+ blocks = []
128
+ while cursor <= last
129
+ size = 1
130
+ size *= 2 while (cursor % (size * 2)).zero? && cursor + (size * 2) - 1 <= last
131
+ blocks << [IPAddr.from_i(cursor), 32 - (size.bit_length - 1)]
132
+ cursor += size
133
+ end
134
+ blocks
135
+ end
136
+ end
137
+
138
+ # 把整数区间压缩成最短的单个网段或端点表达式。
139
+ def format_range(range)
140
+ unless range.is_a?(Range) && !range.exclude_end? && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
141
+ raise ArgumentError, "IPv4 notation requires a closed integer range"
142
+ end
143
+
144
+ integer(range.begin, max: IPV4_MAX, label: "IPv4 lower bound")
145
+ integer(range.end, max: IPV4_MAX, label: "IPv4 upper bound")
146
+ raise ArgumentError, "IPv4 range is reversed" if range.begin > range.end
147
+
148
+ size = range.end - range.begin + 1
149
+ first = from_i(range.begin)
150
+ if size.nobits?(size - 1) && (range.begin % size).zero?
151
+ "#{first}/#{32 - (size.bit_length - 1)}"
152
+ else
153
+ "#{first}-#{from_i(range.end)}"
154
+ end
155
+ end
156
+
157
+ # 解析单个端口或端口范围,并限制在 16 位端口空间内。
158
+ def ports(expression)
159
+ if expression.is_a?(Range)
160
+ result = Set.new(expression)
161
+ raise ArgumentError, "Port must be in 0..#{PORT_MAX}" if result.max > PORT_MAX
162
+
163
+ return result
164
+ end
165
+ text = expression.is_a?(Integer) ? expression.to_s : string(expression, label: "Port")
166
+ match = /\A\s*([0-9]+)(?:(?:\s*-\s*|\s+)([0-9]+))?\s*\z/.match(text)
167
+ raise ArgumentError, "Invalid port expression: #{text}" unless match
168
+
169
+ first, last = [match[1], match[2] || match[1]].map do |endpoint|
170
+ integer(endpoint, max: PORT_MAX, label: "Port")
171
+ end.minmax
172
+ Set.new(first..last)
173
+ end
174
+
175
+ # 把协议名称或编号转换成服务编码使用的协议编号。
176
+ def protocol_number(protocol)
177
+ protocol = protocol.to_s if protocol.is_a?(Integer) || protocol.is_a?(Symbol)
178
+ name = string(protocol, label: "Protocol").downcase
179
+ return ANY_PROTOCOL if name == "any"
180
+
181
+ PROTOCOL_NUMBERS.fetch(name) { integer(name, max: 255, label: "Protocol number") }
182
+ end
183
+
184
+ # 两种 ICMP 都以消息类型和代码匹配,不把类型限制当作普通目的端口。
185
+ def icmp?(protocol) = [ICMP, ICMPV6].include?(protocol_number(protocol))
186
+
187
+ # 服务编码的高八位保存协议,低十六位保存目标端口;省略端口时使用零。
188
+ def service(expression)
189
+ text = string(expression, label: "Service expression")
190
+ protocol, port, *unexpected_parts = text.split("/", -1)
191
+ raise ArgumentError, "Invalid service expression: #{text}" unless protocol && unexpected_parts.empty?
192
+
193
+ ranges = ports(port || 0)
194
+ return Set.new(0..SERVICE_MAX) if protocol.casecmp?(ANY_PROTOCOL.to_s) || protocol.casecmp?("any")
195
+
196
+ base = protocol_number(protocol) << 16
197
+ Set.new(*ranges.map { |range| (base + range.begin)..(base + range.end) })
198
+ end
199
+
200
+ private
201
+
202
+ # 校验并转换整数输入,同时限制其数值范围。
203
+ def integer(value, max:, label:)
204
+ unless value.is_a?(Integer) || (value.is_a?(String) && /\A[0-9]+\z/.match?(value))
205
+ raise ArgumentError, "#{label} must be an integer in 0..#{max}"
206
+ end
207
+
208
+ number = value.is_a?(Integer) ? value : Integer(value, 10)
209
+ raise ArgumentError, "#{label} must be in 0..#{max}" unless number.between?(0, max)
210
+
211
+ number
212
+ end
213
+
214
+ # 校验文本输入并保留原字符串,便于上层继续解析原始表达式。
215
+ def string(value, label:)
216
+ raise ArgumentError, "#{label} must be a string" unless value.is_a?(String)
217
+
218
+ value
219
+ end
220
+ end
221
+ end
222
+ end
data/lib/inet/ipv6.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "set"
5
+
6
+ module Inet
7
+ # IPv6 范围独立于 IPv4;JSON 端点使用地址文本,避免客户端整数精度丢失。
8
+ module IPv6
9
+ module_function
10
+
11
+ def address(value)
12
+ validate_text!(value)
13
+ result = ::IPAddr.new(value)
14
+ raise ArgumentError, "Expected IPv6" unless result.ipv6?
15
+
16
+ result
17
+ end
18
+
19
+ def canonical(value, host: false)
20
+ ip = address(value)
21
+ return "#{address(value.split('/').first)}/#{ip.prefix}" if host
22
+
23
+ value.include?("/") ? "#{ip}/#{ip.prefix}" : ip.to_s
24
+ end
25
+
26
+ def parse(value)
27
+ validate_text!(value)
28
+ if value.include?("-")
29
+ parts = value.split("-", -1)
30
+ unless parts.size == 2 && parts.none? { |part| part.include?("/") }
31
+ raise ArgumentError, "IPv6 range needs two hosts"
32
+ end
33
+
34
+ first, last = parts.map { |part| address(part).to_i }
35
+ Set.new(first..last)
36
+ else
37
+ ip = address(value)
38
+ Set.new(ip.to_range.begin.to_i..ip.to_range.end.to_i)
39
+ end
40
+ end
41
+
42
+ def normalize(value)
43
+ validate_text!(value)
44
+ return canonical(value) unless value.include?("-")
45
+
46
+ range = parse(value).ranges.first
47
+ "#{from_i(range.begin)}-#{from_i(range.end)}"
48
+ end
49
+
50
+ def from_i(value)
51
+ unless value.is_a?(Integer) && value.between?(0, ::IPAddr::IN6MASK)
52
+ raise ArgumentError, "IPv6 integer must be in 0..#{::IPAddr::IN6MASK}"
53
+ end
54
+
55
+ ::IPAddr.new(value, Socket::AF_INET6).to_s
56
+ end
57
+
58
+ def ranges(set) = set.map { |range| [from_i(range.begin), from_i(range.end)] }
59
+
60
+ def validate_text!(value)
61
+ return if value.is_a?(String) && value == value.strip && !value.include?("%")
62
+
63
+ raise ArgumentError, "Invalid IPv6 address"
64
+ end
65
+ private_class_method :validate_text!
66
+ end
67
+ end
data/lib/inet/set.rb ADDED
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inet
4
+ # 不变量是区间有序、互不相交且已合并;遍历区间而不是逐个枚举主机。
5
+ class Set
6
+ include Enumerable
7
+
8
+ attr_reader :ranges, :hash
9
+
10
+ # 校验、排序并合并区间,然后冻结集合以保证值语义。
11
+ def initialize(*ranges)
12
+ ordered = ranges.map { |range| interval_endpoints(range) }.sort_by(&:first)
13
+ merged = []
14
+ ordered.each do |first, last|
15
+ if !merged.empty? && first <= merged.last[1] + 1
16
+ merged.last[1] = [merged.last[1], last].max
17
+ else
18
+ merged << [first, last]
19
+ end
20
+ end
21
+ @ranges = merged.map { |first, last| first..last }.freeze
22
+ @hash = @ranges.hash
23
+ freeze
24
+ end
25
+
26
+ # 遍历规范化后的区间;没有代码块时返回枚举器。
27
+ def each(&block)
28
+ return enum_for(__method__) unless block
29
+
30
+ ranges.each(&block)
31
+ self
32
+ end
33
+
34
+ # 判断集合是否没有区间。
35
+ def empty? = ranges.empty?
36
+
37
+ # 返回区间段数量,而不是地址或端口总数。
38
+ def size = ranges.size
39
+
40
+ # 返回最小端点;空集合返回 nil。
41
+ def min = ranges.first&.begin
42
+
43
+ # 返回最大端点;空集合返回 nil。
44
+ def max = ranges.last&.end
45
+
46
+ # 计算所有闭区间包含的元素总数。
47
+ def cardinality = sum { |range| range.end - range.begin + 1 }
48
+
49
+ # 仅按规范化后的区间比较集合内容。
50
+ def ==(other) = other.is_a?(Set) && ranges == other.ranges
51
+
52
+ alias eql? ==
53
+
54
+ # 计算并集;两个集合为空或相同对象时直接复用已有值。
55
+ def |(other)
56
+ validate_operand!(other)
57
+ return self if other.empty? || equal?(other)
58
+ return other if empty?
59
+
60
+ self.class.new(*ranges, *other.ranges)
61
+ end
62
+
63
+ # 用双指针扫描两个有序集合,计算交集。
64
+ def &(other)
65
+ validate_operand!(other)
66
+ return self if equal?(other) || empty?
67
+ return other if other.empty?
68
+
69
+ result = []
70
+ left = right = 0
71
+ while left < size && right < other.size
72
+ a = ranges[left]
73
+ b = other.ranges[right]
74
+ first = [a.begin, b.begin].max
75
+ last = [a.end, b.end].min
76
+ result << (first..last) if first <= last
77
+ left += 1 if a.end <= b.end
78
+ right += 1 if b.end <= a.end
79
+ end
80
+ self.class.new(*result)
81
+ end
82
+
83
+ # 从当前集合中减去另一个集合,逐段截去重叠部分。
84
+ def -(other)
85
+ validate_operand!(other)
86
+ return self if empty? || other.empty?
87
+ return self.class.new if equal?(other)
88
+
89
+ result = []
90
+ right = 0
91
+ each do |range|
92
+ cursor = range.begin
93
+ right += 1 while right < other.size && other.ranges[right].end < cursor
94
+ result.concat(subtract_interval(range, other.ranges, right))
95
+ end
96
+ self.class.new(*result)
97
+ end
98
+
99
+ # 判断整数是否落在任一规范化区间内。
100
+ def include?(number)
101
+ raise ArgumentError, "Membership requires an integer" unless number.is_a?(Integer)
102
+
103
+ candidate = ranges.bsearch { |range| range.end >= number }
104
+ candidate ? candidate.begin <= number : false
105
+ end
106
+
107
+ # 判断集合是否被当前集合覆盖;空集合始终被覆盖。
108
+ def cover?(other)
109
+ return include?(other) if other.is_a?(Integer)
110
+
111
+ validate_operand!(other)
112
+ return covers_interval?(other.ranges.first) if other.size == 1
113
+
114
+ index = 0
115
+ other.each do |range|
116
+ index += 1 while index < size && ranges[index].end < range.begin
117
+ return false if index == size || ranges[index].begin > range.begin || ranges[index].end < range.end
118
+ end
119
+ true
120
+ end
121
+
122
+ # 判断当前集合是否是另一个集合的子集。
123
+ def subset?(other)
124
+ validate_operand!(other)
125
+ other.cover?(self)
126
+ end
127
+
128
+ # 判断两个有序集合是否存在重叠区间。
129
+ def overlap?(other)
130
+ validate_operand!(other)
131
+ left = right = 0
132
+ while left < size && right < other.size
133
+ a = ranges[left]
134
+ b = other.ranges[right]
135
+ return true if a.overlap?(b)
136
+
137
+ a.end < b.end ? left += 1 : right += 1
138
+ end
139
+ false
140
+ end
141
+
142
+ # 返回相等、包含、被包含或互不相同四种关系之一。
143
+ def relation(other)
144
+ validate_operand!(other)
145
+ return :equal if self == other
146
+ return :contains if cover?(other)
147
+ return :within if subset?(other)
148
+
149
+ :other
150
+ end
151
+
152
+ # 返回区间数组的浅副本,避免调用方修改内部数组。
153
+ def to_a = ranges.dup
154
+
155
+ # 哈希值只在当前进程有效;持久化区间,并在读取进程中重新建立冻结集合。
156
+ def marshal_dump = ranges
157
+
158
+ # 从持久化区间恢复集合的不变量和冻结状态。
159
+ def marshal_load(ranges)
160
+ initialize(*ranges)
161
+ end
162
+
163
+ private
164
+
165
+ # 把合法区间统一为闭区间端点。
166
+ def interval_endpoints(range)
167
+ unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
168
+ raise ArgumentError, "Intervals must have integer endpoints"
169
+ end
170
+
171
+ first = range.begin
172
+ last = range.end - (range.exclude_end? ? 1 : 0)
173
+ raise ArgumentError, "Intervals must be nonnegative and ordered" if first.negative? || first > last
174
+
175
+ [first, last]
176
+ end
177
+
178
+ # 对单个区间做差,调用方复用已找到的右侧起点。
179
+ def subtract_interval(range, removed_ranges, scan)
180
+ result = []
181
+ cursor = range.begin
182
+ while scan < removed_ranges.size && removed_ranges[scan].begin <= range.end
183
+ removed = removed_ranges[scan]
184
+ result << (cursor..(removed.begin - 1)) if removed.begin > cursor
185
+ cursor = [cursor, removed.end + 1].max
186
+ break if cursor > range.end
187
+
188
+ scan += 1
189
+ end
190
+ result << (cursor..range.end) if cursor <= range.end
191
+ result
192
+ end
193
+
194
+ # 单区间包含检查使用二分查找。
195
+ def covers_interval?(target)
196
+ candidate = ranges.bsearch { |range| range.end >= target.end }
197
+ candidate ? candidate.begin <= target.begin : false
198
+ end
199
+
200
+ # 统一校验集合运算的右值类型。
201
+ def validate_operand!(other)
202
+ raise ArgumentError, "Expected a Set" unless other.is_a?(Set)
203
+ end
204
+ end
205
+ end
data/lib/inet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Inet
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/inet.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "inet/version"
4
+ require_relative "inet/set"
5
+ require_relative "inet/ipaddr"
6
+ require_relative "inet/ipv6"
4
7
 
5
- # inet 的公共命名空间;首版只提供加载入口与版本信息。
8
+ # IP 地址、服务编码与不可变整数区间集合的公共命名空间。
6
9
  module Inet
7
10
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - inet contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 2026-10-23 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: The initial inet package provides a loadable namespace and version information.
12
+ description: IP address normalization, address ranges, service encoding and immutable
13
+ integer interval sets.
13
14
  executables: []
14
15
  extensions: []
15
16
  extra_rdoc_files: []
@@ -19,6 +20,9 @@ files:
19
20
  - README.md
20
21
  - docs/releasing.md
21
22
  - lib/inet.rb
23
+ - lib/inet/ipaddr.rb
24
+ - lib/inet/ipv6.rb
25
+ - lib/inet/set.rb
22
26
  - lib/inet/version.rb
23
27
  homepage: https://github.com/gatework/inet
24
28
  licenses:
@@ -43,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
47
  - !ruby/object:Gem::Version
44
48
  version: '0'
45
49
  requirements: []
46
- rubygems_version: 4.0.12
50
+ rubygems_version: 4.0.20
47
51
  specification_version: 4
48
- summary: Foundation for the inet Ruby library
52
+ summary: IPv4, IPv6 and immutable interval sets
49
53
  test_files: []