Skip to content

Commit 00aa1ad

Browse files
committed
refactor!: Result<Option<Self>, Error> -> Result<Self, Error>
BREAKING CHANGE: many of the `new(src: &str)` methods now return `Result<Self, Error>` instead of `Result<Option<Self>, Error>`.
1 parent 1afadf1 commit 00aa1ad

File tree

20 files changed

+359
-224
lines changed

20 files changed

+359
-224
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"nixEnvSelector.nixFile": "${workspaceRoot}/flake.nix",
44
"nix.enableLanguageServer": true,
55
"nix.serverPath": "nil",
6-
"cSpell.words": ["opencontainers", "repr"]
6+
"cSpell.words": ["ebnf", "opencontainers", "repr"]
77
}

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ A docker/OCI image reference parser.
66
[![docs.rs](https://img.shields.io/docsrs/container_image_dist_ref)](https://docs.rs/container_image_dist_ref/latest/container_image_dist_ref/)
77

88
This library is extensively tested against the authoritative image reference implementation, https://github.com/distribution/reference.
9-
`distribution/reference` uses the following [EBNF](https://www.w3.org/TR/xml11/#sec-notation) grammar:
9+
10+
Image references follow this [EBNF](https://www.w3.org/TR/xml11/#sec-notation) grammar:
1011

1112
<!-- {{{sh cat ./grammars/reference.ebnf }}}{{{out skip=2 -->
1213

@@ -37,6 +38,15 @@ identifier ::= [a-f0-9]{64}
3738

3839
(This is translated from [https://github.com/distribution/reference/blob/main/reference.go](https://github.com/distribution/reference/blob/main/reference.go#L4-L26))
3940

41+
To avoid worst-case performance, image references are restricted further:
42+
43+
| part | maximum length |
44+
| ----------- | -------------: |
45+
| `name` | 255 |
46+
| `tag` | 127 |
47+
| `digest` | 1024 |
48+
| `algorithm` | 255 |
49+
4050
## Motivation
4151

4252
<!-- TODO: rewrite -->

examples/parse_canonical.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ fn main() {
1414
Ok(ref_str) => {
1515
let input = escape(&input);
1616
let name = escape(ref_str.name().to_str());
17-
let domain = escape(ref_str.domain_str());
18-
let path = escape(ref_str.path_str());
17+
let domain = escape(ref_str.domain().to_str());
18+
let path = escape(ref_str.path().to_str());
1919
let tag = escape(ref_str.tag().unwrap_or(""));
2020
let digest_algo = escape(ref_str.digest().algorithm().to_str());
2121
let digest_encoded = escape(ref_str.digest().encoded().to_str());

scripts/diff_digest.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use_rule() { grep -E "^$2 " "$1"; }
66
fake_diff_line() {
77
local rule="$1"
88
local oci ref
9-
oci="$(use_rule ./grammars/oci_digest.ebnf "$rule")"
109
ref="$(use_rule ./grammars/reference.ebnf "$rule")"
10+
oci="$(use_rule ./grammars/oci_digest.ebnf "$rule")"
1111
if [ "$oci" = "$ref" ]; then
1212
printf " %s\n" "$oci";
1313
else
14-
printf "-"; printf "%s\n+%s\n" "$ref" "$oci";
14+
printf "-%s\n" "$ref";
15+
printf "+%s\n" "$oci";
1516
fi
1617
}
1718

src/ambiguous/domain_or_tagged_ref.rs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -86,34 +86,27 @@ fn map_err(e: err::Error<u8>) -> err::Error<u16> {
8686

8787
impl<'src> DomainOrRefSpan<'src> {
8888
pub(crate) fn new(src: &'src str) -> Result<Self, Error> {
89-
let left = HostOrPathSpan::new(src, HostOrPathKind::Any)?
90-
.ok_or(Error::at(0, err::Kind::HostOrPathMissing))?;
91-
let right_src = &src[left.len()..];
89+
let left = HostOrPathSpan::new(src, HostOrPathKind::Any)?;
90+
let mut len = left.short_len().widen().upcast();
91+
let right_src = &src[len as usize..];
9292
let right = match right_src.bytes().next() {
9393
Some(b':') => {
94-
let len = left.short_len().widen().upcast() + 1;
95-
// +1 for the leading ':'
96-
let right = PortOrTagSpan::new(&right_src[1..], Port)
94+
len += 1; // +1 for the leading ':'
95+
PortOrTagSpan::new(&right_src[1..], Port)
9796
.map_err(map_err)
98-
.map_err(|e| e + len)?;
99-
Ok(Some(
100-
right.ok_or(Error::at(len, err::Kind::PortOrTagMissing))?,
101-
))
97+
.map(Some)
10298
}
10399
Some(b'/') | Some(b'@') | None => Ok(None),
104-
Some(_) => Error::at(
105-
left.short_len().upcast().into(),
106-
err::Kind::PortOrTagInvalidChar,
107-
)
108-
.into(),
109-
}?;
100+
Some(_) => Error::at(0, err::Kind::PortOrTagInvalidChar).into(),
101+
}
102+
.map_err(|e| e + len)?;
110103

111-
let len = left.len() + right.map(|r| r.len() + 1).unwrap_or(0); // +1 for the leading ':'
112-
let rest = &src[len..];
104+
len += right.map(|r| r.short_len().widen().upcast()).unwrap_or(0);
105+
let rest = &src[len as usize..];
113106
match rest.bytes().next() {
114107
Some(b'@') | None => {
115108
// since the next section must be a digest, the right side must be a tag
116-
let path = PathSpan::from_ambiguous(left).map_err(map_err)?;
109+
let path = PathSpan::try_from(left).map_err(map_err)?;
117110
let tag = if let Some(tag) = right {
118111
Some(
119112
tag.try_into()
@@ -136,9 +129,7 @@ impl<'src> DomainOrRefSpan<'src> {
136129
match left.kind() {
137130
Path => {
138131
// need to extend the path
139-
let path = PathSpan::from_ambiguous(left)?
140-
.extend(rest)
141-
.map_err(map_err)?;
132+
let path = PathSpan::try_from(left)?.extend(rest).map_err(map_err)?;
142133

143134
let tag = if let Some(t) = right {
144135
Some(

src/ambiguous/host_or_path.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ impl<'src> HostOrPathSpan<'src> {
251251
self.1
252252
}
253253

254-
/// can return 0-length spans if at EOF or the first character is a `/` or `@`
255-
pub(crate) fn new(src: &'src str, kind: Kind) -> Result<Option<Self>, Error> {
254+
/// can return None if at EOF or the first character is a `/` or `@`
255+
pub(crate) fn new(src: &'src str, kind: Kind) -> Result<Self, Error> {
256256
let mut state = State {
257257
len: 0,
258258
scan: kind.into(), // <- scan's setters will enforce the kind's constraint(s)
@@ -264,11 +264,13 @@ impl<'src> HostOrPathSpan<'src> {
264264
#[cfg(test)]
265265
let _c = c.map(|c| c as char);
266266
match c {
267-
None => return Ok(None),
267+
None => return Error::at(0, err::Kind::HostOrPathMissing).into(),
268268
Some(b'[') => {
269269
return match kind {
270-
Kind::IpV6 | Kind::Any => Ok(ipv6::Ipv6Span::new(src)?
271-
.map(|span| Self(span.into_length().unwrap(), Kind::IpV6, 0))),
270+
Kind::IpV6 | Kind::Any => {
271+
let span = ipv6::Ipv6Span::new(src)?;
272+
Ok(Self(span.into_length().unwrap(), Kind::IpV6, 0))
273+
}
272274
_ => Err(Error::at(0, InvalidChar)),
273275
}
274276
}
@@ -300,8 +302,9 @@ impl<'src> HostOrPathSpan<'src> {
300302
state.len,
301303
src.len()
302304
);
303-
Ok(ShortLength::new(state.len)
304-
.map(|length| Self(length, state.scan.into(), state.deciding_char.unwrap_or(0))))
305+
ShortLength::new(state.len)
306+
.ok_or(Error::at(0, err::Kind::HostOrPathMissing))
307+
.map(|length| Self(length, state.scan.into(), state.deciding_char.unwrap_or(0)))
305308
}
306309
pub(crate) fn narrow(self, target_kind: Kind) -> Result<Self, Error> {
307310
use Kind::*;
@@ -337,7 +340,6 @@ mod tests {
337340
);
338341
})
339342
.unwrap()
340-
.unwrap()
341343
}
342344
fn should_parse_as(src: &str, expected: &str, kind: Kind) {
343345
let host_or_path = should_parse(src);
@@ -361,7 +363,6 @@ mod tests {
361363

362364
fn should_fail_with(src: &str, err_kind: err::Kind, bad_char_index: u8) {
363365
let err = super::HostOrPathSpan::new(src, Kind::Any)
364-
.map(|e| e.unwrap())
365366
.map(|e| {
366367
assert!(
367368
false,

src/ambiguous/port_or_tag.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,10 @@ impl<'src> PortOrTagSpan<'src> {
9797
first_tag_char: self.first_tag_char,
9898
})
9999
}
100-
/// can match an empty span if the first character in src is a `/` or `@`
101-
pub(crate) fn new(src: &str, kind: Kind) -> Result<Option<Self>, Error> {
100+
/// Parse a port or tag from the start of a string.
101+
/// Does NOT include the leading colon.
102+
/// Can match an empty span if the first character in src is a `/` or `@`
103+
pub(crate) fn new(src: &str, kind: Kind) -> Result<Self, Error> {
102104
let mut bytes = src.bytes();
103105

104106
// the first character after the colon must be alphanumeric or an underscore
@@ -110,7 +112,7 @@ impl<'src> PortOrTagSpan<'src> {
110112
Some(b'a'..=b'z') | Some(b'A'..=b'Z') | Some(b'_') => kind
111113
.update(Kind::Tag) // only tags can have non-numeric characters
112114
.map_err(|_| err::Kind::PortInvalidChar),
113-
None | Some(b'/') | Some(b'@') => return Ok(None),
115+
None | Some(b'/') | Some(b'@') => Err(err::Kind::PortOrTagMissing),
114116
_ => Err(err::Kind::PortOrTagInvalidChar),
115117
}
116118
.map_err(|err_kind| Error::at(0, err_kind))?;
@@ -145,11 +147,11 @@ impl<'src> PortOrTagSpan<'src> {
145147
true
146148
});
147149

148-
Ok(Some(Self {
150+
Ok(Self {
149151
length: ShortLength::from_nonzero(state.len),
150152
kind: state.kind,
151153
first_tag_char: state.first_tag_char,
152-
}))
154+
})
153155
}
154156
}
155157

@@ -160,13 +162,10 @@ mod tests {
160162
fn should_parse_as(src: &str, kind: Kind) {
161163
let tag = PortOrTagSpan::new(src, kind);
162164
match tag {
163-
Ok(Some(tag)) => {
165+
Ok(tag) => {
164166
assert_eq!(tag.span().span_of(src), src);
165167
assert_eq!(tag.kind, kind);
166168
}
167-
Ok(None) => {
168-
assert!(src.is_empty() || src.starts_with('/') || src.starts_with('@'));
169-
}
170169
Err(e) => panic!("failed to parse tag {src:?}: {:?}", e),
171170
}
172171
}

src/digest/algorithm.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//! # Algorithm
1+
//! # Digest Algorithm
2+
//!
23
//! There are two specifications for a digest algorithm string:
34
//! - the [OCI Image Spec](https://github.com/opencontainers/image-spec/blob/v1.0.2/descriptor.md#digests)
45
//! - [github.com/distribution/reference](https://github.com/distribution/reference/blob/v0.5.0/reference.go#L21-L23)
@@ -41,6 +42,7 @@ fn try_add(a: NonZeroU8, b: u8) -> Result<NonZeroU8, Error> {
4142
a.checked_add(b)
4243
.ok_or(Error::at(u8::MAX.into(), err::Kind::AlgorithmTooLong))
4344
}
45+
4446
/// While there's no specification for the max length of an algorithm string,
4547
/// 255 characters is a reasonable upper bound.
4648
pub const MAX_LEN: u8 = u8::MAX;
@@ -80,32 +82,56 @@ impl<'src> AlgorithmSpan<'src> {
8082
}
8183
}
8284

85+
/// The algorithm section of a digest.
86+
/// ```rust
87+
/// use container_image_dist_ref::digest::{
88+
/// algorithm::AlgorithmStr, Compliance, Standard
89+
/// };
90+
/// let (algorithm, compliance) = AlgorithmStr::new("sha256").unwrap();
91+
/// assert_eq!(algorithm.to_str(), "sha256");
92+
/// assert_eq!(compliance, Compliance::Universal);
93+
/// assert_eq!(algorithm.compliance(), Compliance::Universal);
94+
/// assert!(compliance.compliant_with(Standard::Oci));
95+
/// assert!(compliance.compliant_with(Standard::Distribution));
96+
///
97+
/// let (algorithm, _) = AlgorithmStr::new("a+b").unwrap();
98+
/// assert_eq!(algorithm.to_str(), "a+b");
99+
/// assert_eq!(algorithm.parts().collect::<Vec<_>>(), vec!["a", "b"]);
100+
/// ```
83101
pub struct AlgorithmStr<'src>(&'src str);
84102
impl<'src> AlgorithmStr<'src> {
103+
#[allow(missing_docs)]
85104
#[inline]
86105
pub fn to_str(&self) -> &'src str {
87106
self.0
88107
}
108+
#[allow(missing_docs)]
89109
pub fn len(&self) -> usize {
90110
self.to_str().len()
91111
}
112+
#[allow(missing_docs)]
92113
pub fn is_empty(&self) -> bool {
93114
self.to_str().is_empty()
94115
}
95-
pub fn from_prefix(src: &'src str) -> Result<(Self, Compliance), Error> {
116+
/// Parse an algorithm from the start of the string. Parsing may not consume the entire string
117+
/// if it reaches a valid stopping point, i.e. `:`.
118+
pub fn new(src: &'src str) -> Result<(Self, Compliance), Error> {
96119
let (span, compliance) = AlgorithmSpan::new(src)?;
97120
Ok((Self(span.span_of(src)), compliance))
98121
}
122+
/// checks that the entire source string is parsed.
99123
pub fn from_exact_match(src: &'src str) -> Result<(Self, Compliance), Error> {
100124
let (span, compliance) = AlgorithmSpan::from_exact_match(src)?;
101125
Ok((Self(span.span_of(src)), compliance))
102126
}
103127
pub(super) fn from_span(src: &'src str, span: AlgorithmSpan<'src>) -> Self {
104128
Self(span.span_of(src))
105129
}
130+
/// Split the algorithm string into its components separated by `+`, `.`, `_`, or `-`.
106131
pub fn parts(&self) -> impl Iterator<Item = &str> {
107132
self.to_str().split(|c| is_separator(c as u8))
108133
}
134+
/// Whether the algorithm is compliant with the OCI or distribution/reference specifications.
109135
pub fn compliance(&self) -> Compliance {
110136
let mut bytes = self.to_str().bytes();
111137
match bytes.next().unwrap() {

src/digest/encoded.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
use core::num::NonZeroU16;
2222

2323
use super::{algorithm::AlgorithmStr, Compliance};
24+
use crate::err;
2425
use crate::span::{impl_span_methods_on_tuple, Lengthy, LongLength};
25-
26-
pub const MAX_LEN: u16 = 1024; // arbitrary but realistic limit
26+
/// an arbitrary maximum length for the encoded section of a digest.
27+
/// This a realistic limit; hex-encoded sha512 digests are 128 characters long.
28+
pub const MAX_LEN: u16 = 1024;
2729

2830
use crate::err::Kind::{
2931
EncodedInvalidChar, EncodedNonLowerHex, EncodingTooLong, EncodingTooShort,
@@ -35,10 +37,7 @@ type Error = crate::err::Error<u16>;
3537
pub(crate) struct EncodedSpan<'src>(LongLength<'src>);
3638
impl_span_methods_on_tuple!(EncodedSpan, u16, NonZeroU16);
3739
impl<'src> EncodedSpan<'src> {
38-
pub(crate) fn new(
39-
src: &'src str,
40-
compliance: Compliance,
41-
) -> Result<Option<(Self, Compliance)>, Error> {
40+
pub(crate) fn new(src: &'src str, compliance: Compliance) -> Result<(Self, Compliance), Error> {
4241
use Compliance::*;
4342
let mut len = 0;
4443
let mut compliance = compliance;
@@ -65,18 +64,27 @@ impl<'src> EncodedSpan<'src> {
6564

6665
debug_assert!(len as usize == src.len(), "must have consume all src");
6766

68-
Ok(LongLength::new(len).map(|length| (Self(length), compliance)))
67+
LongLength::new(len)
68+
.ok_or(Error::at(0, err::Kind::EncodedMissing))
69+
.map(|length| (Self(length), compliance))
6970
}
7071
}
7172

73+
/// The encoded portion of a digest string. This may not be a hex-encoded value,
74+
/// since the OCI spec allows for base64 encoding.
7275
pub struct EncodedStr<'src>(&'src str);
7376
impl<'src> EncodedStr<'src> {
77+
#[allow(missing_docs)]
7478
pub fn to_str(&self) -> &'src str {
7579
self.0
7680
}
77-
// no implementation of from_prefix(&str) because digests MUST terminate a
78-
// reference
79-
81+
/// Parses a string into an encoded digest value. The string must be a valid
82+
/// with respect to the given standard (Oci, Distribution, or Universal).
83+
/// Parsing always continues until the end of the string or an error.
84+
pub fn new(src: &'src str, compliance: Compliance) -> Result<Self, Error> {
85+
let (span, _compliance) = EncodedSpan::new(src, compliance)?;
86+
Ok(Self::from_span(src, span))
87+
}
8088
pub(crate) fn from_span(src: &'src str, span: EncodedSpan<'src>) -> Self {
8189
Self(span.span_of(src))
8290
}
@@ -160,7 +168,7 @@ mod tests {
160168
#[test]
161169
fn encoded_consumes_all() {
162170
fn consumes_all(src: &str) -> Result<(), Error> {
163-
let (span, _) = EncodedSpan::new(src, Compliance::Oci)?.unwrap();
171+
let (span, _) = EncodedSpan::new(src, Compliance::Oci)?;
164172
assert_eq!(span.len(), src.len());
165173
Ok(())
166174
}

0 commit comments

Comments
 (0)