1use crate::script::error::ScriptError;
7use crate::script::op::Op;
8use crate::script::script_chunk::ScriptChunk;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Script {
13 chunks: Vec<ScriptChunk>,
14}
15
16impl Script {
17 pub fn new() -> Self {
19 Script { chunks: Vec::new() }
20 }
21
22 pub fn from_binary(bytes: &[u8]) -> Self {
24 Script {
25 chunks: Self::parse_chunks(bytes),
26 }
27 }
28
29 pub fn from_hex(hex: &str) -> Result<Self, ScriptError> {
31 if hex.is_empty() {
32 return Ok(Script::new());
33 }
34 if !hex.len().is_multiple_of(2) {
35 return Err(ScriptError::InvalidFormat(
36 "hex string has odd length".to_string(),
37 ));
38 }
39 let bytes = hex_to_bytes(hex).map_err(ScriptError::InvalidFormat)?;
40 Ok(Script::from_binary(&bytes))
41 }
42
43 pub fn from_asm(asm: &str) -> Self {
49 if asm.is_empty() {
50 return Script::new();
51 }
52
53 let mut chunks = Vec::new();
54 let tokens: Vec<&str> = asm.split(' ').collect();
55 let mut i = 0;
56
57 while i < tokens.len() {
58 let token = tokens[i];
59
60 if token == "0" {
62 chunks.push(ScriptChunk::new_opcode(Op::Op0));
63 i += 1;
64 continue;
65 }
66 if token == "-1" {
67 chunks.push(ScriptChunk::new_opcode(Op::Op1Negate));
68 i += 1;
69 continue;
70 }
71
72 if let Some(op) = Op::from_name(token) {
74 if op == Op::OpPushData1 || op == Op::OpPushData2 || op == Op::OpPushData4 {
76 if i + 2 < tokens.len() {
78 let hex_data = tokens[i + 2];
79 let data = hex_to_bytes(hex_data).unwrap_or_default();
80 chunks.push(ScriptChunk::new_raw(op.to_byte(), Some(data)));
81 i += 3;
82 } else {
83 chunks.push(ScriptChunk::new_opcode(op));
84 i += 1;
85 }
86 } else {
87 chunks.push(ScriptChunk::new_opcode(op));
88 i += 1;
89 }
90 continue;
91 }
92
93 let mut hex = token.to_string();
95 if !hex.len().is_multiple_of(2) {
96 hex = format!("0{}", hex);
97 }
98 if let Ok(data) = hex_to_bytes(&hex) {
99 let len = data.len();
100 let op_byte = if len < 0x4c {
101 len as u8
102 } else if len < 256 {
103 Op::OpPushData1.to_byte()
104 } else if len < 65536 {
105 Op::OpPushData2.to_byte()
106 } else {
107 Op::OpPushData4.to_byte()
108 };
109 chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
110 }
111 i += 1;
112 }
113
114 Script { chunks }
115 }
116
117 pub fn from_chunks(chunks: Vec<ScriptChunk>) -> Self {
119 Script { chunks }
120 }
121
122 pub fn to_binary(&self) -> Vec<u8> {
124 let mut out = Vec::new();
125 for (i, chunk) in self.chunks.iter().enumerate() {
126 let serialized = chunk.serialize();
127 out.extend_from_slice(&serialized);
128 if chunk.op == Op::OpReturn && chunk.data.is_some() {
132 let _ = i; }
136 }
137 out
138 }
139
140 pub fn to_hex(&self) -> String {
142 let bytes = self.to_binary();
143 bytes.iter().map(|b| format!("{:02x}", b)).collect()
144 }
145
146 pub fn to_asm(&self) -> String {
148 self.chunks
149 .iter()
150 .map(|c| c.to_asm())
151 .collect::<Vec<_>>()
152 .join(" ")
153 }
154
155 pub fn chunks(&self) -> &[ScriptChunk] {
157 &self.chunks
158 }
159
160 pub fn len(&self) -> usize {
162 self.chunks.len()
163 }
164
165 pub fn is_empty(&self) -> bool {
167 self.chunks.is_empty()
168 }
169
170 pub fn find_and_delete(&self, target: &Script) -> Script {
176 let target_bytes = target.to_binary();
177 let target_len = target_bytes.len();
178 if target_len == 0 {
179 return self.clone();
180 }
181
182 let target_op = target_bytes[0];
183
184 if !self.chunks.iter().any(|c| c.op_byte == target_op) {
186 return self.clone();
187 }
188
189 let mut result_chunks = self.chunks.clone();
191 Self::retain_non_matching(&mut result_chunks, target_op, target_len, &target_bytes);
192 Script {
193 chunks: result_chunks,
194 }
195 }
196
197 pub fn find_and_delete_owned(mut self, target: &Script) -> Script {
202 let target_bytes = target.to_binary();
203 let target_len = target_bytes.len();
204 if target_len == 0 {
205 return self;
206 }
207
208 let target_op = target_bytes[0];
209
210 if !self.chunks.iter().any(|c| c.op_byte == target_op) {
212 return self;
213 }
214
215 Self::retain_non_matching(&mut self.chunks, target_op, target_len, &target_bytes);
216 self
217 }
218
219 fn retain_non_matching(
221 chunks: &mut Vec<ScriptChunk>,
222 target_op: u8,
223 target_len: usize,
224 target_bytes: &[u8],
225 ) {
226 chunks.retain(|chunk| {
227 if chunk.op_byte != target_op {
229 return true;
230 }
231 let data = chunk.data.as_deref().unwrap_or(&[]);
232 let data_len = data.len();
233
234 if data_len == 0 && chunk.data.is_none() {
235 return target_len != 1;
236 }
237
238 if chunk.op == Op::OpReturn || chunk.op_byte < Op::OpPushData1.to_byte() {
240 if target_len != 1 + data_len {
241 return true;
242 }
243 return target_bytes[1..] != *data;
244 }
245
246 if chunk.op == Op::OpPushData1 {
247 if target_len != 2 + data_len {
248 return true;
249 }
250 if target_bytes[1] != (data_len & 0xff) as u8 {
251 return true;
252 }
253 return target_bytes[2..] != *data;
254 }
255
256 if chunk.op == Op::OpPushData2 {
257 if target_len != 3 + data_len {
258 return true;
259 }
260 if target_bytes[1] != (data_len & 0xff) as u8 {
261 return true;
262 }
263 if target_bytes[2] != ((data_len >> 8) & 0xff) as u8 {
264 return true;
265 }
266 return target_bytes[3..] != *data;
267 }
268
269 if chunk.op == Op::OpPushData4 {
270 if target_len != 5 + data_len {
271 return true;
272 }
273 let size = data_len as u32;
274 if target_bytes[1] != (size & 0xff) as u8 {
275 return true;
276 }
277 if target_bytes[2] != ((size >> 8) & 0xff) as u8 {
278 return true;
279 }
280 if target_bytes[3] != ((size >> 16) & 0xff) as u8 {
281 return true;
282 }
283 if target_bytes[4] != ((size >> 24) & 0xff) as u8 {
284 return true;
285 }
286 return target_bytes[5..] != *data;
287 }
288
289 true
290 });
291 }
292
293 pub fn is_push_only(&self) -> bool {
297 for chunk in &self.chunks {
298 if chunk.op_byte > Op::Op16.to_byte() {
299 return false;
300 }
301 }
302 true
303 }
304
305 fn parse_chunks(bytes: &[u8]) -> Vec<ScriptChunk> {
310 let mut chunks = Vec::new();
311 let length = bytes.len();
312 let mut pos = 0;
313 let mut in_conditional_block: i32 = 0;
314
315 while pos < length {
316 let op_byte = bytes[pos];
317 pos += 1;
318
319 if op_byte == Op::OpReturn.to_byte() && in_conditional_block == 0 {
321 let remaining = bytes[pos..].to_vec();
322 chunks.push(ScriptChunk::new_raw(
323 op_byte,
324 if remaining.is_empty() {
325 None
326 } else {
327 Some(remaining)
328 },
329 ));
330 break;
331 }
332
333 if op_byte == Op::OpIf.to_byte()
335 || op_byte == Op::OpNotIf.to_byte()
336 || op_byte == Op::OpVerIf.to_byte()
337 || op_byte == Op::OpVerNotIf.to_byte()
338 {
339 in_conditional_block += 1;
340 } else if op_byte == Op::OpEndIf.to_byte() {
341 in_conditional_block -= 1;
342 }
343
344 if op_byte > 0 && op_byte < Op::OpPushData1.to_byte() {
346 let push_len = op_byte as usize;
347 let end = (pos + push_len).min(length);
348 let data = bytes[pos..end].to_vec();
349 chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
350 pos = end;
351 } else if op_byte == Op::OpPushData1.to_byte() {
352 let push_len = if pos < length { bytes[pos] as usize } else { 0 };
353 pos += 1;
354 let end = (pos + push_len).min(length);
355 let data = bytes[pos..end].to_vec();
356 chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
357 pos = end;
358 } else if op_byte == Op::OpPushData2.to_byte() {
359 let b0 = if pos < length { bytes[pos] as usize } else { 0 };
360 let b1 = if pos + 1 < length {
361 bytes[pos + 1] as usize
362 } else {
363 0
364 };
365 let push_len = b0 | (b1 << 8);
366 pos = (pos + 2).min(length);
367 let end = (pos + push_len).min(length);
368 let data = bytes[pos..end].to_vec();
369 chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
370 pos = end;
371 } else if op_byte == Op::OpPushData4.to_byte() {
372 let b0 = if pos < length { bytes[pos] as usize } else { 0 };
373 let b1 = if pos + 1 < length {
374 bytes[pos + 1] as usize
375 } else {
376 0
377 };
378 let b2 = if pos + 2 < length {
379 bytes[pos + 2] as usize
380 } else {
381 0
382 };
383 let b3 = if pos + 3 < length {
384 bytes[pos + 3] as usize
385 } else {
386 0
387 };
388 let push_len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
389 pos = (pos + 4).min(length);
390 let end = (pos + push_len).min(length);
391 let data = bytes[pos..end].to_vec();
392 chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
393 pos = end;
394 } else {
395 chunks.push(ScriptChunk::new_raw(op_byte, None));
397 }
398 }
399
400 chunks
401 }
402}
403
404impl Default for Script {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
413 if !hex.len().is_multiple_of(2) {
414 return Err("odd hex length".to_string());
415 }
416 let mut bytes = Vec::with_capacity(hex.len() / 2);
417 for i in (0..hex.len()).step_by(2) {
418 let byte = u8::from_str_radix(&hex[i..i + 2], 16)
419 .map_err(|_| format!("invalid hex at position {}", i))?;
420 bytes.push(byte);
421 }
422 Ok(bytes)
423}
424
425#[cfg(test)]
430mod tests {
431 use super::*;
432
433 fn bytes_to_hex(bytes: &[u8]) -> String {
434 bytes.iter().map(|b| format!("{:02x}", b)).collect()
435 }
436
437 #[test]
438 fn test_binary_roundtrip_empty() {
439 let script = Script::from_binary(&[]);
440 assert!(script.is_empty());
441 assert_eq!(script.to_binary(), Vec::<u8>::new());
442 }
443
444 #[test]
445 fn test_binary_roundtrip_p2pkh() {
446 let pubkey_hash = [0xab; 20];
448 let mut script_bytes = vec![0x76, 0xa9, 0x14]; script_bytes.extend_from_slice(&pubkey_hash);
450 script_bytes.push(0x88); script_bytes.push(0xac); let script = Script::from_binary(&script_bytes);
454 let rt = script.to_binary();
455 assert_eq!(rt, script_bytes, "binary round-trip failed for P2PKH");
456
457 assert_eq!(script.len(), 5);
459 }
460
461 #[test]
462 fn test_binary_roundtrip_pushdata1() {
463 let mut script_bytes = vec![0x4c, 100]; script_bytes.extend_from_slice(&[0xcc; 100]);
466 let script = Script::from_binary(&script_bytes);
467 assert_eq!(script.to_binary(), script_bytes);
468 }
469
470 #[test]
471 fn test_binary_roundtrip_pushdata2() {
472 let mut script_bytes = vec![0x4d, 0x2c, 0x01]; script_bytes.extend_from_slice(&[0xdd; 300]);
475 let script = Script::from_binary(&script_bytes);
476 assert_eq!(script.to_binary(), script_bytes);
477 }
478
479 #[test]
480 fn test_hex_roundtrip() {
481 let hex = "76a914abababababababababababababababababababab88ac";
482 let script = Script::from_hex(hex).unwrap();
483 assert_eq!(script.to_hex(), hex);
484 }
485
486 #[test]
487 fn test_from_hex_empty() {
488 let script = Script::from_hex("").unwrap();
489 assert!(script.is_empty());
490 }
491
492 #[test]
493 fn test_from_hex_odd_length() {
494 let result = Script::from_hex("abc");
495 assert!(result.is_err());
496 }
497
498 #[test]
499 fn test_asm_roundtrip_p2pkh() {
500 let asm =
501 "OP_DUP OP_HASH160 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa OP_EQUALVERIFY OP_CHECKSIG";
502 let script = Script::from_asm(asm);
503 let result_asm = script.to_asm();
504 assert_eq!(result_asm, asm);
505 }
506
507 #[test]
508 fn test_asm_zero_and_negative_one() {
509 let asm = "0 -1 OP_ADD";
510 let script = Script::from_asm(asm);
511 assert_eq!(script.to_asm(), "0 -1 OP_ADD");
513 }
514
515 #[test]
516 fn test_op_return_outside_conditional() {
517 let mut script_bytes = vec![0x6a]; script_bytes.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
521
522 let script = Script::from_binary(&script_bytes);
523 assert_eq!(script.len(), 1, "OP_RETURN + data should be one chunk");
524 assert_eq!(
525 script.chunks()[0].data.as_ref().unwrap(),
526 &[0x01, 0x02, 0x03, 0x04]
527 );
528
529 assert_eq!(script.to_binary(), script_bytes);
531 }
532
533 #[test]
534 fn test_op_return_inside_conditional() {
535 let script_bytes = vec![
538 0x63, 0x6a, 0x68, ];
542
543 let script = Script::from_binary(&script_bytes);
544 assert_eq!(
545 script.len(),
546 3,
547 "OP_RETURN inside conditional should be a standalone opcode"
548 );
549 assert!(
550 script.chunks()[1].data.is_none(),
551 "OP_RETURN inside conditional should have no data"
552 );
553
554 assert_eq!(script.to_binary(), script_bytes);
555 }
556
557 #[test]
558 fn test_op_return_no_data() {
559 let script_bytes = vec![0x6a];
561 let script = Script::from_binary(&script_bytes);
562 assert_eq!(script.len(), 1);
563 assert!(script.chunks()[0].data.is_none());
564 assert_eq!(script.to_binary(), script_bytes);
565 }
566
567 #[test]
568 fn test_find_and_delete_simple() {
569 let script = Script::from_binary(&[0x51, 0x52, 0x53, 0x52]);
571 let target = Script::from_binary(&[0x52]);
573 let result = script.find_and_delete(&target);
574 assert_eq!(result.to_binary(), vec![0x51, 0x53]);
575 }
576
577 #[test]
578 fn test_find_and_delete_data_push() {
579 let script_bytes = vec![0x03, 0xaa, 0xbb, 0xcc, 0x76];
581 let script = Script::from_binary(&script_bytes);
582
583 let target = Script::from_binary(&[0x03, 0xaa, 0xbb, 0xcc]);
585 let result = script.find_and_delete(&target);
586 assert_eq!(result.to_binary(), vec![0x76]); }
588
589 #[test]
590 fn test_find_and_delete_empty_target() {
591 let script = Script::from_binary(&[0x76, 0x76]);
592 let target = Script::new();
593 let result = script.find_and_delete(&target);
594 assert_eq!(result.to_binary(), vec![0x76, 0x76]);
595 }
596
597 #[test]
598 fn test_is_push_only() {
599 let push_script = Script::from_binary(&[0x51, 0x03, 0xaa, 0xbb, 0xcc, 0x60]);
601 assert!(push_script.is_push_only());
602
603 let non_push = Script::from_binary(&[0x76]);
605 assert!(!non_push.is_push_only());
606 }
607
608 #[test]
609 fn test_from_chunks() {
610 let chunks = vec![
611 ScriptChunk::new_opcode(Op::OpDup),
612 ScriptChunk::new_opcode(Op::OpCheckSig),
613 ];
614 let script = Script::from_chunks(chunks);
615 assert_eq!(script.len(), 2);
616 assert_eq!(script.to_binary(), vec![0x76, 0xac]);
617 }
618
619 #[test]
620 fn test_complex_script_roundtrip() {
621 let hex = "5121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe52ae";
623 let script = Script::from_hex(hex).unwrap();
624 assert_eq!(script.to_hex(), hex);
625 }
626
627 #[test]
628 fn test_nested_conditional_op_return() {
629 let script_bytes = vec![
632 0x63, 0x63, 0x6a, 0x68, 0x68, ];
638 let script = Script::from_binary(&script_bytes);
639 assert_eq!(script.len(), 5);
640 assert!(script.chunks()[2].data.is_none());
641 assert_eq!(script.to_binary(), script_bytes);
642 }
643}