11//! Longest-prefix routing dispatches repository traffic without encoding ecosystem paths here.
22
3+ use std:: collections:: BTreeSet ;
4+
35mod acl;
46mod analytics;
57mod discover;
@@ -22,6 +24,7 @@ mod usage;
2224use axum:: http:: { HeaderMap , StatusCode , header} ;
2325use axum:: response:: { IntoResponse , Response } ;
2426use mediatype:: { MediaType , names} ;
27+ use peryx_driver:: http_services:: VersionPrecondition ;
2528use peryx_driver:: state:: { AppState , Index } ;
2629use peryx_identity:: { Action , Denial } ;
2730
@@ -77,6 +80,96 @@ fn is_json(headers: &HeaderMap) -> bool {
7780 . is_some_and ( |media_type| media_type. ty == names:: APPLICATION && media_type. subty == names:: JSON )
7881}
7982
83+ #[ derive( Clone , Copy ) ]
84+ enum IfMatchError {
85+ Missing ,
86+ Malformed ,
87+ }
88+
89+ fn if_match ( headers : & HeaderMap ) -> Result < VersionPrecondition , IfMatchError > {
90+ let fields = headers. get_all ( header:: IF_MATCH ) ;
91+ let mut values = fields. iter ( ) ;
92+ let first = values. next ( ) . ok_or ( IfMatchError :: Missing ) ?;
93+ let mut wildcard_count = 0 ;
94+ let mut saw_tag = false ;
95+ let mut versions = BTreeSet :: new ( ) ;
96+ for value in std:: iter:: once ( first) . chain ( values) {
97+ parse_if_match_field ( value. as_bytes ( ) , & mut wildcard_count, & mut saw_tag, & mut versions) ?;
98+ }
99+ if wildcard_count > 1 || wildcard_count == 1 && saw_tag {
100+ return Err ( IfMatchError :: Malformed ) ;
101+ }
102+ Ok ( if wildcard_count == 1 {
103+ VersionPrecondition :: Exists
104+ } else {
105+ VersionPrecondition :: Versions ( versions)
106+ } )
107+ }
108+
109+ // Commas are valid inside opaque tags: https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.3
110+ fn parse_if_match_field (
111+ field : & [ u8 ] ,
112+ wildcard_count : & mut usize ,
113+ saw_tag : & mut bool ,
114+ versions : & mut BTreeSet < u64 > ,
115+ ) -> Result < ( ) , IfMatchError > {
116+ let mut index = 0 ;
117+ while index < field. len ( ) {
118+ while field. get ( index) . is_some_and ( |byte| matches ! ( byte, b' ' | b'\t' ) ) {
119+ index += 1 ;
120+ }
121+ if index == field. len ( ) {
122+ break ;
123+ }
124+ if field[ index] == b',' {
125+ index += 1 ;
126+ continue ;
127+ }
128+ if field[ index] == b'*' {
129+ * wildcard_count += 1 ;
130+ } else {
131+ let weak = field[ index..] . starts_with ( b"W/" ) ;
132+ if weak {
133+ index += 2 ;
134+ }
135+ if field. get ( index) != Some ( & b'"' ) {
136+ return Err ( IfMatchError :: Malformed ) ;
137+ }
138+ index += 1 ;
139+ let start = index;
140+ while field. get ( index) . is_some_and ( |byte| * byte != b'"' ) {
141+ if !matches ! ( field[ index] , b'!' | b'#' ..=b'~' | b'\x80' ..=b'\xff' ) {
142+ return Err ( IfMatchError :: Malformed ) ;
143+ }
144+ index += 1 ;
145+ }
146+ if field. get ( index) != Some ( & b'"' ) {
147+ return Err ( IfMatchError :: Malformed ) ;
148+ }
149+ * saw_tag = true ;
150+ if !weak
151+ && let Ok ( value) = std:: str:: from_utf8 ( & field[ start..index] )
152+ && let Ok ( version) = value. parse :: < u64 > ( )
153+ && version. to_string ( ) == value
154+ {
155+ versions. insert ( version) ;
156+ }
157+ }
158+ index += 1 ;
159+ while field. get ( index) . is_some_and ( |byte| matches ! ( byte, b' ' | b'\t' ) ) {
160+ index += 1 ;
161+ }
162+ if index == field. len ( ) {
163+ break ;
164+ }
165+ if field[ index] != b',' {
166+ return Err ( IfMatchError :: Malformed ) ;
167+ }
168+ index += 1 ;
169+ }
170+ Ok ( ( ) )
171+ }
172+
80173/// HTTP handlers distinguish an authenticated denial from a missing or invalid credential.
81174enum EcosystemCredentialDenied {
82175 Forbidden ,
0 commit comments