diff --git a/crates/stackforge-core/src/layer/mod.rs b/crates/stackforge-core/src/layer/mod.rs index d29d6ab..c24c777 100644 --- a/crates/stackforge-core/src/layer/mod.rs +++ b/crates/stackforge-core/src/layer/mod.rs @@ -290,6 +290,422 @@ impl LayerEnum { Self::Raw(l) => l.header_len(buf), } } + + /// Returns a detailed field-by-field representation for show() output. + /// Format: Vec<(field_name, field_value)> + pub fn show_fields(&self, buf: &[u8]) -> Vec<(&'static str, String)> { + match self { + Self::Ethernet(l) => ethernet_show_fields(l, buf), + Self::Dot3(l) => dot3_show_fields(l, buf), + Self::Arp(l) => arp_show_fields(l, buf), + Self::Ipv4(l) => ipv4_show_fields(l, buf), + Self::Ipv6(l) => ipv6_show_fields(l, buf), + Self::Icmp(l) => icmp_show_fields(l, buf), + Self::Icmpv6(l) => icmpv6_show_fields(l, buf), + Self::Tcp(l) => tcp_show_fields(l, buf), + Self::Udp(l) => udp_show_fields(l, buf), + Self::Dns(l) => dns_show_fields(l, buf), + Self::Raw(l) => raw_show_fields(l, buf), + } + } +} + +// ============================================================================ +// Show Fields Implementations +// ============================================================================ + +fn ethernet_show_fields(l: &EthernetLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let mut fields = Vec::new(); + fields.push(( + "dst", + l.dst(buf) + .map(|m| m.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "src", + l.src(buf) + .map(|m| m.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + let etype = l.ethertype(buf).unwrap_or(0); + fields.push(( + "type", + format!("{:#06x} ({})", etype, ethertype::name(etype)), + )); + fields +} + +fn dot3_show_fields(l: &Dot3Layer, buf: &[u8]) -> Vec<(&'static str, String)> { + let mut fields = Vec::new(); + fields.push(( + "dst", + l.dst(buf) + .map(|m| m.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "src", + l.src(buf) + .map(|m| m.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "len", + l.len_field(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields +} + +fn arp_show_fields(l: &ArpLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let mut fields = Vec::new(); + let hwtype = l.hwtype(buf).unwrap_or(0); + fields.push(( + "hwtype", + format!("{:#06x} ({})", hwtype, arp::hardware_type::name(hwtype)), + )); + let ptype = l.ptype(buf).unwrap_or(0); + fields.push(("ptype", format!("{:#06x}", ptype))); + fields.push(( + "hwlen", + l.hwlen(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "plen", + l.plen(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + let op = l.op(buf).unwrap_or(0); + fields.push(("op", format!("{} ({})", op, arp::opcode::name(op)))); + fields.push(( + "hwsrc", + l.hwsrc_raw(buf) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "psrc", + l.psrc_raw(buf) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "hwdst", + l.hwdst_raw(buf) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "pdst", + l.pdst_raw(buf) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields +} + +fn ipv4_show_fields(l: &Ipv4Layer, buf: &[u8]) -> Vec<(&'static str, String)> { + let mut fields = Vec::new(); + fields.push(( + "version", + l.version(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "ihl", + l.ihl(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "tos", + l.tos(buf) + .map(|v| format!("{:#04x}", v)) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "len", + l.total_len(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "id", + l.id(buf) + .map(|v| format!("{:#06x}", v)) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "flags", + l.flags(buf) + .map(|f| f.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "frag", + l.frag_offset(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "ttl", + l.ttl(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + let proto = l.protocol(buf).unwrap_or(0); + fields.push(("proto", format!("{} ({})", proto, l.protocol_name(buf)))); + fields.push(( + "chksum", + l.checksum(buf) + .map(|v| format!("{:#06x}", v)) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "src", + l.src(buf) + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "dst", + l.dst(buf) + .map(|ip| ip.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + // Options (if present) + let opts_len = l.options_len(buf); + if opts_len > 0 { + fields.push(("options", format!("[{} bytes]", opts_len))); + } + fields +} + +fn ipv6_show_fields(l: &Ipv6Layer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + let mut fields = Vec::new(); + if slice.len() >= 40 { + let version = (slice[0] >> 4) & 0x0F; + fields.push(("version", version.to_string())); + let traffic_class = ((slice[0] & 0x0F) << 4) | ((slice[1] >> 4) & 0x0F); + fields.push(("tc", format!("{:#04x}", traffic_class))); + let flow_label = + ((slice[1] as u32 & 0x0F) << 16) | ((slice[2] as u32) << 8) | (slice[3] as u32); + fields.push(("fl", format!("{:#07x}", flow_label))); + let payload_len = u16::from_be_bytes([slice[4], slice[5]]); + fields.push(("plen", payload_len.to_string())); + let nh = slice[6]; + fields.push(("nh", format!("{} ({})", nh, ipv4::protocol::to_name(nh)))); + let hlim = slice[7]; + fields.push(("hlim", hlim.to_string())); + // src/dst addresses + if slice.len() >= 40 { + let mut src_bytes = [0u8; 16]; + let mut dst_bytes = [0u8; 16]; + src_bytes.copy_from_slice(&slice[8..24]); + dst_bytes.copy_from_slice(&slice[24..40]); + let src = std::net::Ipv6Addr::from(src_bytes); + let dst = std::net::Ipv6Addr::from(dst_bytes); + fields.push(("src", src.to_string())); + fields.push(("dst", dst.to_string())); + } + } + fields +} + +fn icmp_show_fields(l: &IcmpLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + let mut fields = Vec::new(); + if !slice.is_empty() { + let icmp_type = slice[0]; + let type_name = match icmp_type { + 0 => "echo-reply", + 3 => "dest-unreach", + 4 => "source-quench", + 5 => "redirect", + 8 => "echo-request", + 11 => "time-exceeded", + 12 => "parameter-problem", + 13 => "timestamp", + 14 => "timestamp-reply", + _ => "unknown", + }; + fields.push(("type", format!("{} ({})", icmp_type, type_name))); + } + if slice.len() > 1 { + fields.push(("code", slice[1].to_string())); + } + if slice.len() >= 4 { + let chksum = u16::from_be_bytes([slice[2], slice[3]]); + fields.push(("chksum", format!("{:#06x}", chksum))); + } + if slice.len() >= 8 { + let id = u16::from_be_bytes([slice[4], slice[5]]); + let seq = u16::from_be_bytes([slice[6], slice[7]]); + fields.push(("id", format!("{:#06x}", id))); + fields.push(("seq", seq.to_string())); + } + fields +} + +fn icmpv6_show_fields(l: &Icmpv6Layer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + let mut fields = Vec::new(); + if !slice.is_empty() { + let icmp_type = slice[0]; + let type_name = match icmp_type { + 1 => "dest-unreach", + 2 => "pkt-too-big", + 3 => "time-exceeded", + 4 => "param-problem", + 128 => "echo-request", + 129 => "echo-reply", + 133 => "router-solicit", + 134 => "router-advert", + 135 => "neighbor-solicit", + 136 => "neighbor-advert", + _ => "unknown", + }; + fields.push(("type", format!("{} ({})", icmp_type, type_name))); + } + if slice.len() > 1 { + fields.push(("code", slice[1].to_string())); + } + if slice.len() >= 4 { + let chksum = u16::from_be_bytes([slice[2], slice[3]]); + fields.push(("chksum", format!("{:#06x}", chksum))); + } + fields +} + +fn tcp_show_fields(l: &TcpLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let mut fields = Vec::new(); + fields.push(( + "sport", + l.src_port(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "dport", + l.dst_port(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "seq", + l.seq(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "ack", + l.ack(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "dataofs", + l.data_offset(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "reserved", + l.reserved(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "flags", + l.flags(buf) + .map(|f| f.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "window", + l.window(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "chksum", + l.checksum(buf) + .map(|v| format!("{:#06x}", v)) + .unwrap_or_else(|_| "?".into()), + )); + fields.push(( + "urgptr", + l.urgent_ptr(buf) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "?".into()), + )); + // Options (if present) + let opts_len = l.options_len(buf); + if opts_len > 0 { + fields.push(("options", format!("[{} bytes]", opts_len))); + } + fields +} + +fn udp_show_fields(l: &UdpLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + let mut fields = Vec::new(); + if slice.len() >= 2 { + let sport = u16::from_be_bytes([slice[0], slice[1]]); + fields.push(("sport", sport.to_string())); + } + if slice.len() >= 4 { + let dport = u16::from_be_bytes([slice[2], slice[3]]); + fields.push(("dport", dport.to_string())); + } + if slice.len() >= 6 { + let len = u16::from_be_bytes([slice[4], slice[5]]); + fields.push(("len", len.to_string())); + } + if slice.len() >= 8 { + let chksum = u16::from_be_bytes([slice[6], slice[7]]); + fields.push(("chksum", format!("{:#06x}", chksum))); + } + fields +} + +fn dns_show_fields(l: &DnsLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + let mut fields = Vec::new(); + if slice.len() >= 12 { + let id = u16::from_be_bytes([slice[0], slice[1]]); + fields.push(("id", format!("{:#06x}", id))); + let flags = u16::from_be_bytes([slice[2], slice[3]]); + let qr = if (flags & 0x8000) != 0 { + "response" + } else { + "query" + }; + fields.push(("qr", qr.to_string())); + let opcode = (flags >> 11) & 0x0F; + fields.push(("opcode", opcode.to_string())); + let qdcount = u16::from_be_bytes([slice[4], slice[5]]); + fields.push(("qdcount", qdcount.to_string())); + let ancount = u16::from_be_bytes([slice[6], slice[7]]); + fields.push(("ancount", ancount.to_string())); + let nscount = u16::from_be_bytes([slice[8], slice[9]]); + fields.push(("nscount", nscount.to_string())); + let arcount = u16::from_be_bytes([slice[10], slice[11]]); + fields.push(("arcount", arcount.to_string())); + } + fields +} + +fn raw_show_fields(l: &RawLayer, buf: &[u8]) -> Vec<(&'static str, String)> { + let slice = l.index.slice(buf); + vec![("load", format!("[{} bytes]", slice.len()))] } // Placeholder layer structs (to be fully implemented in later weeks) diff --git a/crates/stackforge-core/src/packet.rs b/crates/stackforge-core/src/packet.rs index 814ce7d..40c3d57 100644 --- a/crates/stackforge-core/src/packet.rs +++ b/crates/stackforge-core/src/packet.rs @@ -14,9 +14,10 @@ use smallvec::SmallVec; use crate::error::{PacketError, Result}; use crate::layer::{ - LayerIndex, LayerKind, + DnsLayer, IcmpLayer, Icmpv6Layer, Ipv6Layer, LayerEnum, LayerIndex, LayerKind, RawLayer, + TcpLayer, UdpLayer, arp::ArpLayer, - ethernet::{ETHERNET_HEADER_LEN, EthernetLayer}, + ethernet::{Dot3Layer, ETHERNET_HEADER_LEN, EthernetLayer}, ethertype, ip_protocol, ipv4::Ipv4Layer, }; @@ -179,6 +180,33 @@ impl Packet { .map(|idx| ArpLayer::new(idx.start, idx.end)) } + /// Get a LayerEnum for a given LayerIndex. + pub fn layer_enum(&self, idx: &LayerIndex) -> LayerEnum { + match idx.kind { + LayerKind::Ethernet => LayerEnum::Ethernet(EthernetLayer::new(idx.start, idx.end)), + LayerKind::Dot3 => LayerEnum::Dot3(Dot3Layer::new(idx.start, idx.end)), + LayerKind::Arp => LayerEnum::Arp(ArpLayer::new(idx.start, idx.end)), + LayerKind::Ipv4 => LayerEnum::Ipv4(Ipv4Layer::new(idx.start, idx.end)), + LayerKind::Ipv6 => LayerEnum::Ipv6(Ipv6Layer { index: *idx }), + LayerKind::Icmp => LayerEnum::Icmp(IcmpLayer { index: *idx }), + LayerKind::Icmpv6 => LayerEnum::Icmpv6(Icmpv6Layer { index: *idx }), + LayerKind::Tcp => LayerEnum::Tcp(TcpLayer::new(idx.start, idx.end)), + LayerKind::Udp => LayerEnum::Udp(UdpLayer { index: *idx }), + LayerKind::Dns => LayerEnum::Dns(DnsLayer { index: *idx }), + LayerKind::Raw + | LayerKind::Dot1Q + | LayerKind::Dot1AD + | LayerKind::Dot1AH + | LayerKind::LLC + | LayerKind::SNAP => LayerEnum::Raw(RawLayer { index: *idx }), + } + } + + /// Get all layers as LayerEnum objects. + pub fn layer_enums(&self) -> Vec { + self.layers.iter().map(|idx| self.layer_enum(idx)).collect() + } + // ======================================================================== // Parsing (Index-Only) // ======================================================================== diff --git a/src/lib.rs b/src/lib.rs index ff9284c..992f3df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -279,40 +279,129 @@ impl PyPacket { PyBytes::new(py, self.inner.payload()) } - /// Returns a string representation of the packet. + /// Returns a Scapy-style string representation of the packet. + /// + /// For parsed packets, shows a summary of each layer: + /// ` 192.168.1.2 | TCP 80 > 8080 [S]>` fn __repr__(&self) -> String { - let layer_info = if self.inner.is_parsed() { - let kinds: Vec<_> = self.inner.layers().iter().map(|l| l.kind.name()).collect(); - kinds.join(" / ") + if !self.inner.is_parsed() { + return format!("", self.inner.len()); + } + + let buf = self.inner.as_bytes(); + let summaries: Vec = self + .inner + .layer_enums() + .iter() + .map(|layer| layer.summary(buf)) + .collect(); + + if summaries.is_empty() { + format!("", self.inner.len()) } else { - "unparsed".to_string() - }; - format!("", self.inner.len(), layer_info) + format!("<{}>", summaries.join(" | ")) + } } - /// Returns a human-readable summary of the packet structure. + /// Returns a Scapy-style detailed view of the packet structure. + /// + /// Displays each layer with all its fields, similar to Scapy's show() method: + /// ```text + /// ###[ Ethernet ]### + /// dst = ff:ff:ff:ff:ff:ff + /// src = 00:11:22:33:44:55 + /// type = 0x0800 (IPv4) + /// ###[ IP ]### + /// version = 4 + /// ihl = 5 + /// ... + /// ``` fn show(&self) -> String { let mut output = String::new(); - output.push_str(&format!("###[ Packet: {} bytes ]###\n", self.inner.len())); if !self.inner.is_parsed() { - output.push_str(" (not parsed)\n"); + output.push_str(&format!("###[ Packet: {} bytes ]###\n", self.inner.len())); + output.push_str(" (not parsed - call parse() first)\n"); return output; } - for layer in self.inner.layers() { - output.push_str(&format!("###[ {} ]###\n", layer.kind.name())); - output.push_str(&format!( - " offset = {}..{} ({} bytes)\n", - layer.start, - layer.end, - layer.len() - )); + let buf = self.inner.as_bytes(); + + for layer_enum in self.inner.layer_enums() { + let kind = layer_enum.kind(); + output.push_str(&format!("###[ {} ]###\n", kind.name())); + + // Get all fields for this layer + let fields = layer_enum.show_fields(buf); + + // Calculate max field name length for alignment + let max_name_len = fields.iter().map(|(name, _)| name.len()).max().unwrap_or(0); + + for (name, value) in fields { + output.push_str(&format!( + " {:>() + .join(" "); + output.push_str(&format!(" {}", hex_str)); + if payload.len() > 32 { + output.push_str("..."); + } + output.push('\n'); } output } + /// Returns a compact one-line summary of the packet. + /// + /// Example: `Ether / IP / TCP 80 > 8080 [S]` + fn summary(&self) -> String { + if !self.inner.is_parsed() { + return format!("Packet ({} bytes, unparsed)", self.inner.len()); + } + + let buf = self.inner.as_bytes(); + let layer_names: Vec = self + .inner + .layers() + .iter() + .map(|l| l.kind.name().to_string()) + .collect(); + + if layer_names.is_empty() { + format!("Packet ({} bytes, no layers)", self.inner.len()) + } else { + // Get summary of the last significant layer + let layers = self.inner.layer_enums(); + if let Some(last) = layers.last() { + let last_summary = last.summary(buf); + format!( + "{} / {}", + layer_names[..layer_names.len() - 1].join(" / "), + last_summary + ) + } else { + layer_names.join(" / ") + } + } + } + /// Returns a hexdump of the packet bytes. fn hexdump(&self) -> String { hexdump_bytes(self.inner.as_bytes()) diff --git a/uv.lock b/uv.lock index d268b5e..b81d336 100644 --- a/uv.lock +++ b/uv.lock @@ -56,6 +56,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "maturin" +version = "1.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/84/bfed8cc10e2d8b6656cf0f0ca6609218e6fcb45a62929f5094e1063570f7/maturin-1.11.5.tar.gz", hash = "sha256:7579cf47640fb9595a19fe83a742cbf63203f0343055c349c1cab39045a30c29", size = 226885, upload-time = "2026-01-09T11:06:13.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/6c/3443d2f8c6d4eae5fc7479cd4053542aff4c1a8566d0019d0612d241b15a/maturin-1.11.5-py3-none-linux_armv6l.whl", hash = "sha256:edd1d4d35050ea2b9ef42aa01e87fe019a1e822940346b35ccb973e0aa8f6d82", size = 8845897, upload-time = "2026-01-09T11:06:17.327Z" }, + { url = "https://files.pythonhosted.org/packages/c5/03/abf1826d8aebc0d47ef6d21bdd752d98d63ac4372ad2b115db9cd5176229/maturin-1.11.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2a596eab137cb3e169b97e89a739515abfa7a8755e2e5f0fc91432ef446f74f4", size = 17233855, upload-time = "2026-01-09T11:06:04.272Z" }, + { url = "https://files.pythonhosted.org/packages/90/a1/5ad62913271724035a7e4bcf796d7c95b4119317ae5f8cb034844aa99bc4/maturin-1.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1c27a2eb47821edf26c75d100b3150b52dca2c1a5f074d7514af06f7a7acb9d5", size = 8881776, upload-time = "2026-01-09T11:06:10.24Z" }, + { url = "https://files.pythonhosted.org/packages/c6/66/997974b44f8d3de641281ec04fbf5b6ca821bdc8291a2fa73305978db74d/maturin-1.11.5-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f1320dacddcd3aa84a4bdfc77ee6fdb60e4c3835c853d7eb79c09473628b0498", size = 8870347, upload-time = "2026-01-09T11:06:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/58/e0/c8fa042daf0608cc2e9a59b6df3a9e287bfc7f229136f17727f4118bac2d/maturin-1.11.5-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:ffe7418834ff3b4a6c987187b7abb85ba033f4733e089d77d84e2de87057b4e7", size = 9291396, upload-time = "2026-01-09T11:06:02.05Z" }, + { url = "https://files.pythonhosted.org/packages/99/af/9d3edc8375efc8d435d5f24794bc4de234d4e743447592da970d53b31361/maturin-1.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c739b243d012386902f112ea63a54a94848932b70ae3565fa5e121fd1c0200e0", size = 8827831, upload-time = "2026-01-09T11:06:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc341f6abbf9005f90935a4ee5dc7b30e2df7d1bb90b96d48b756b2c0ee7/maturin-1.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:8127d2cd25950bacbcdc8a2ec6daab1d4d27200f7d73964392680ad64d27f7f0", size = 8718895, upload-time = "2026-01-09T11:06:21.617Z" }, + { url = "https://files.pythonhosted.org/packages/76/17/654a59c66287e287373f2a0086e4fc8a23f0545a81c2bd6e324db26a5801/maturin-1.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:2a4e872fb78e77748217084ffeb59de565d08a86ccefdace054520aaa7b66db4", size = 11384741, upload-time = "2026-01-09T11:06:15.261Z" }, + { url = "https://files.pythonhosted.org/packages/2e/da/7118de648182971d723ea99d79c55007f96cdafc95f5322cc1ad15f6683e/maturin-1.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2079447967819b5cf615e5b5b99a406d662effdc8d6afd493dcd253c6afc3707", size = 9423814, upload-time = "2026-01-09T11:05:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/be14395c6e23b19ddaa0c171e68915bdcd1ef61ad1f411739c6721196903/maturin-1.11.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:50f6c668c1d5d4d4dc1c3ffec7b4270dab493e5b2368f8e4213f4bcde6a50eea", size = 9104378, upload-time = "2026-01-09T11:05:59.835Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/53ea82a2f42a03930ea5545673d11a4ef49bb886827353a701f41a5f11c4/maturin-1.11.5-py3-none-win32.whl", hash = "sha256:49f85ce6cbe478e9743ecddd6da2964afc0ded57013aa4d054256be702d23d40", size = 7738696, upload-time = "2026-01-09T11:06:06.651Z" }, + { url = "https://files.pythonhosted.org/packages/3c/41/353a26d49aa80081c514a6354d429efbecedb90d0153ec598cece3baa607/maturin-1.11.5-py3-none-win_amd64.whl", hash = "sha256:70d3e5beffb9ef9dfae5f3c1a7eeb572091505eb8cb076e9434518df1c42a73b", size = 9029838, upload-time = "2026-01-09T11:05:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/c94f8f5440bc42d54113a2d99de0d6107f06b5a33f31823e52b2715d856f/maturin-1.11.5-py3-none-win_arm64.whl", hash = "sha256:9348f7f0a346108e0c96e6719be91da4470bd43c15802435e9f4157f5cca43d4", size = 7624029, upload-time = "2026-01-09T11:06:08.728Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -197,11 +218,11 @@ wheels = [ [[package]] name = "stackforge" -version = "0.1.0" source = { editable = "." } [package.dev-dependencies] dev = [ + { name = "maturin" }, { name = "pre-commit" }, { name = "pytest" }, { name = "ruff" }, @@ -211,6 +232,7 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "maturin", specifier = ">=1.4,<2.0" }, { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "ruff", specifier = ">=0.14.10" },