Skip to content

Commit cbef239

Browse files
committed
expression: add method to validate and construct a threshold
1 parent 7224a64 commit cbef239

File tree

4 files changed

+110
-1
lines changed

4 files changed

+110
-1
lines changed

src/expression/error.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! Expression-related errors
4+
5+
use core::fmt;
6+
7+
use crate::ThresholdError;
8+
9+
/// Error parsing a threshold expression.
10+
#[derive(Clone, Debug, PartialEq, Eq)]
11+
pub enum ParseThresholdError {
12+
/// Expression had no children, not even a threshold value.
13+
NoChildren,
14+
/// The threshold value appeared to be a sub-expression rather than a number.
15+
KNotTerminal,
16+
/// Failed to parse the threshold value.
17+
// FIXME this should be a more specific type. Will be handled in a later PR
18+
// that rewrites the expression parsing logic.
19+
ParseK(String),
20+
/// Threshold parameters were invalid.
21+
Threshold(ThresholdError),
22+
}
23+
24+
impl fmt::Display for ParseThresholdError {
25+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26+
use ParseThresholdError::*;
27+
28+
match *self {
29+
NoChildren => f.write_str("expected threshold, found terminal"),
30+
KNotTerminal => f.write_str("expected positive integer, found expression"),
31+
ParseK(ref x) => write!(f, "failed to parse threshold value {}", x),
32+
Threshold(ref e) => e.fmt(f),
33+
}
34+
}
35+
}
36+
37+
#[cfg(feature = "std")]
38+
impl std::error::Error for ParseThresholdError {
39+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40+
use ParseThresholdError::*;
41+
42+
match *self {
43+
NoChildren => None,
44+
KNotTerminal => None,
45+
ParseK(..) => None,
46+
Threshold(ref e) => Some(e),
47+
}
48+
}
49+
}

src/expression/mod.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
//! # Function-like Expression Language
44
//!
5+
6+
mod error;
7+
58
use core::fmt;
69
use core::str::FromStr;
710

11+
pub use self::error::ParseThresholdError;
812
use crate::prelude::*;
9-
use crate::{errstr, Error, MAX_RECURSION_DEPTH};
13+
use crate::{errstr, Error, Threshold, MAX_RECURSION_DEPTH};
1014

1115
/// Allowed characters are descriptor strings.
1216
pub const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
@@ -185,6 +189,35 @@ impl<'a> Tree<'a> {
185189
Err(errstr(rem))
186190
}
187191
}
192+
193+
/// Parses an expression tree as a threshold (a term with at least one child,
194+
/// the first of which is a positive integer k).
195+
///
196+
/// This sanity-checks that the threshold is well-formed (begins with a valid
197+
/// threshold value, etc.) but does not parse the children of the threshold.
198+
/// Instead it returns a threshold holding the empty type `()`, which is
199+
/// constructed without any allocations, and expects the caller to convert
200+
/// this to the "real" threshold type by calling [`Threshold::translate`].
201+
///
202+
/// (An alternate API which does the conversion inline turned out to be
203+
/// too messy; it needs to take a closure, have multiple generic parameters,
204+
/// and be able to return multiple error types.)
205+
pub fn to_null_threshold<const MAX: usize>(
206+
&self,
207+
) -> Result<Threshold<(), MAX>, ParseThresholdError> {
208+
// First, special case "no arguments" so we can index the first argument without panics.
209+
if self.args.is_empty() {
210+
return Err(ParseThresholdError::NoChildren);
211+
}
212+
213+
if !self.args[0].args.is_empty() {
214+
return Err(ParseThresholdError::KNotTerminal);
215+
}
216+
217+
let k = parse_num(self.args[0].name)
218+
.map_err(|e| ParseThresholdError::ParseK(e.to_string()))? as usize;
219+
Threshold::new(k, vec![(); self.args.len() - 1]).map_err(ParseThresholdError::Threshold)
220+
}
188221
}
189222

190223
/// Filter out non-ASCII because we byte-index strings all over the

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ use bitcoin::{script, Opcode};
143143

144144
pub use crate::blanket_traits::FromStrKey;
145145
pub use crate::descriptor::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey};
146+
pub use crate::expression::ParseThresholdError;
146147
pub use crate::interpreter::Interpreter;
147148
pub use crate::miniscript::analyzable::{AnalysisError, ExtParams};
148149
pub use crate::miniscript::context::{BareCtx, Legacy, ScriptContext, Segwitv0, SigType, Tap};
@@ -495,6 +496,8 @@ pub enum Error {
495496
RelativeLockTime(RelLockTimeError),
496497
/// Invalid threshold.
497498
Threshold(ThresholdError),
499+
/// Invalid threshold.
500+
ParseThreshold(ParseThresholdError),
498501
}
499502

500503
// https://github.com/sipa/miniscript/pull/5 for discussion on this number
@@ -553,6 +556,7 @@ impl fmt::Display for Error {
553556
Error::AbsoluteLockTime(ref e) => e.fmt(f),
554557
Error::RelativeLockTime(ref e) => e.fmt(f),
555558
Error::Threshold(ref e) => e.fmt(f),
559+
Error::ParseThreshold(ref e) => e.fmt(f),
556560
}
557561
}
558562
}
@@ -600,6 +604,7 @@ impl error::Error for Error {
600604
AbsoluteLockTime(e) => Some(e),
601605
RelativeLockTime(e) => Some(e),
602606
Threshold(e) => Some(e),
607+
ParseThreshold(e) => Some(e),
603608
}
604609
}
605610
}

src/primitives/threshold.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ impl<T, const MAX: usize> Threshold<T, MAX> {
142142
.map(|inner| Threshold { k, inner })
143143
}
144144

145+
/// Like [`Self::translate_ref`] but passes indices to the closure rather than internal data.
146+
///
147+
/// This is useful in situations where the data to be translated exists outside of the
148+
/// threshold itself, and the threshold data is irrelevant. In particular it is commonly
149+
/// paired with [`crate::expression::Tree::to_null_threshold`].
150+
///
151+
/// If the data to be translated comes from a post-order iterator, you may instead want
152+
/// [`Self::map_from_post_order_iter`].
153+
pub fn translate_by_index<U, F, FuncError>(
154+
&self,
155+
translatefn: F,
156+
) -> Result<Threshold<U, MAX>, FuncError>
157+
where
158+
F: FnMut(usize) -> Result<U, FuncError>,
159+
{
160+
let k = self.k;
161+
(0..self.inner.len())
162+
.map(translatefn)
163+
.collect::<Result<Vec<_>, _>>()
164+
.map(|inner| Threshold { k, inner })
165+
}
166+
145167
/// Construct a threshold from an existing threshold which has been processed in some way.
146168
///
147169
/// It is a common pattern in this library to transform data structures by

0 commit comments

Comments
 (0)