Skip to main content

bsv_rs/script/
chunk.rs

1//! Script chunk representation.
2//!
3//! A ScriptChunk represents a single element of a script: either an opcode
4//! or a data push operation.
5
6use super::op::opcode_to_name;
7use crate::primitives::to_hex;
8
9/// A representation of a chunk of a script, which includes an opcode.
10/// For push operations, the associated data to push onto the stack is also included.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ScriptChunk {
13    /// The opcode value
14    pub op: u8,
15    /// Data for push operations (None for non-push ops)
16    pub data: Option<Vec<u8>>,
17}
18
19impl ScriptChunk {
20    /// Creates a new opcode-only chunk (no data).
21    pub fn new_opcode(op: u8) -> Self {
22        Self { op, data: None }
23    }
24
25    /// Creates a new data push chunk.
26    ///
27    /// The appropriate opcode is determined based on the data length:
28    /// - 0 bytes: OP_0
29    /// - 1-75 bytes: direct push (opcode = length)
30    /// - 76-255 bytes: OP_PUSHDATA1
31    /// - 256-65535 bytes: OP_PUSHDATA2
32    /// - Larger: OP_PUSHDATA4
33    pub fn new_push(data: Vec<u8>) -> Self {
34        let len = data.len();
35        let op = if len == 0 {
36            super::op::OP_0
37        } else if len < super::op::OP_PUSHDATA1 as usize {
38            len as u8
39        } else if len < 256 {
40            super::op::OP_PUSHDATA1
41        } else if len < 65536 {
42            super::op::OP_PUSHDATA2
43        } else {
44            super::op::OP_PUSHDATA4
45        };
46
47        Self {
48            op,
49            data: if data.is_empty() { None } else { Some(data) },
50        }
51    }
52
53    /// Creates a chunk from an opcode and optional data.
54    pub fn new(op: u8, data: Option<Vec<u8>>) -> Self {
55        Self { op, data }
56    }
57
58    /// Returns true if this chunk is a data push operation.
59    pub fn is_push_data(&self) -> bool {
60        self.op <= super::op::OP_16
61    }
62
63    /// Converts this chunk to its ASM string representation.
64    pub fn to_asm(&self) -> String {
65        match &self.data {
66            Some(data) => to_hex(data),
67            None => {
68                if self.op == super::op::OP_0 {
69                    "0".to_string()
70                } else if self.op == super::op::OP_1NEGATE {
71                    "-1".to_string()
72                } else {
73                    opcode_to_name(self.op)
74                        .map(|s| s.to_string())
75                        .unwrap_or_else(|| format!("OP_UNKNOWN{}", self.op))
76                }
77            }
78        }
79    }
80}
81
82impl Default for ScriptChunk {
83    fn default() -> Self {
84        Self::new_opcode(super::op::OP_0)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::script::op::*;
92
93    #[test]
94    fn test_new_opcode() {
95        let chunk = ScriptChunk::new_opcode(OP_DUP);
96        assert_eq!(chunk.op, OP_DUP);
97        assert!(chunk.data.is_none());
98    }
99
100    #[test]
101    fn test_new_push_empty() {
102        let chunk = ScriptChunk::new_push(vec![]);
103        assert_eq!(chunk.op, OP_0);
104        assert!(chunk.data.is_none());
105    }
106
107    #[test]
108    fn test_new_push_small() {
109        let data = vec![0x01, 0x02, 0x03];
110        let chunk = ScriptChunk::new_push(data.clone());
111        assert_eq!(chunk.op, 3); // Direct push opcode
112        assert_eq!(chunk.data, Some(data));
113    }
114
115    #[test]
116    fn test_new_push_medium() {
117        let data = vec![0u8; 100];
118        let chunk = ScriptChunk::new_push(data.clone());
119        assert_eq!(chunk.op, OP_PUSHDATA1);
120        assert_eq!(chunk.data, Some(data));
121    }
122
123    #[test]
124    fn test_new_push_large() {
125        let data = vec![0u8; 300];
126        let chunk = ScriptChunk::new_push(data.clone());
127        assert_eq!(chunk.op, super::super::op::OP_PUSHDATA2);
128        assert_eq!(chunk.data, Some(data));
129    }
130
131    #[test]
132    fn test_is_push_data() {
133        assert!(ScriptChunk::new_opcode(OP_0).is_push_data());
134        assert!(ScriptChunk::new_opcode(OP_1).is_push_data());
135        assert!(ScriptChunk::new_opcode(OP_16).is_push_data());
136        assert!(!ScriptChunk::new_opcode(OP_DUP).is_push_data());
137        assert!(!ScriptChunk::new_opcode(OP_CHECKSIG).is_push_data());
138    }
139
140    #[test]
141    fn test_to_asm_opcode() {
142        assert_eq!(ScriptChunk::new_opcode(OP_DUP).to_asm(), "OP_DUP");
143        assert_eq!(ScriptChunk::new_opcode(OP_HASH160).to_asm(), "OP_HASH160");
144        assert_eq!(ScriptChunk::new_opcode(OP_CHECKSIG).to_asm(), "OP_CHECKSIG");
145    }
146
147    #[test]
148    fn test_to_asm_special() {
149        assert_eq!(ScriptChunk::new_opcode(OP_0).to_asm(), "0");
150        assert_eq!(ScriptChunk::new_opcode(OP_1NEGATE).to_asm(), "-1");
151    }
152
153    #[test]
154    fn test_to_asm_data() {
155        let chunk = ScriptChunk::new(0x03, Some(vec![0xab, 0xcd, 0xef]));
156        assert_eq!(chunk.to_asm(), "abcdef");
157    }
158}