h3: initial version of extensible priority scheme

This builds on top of the QUIC-level priority support.

This change adds a new method `send_response_with_priority()`to
the HTTP/3 API. This takes a `priority` argument, a string that
contains the Extensible Priority header field value formatted
as Structured Headers e.g. "u=2, i". The contents of `priority`
is used for scheduling when sending response body bytes with
`send_body`.

The Extensible Priority scheme defines the defaults u=3 and i=?0.
Providing an empty value for `priority` will cause the defaults
to be used. The existing method `send_response()` has been updated
to send responses using defaults.

At this time, once a response is initiated it's priority cannot
changed.

No new API method is provided for clients to set request priority.
Clients can provide the `Priority` HTTP header in the `Headers`
argument of the `send_request()` method. This will be passed through
unmodified.

The quiche-client and quiche-server applications have been updated
to test this out. quiche-client can be run with e.g.
`-H "Priority:u=4,i"` to send a priority signal. quiche-server
process the Priority header (or assumes the default if none is
provided). It also supports a special query string syntax that
takes precedence over the header. For example, a client can send
`https://example.com?u=4&i=1` to indicate the priority as `u=1,i`.

Co-authored-by: Alessandro Ghedini <alessandro@ghedini.me>
diff --git a/include/quiche.h b/include/quiche.h
index b63fc2e..0ad39b8 100644
--- a/include/quiche.h
+++ b/include/quiche.h
@@ -480,11 +480,17 @@
                                quiche_h3_header *headers, size_t headers_len,
                                bool fin);
 
-// Sends an HTTP/3 response on the specified stream.
+// Sends an HTTP/3 response on the specified stream with default priority.
 int quiche_h3_send_response(quiche_h3_conn *conn, quiche_conn *quic_conn,
                             uint64_t stream_id, quiche_h3_header *headers,
                             size_t headers_len, bool fin);
 
+// Sends an HTTP/3 response on the specified stream with specified priority.
+int quiche_h3_send_response_with_priority(quiche_h3_conn *conn,
+                            quiche_conn *quic_conn, uint64_t stream_id,
+                            quiche_h3_header *headers, size_t headers_len,
+                            const char *priority, bool fin);
+
 // Sends an HTTP/3 body chunk on the given stream.
 ssize_t quiche_h3_send_body(quiche_h3_conn *conn, quiche_conn *quic_conn,
                             uint64_t stream_id, uint8_t *body, size_t body_len,
diff --git a/src/h3/ffi.rs b/src/h3/ffi.rs
index 50ffb1d..13b8394 100644
--- a/src/h3/ffi.rs
+++ b/src/h3/ffi.rs
@@ -24,10 +24,12 @@
 // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+use std::ffi;
 use std::ptr;
 use std::slice;
 use std::str;
 
+use libc::c_char;
 use libc::c_int;
 use libc::c_void;
 use libc::size_t;
@@ -198,6 +200,28 @@
 }
 
 #[no_mangle]
+pub extern fn quiche_h3_send_response_with_priority(
+    conn: &mut h3::Connection, quic_conn: &mut Connection, stream_id: u64,
+    headers: *const Header, headers_len: size_t, priority: *const c_char,
+    fin: bool,
+) -> c_int {
+    let resp_headers = headers_from_ptr(headers, headers_len);
+    let priority = unsafe { ffi::CStr::from_ptr(priority).to_str().unwrap() };
+
+    match conn.send_response_with_priority(
+        quic_conn,
+        stream_id,
+        &resp_headers,
+        &priority,
+        fin,
+    ) {
+        Ok(_) => 0,
+
+        Err(e) => e.to_c() as c_int,
+    }
+}
+
+#[no_mangle]
 pub extern fn quiche_h3_send_body(
     conn: &mut h3::Connection, quic_conn: &mut Connection, stream_id: u64,
     body: *const u8, body_len: size_t, fin: bool,
diff --git a/src/h3/mod.rs b/src/h3/mod.rs
index cea128f..0697bd8 100644
--- a/src/h3/mod.rs
+++ b/src/h3/mod.rs
@@ -260,6 +260,9 @@
 /// ../struct.Config.html#method.set_application_protos
 pub const APPLICATION_PROTOCOL: &[u8] = b"\x05h3-29\x05h3-28\x05h3-27";
 
+// The offset used when converting HTTP/3 urgency to quiche urgency.
+const PRIORITY_URGENCY_OFFSET: u8 = 124;
+
 /// A specialized [`Result`] type for quiche HTTP/3 operations.
 ///
 /// This type is used throughout quiche's HTTP/3 public API for any operation
@@ -628,7 +631,7 @@
         Ok(stream_id)
     }
 
-    /// Sends an HTTP/3 response on the specified stream.
+    /// Sends an HTTP/3 response on the specified stream with default priority.
     ///
     /// This method sends the provided `headers` without a body. To include a
     /// body, set `fin` as `false` and subsequently call [`send_body()`] with
@@ -645,6 +648,66 @@
         &mut self, conn: &mut super::Connection, stream_id: u64,
         headers: &[Header], fin: bool,
     ) -> Result<()> {
+        let priority = "u=3";
+
+        self.send_response_with_priority(
+            conn, stream_id, headers, priority, fin,
+        )?;
+
+        Ok(())
+    }
+
+    /// Sends an HTTP/3 response on the specified stream with specified
+    /// priority.
+    ///
+    /// The [`StreamBlocked`] error is returned when the underlying QUIC stream
+    /// doesn't have enough capacity for the operation to complete. When this
+    /// happens the application should retry the operation once the stream is
+    /// reported as writable again.
+    ///
+    /// [`StreamBlocked`]: enum.Error.html#variant.StreamBlocked
+    pub fn send_response_with_priority(
+        &mut self, conn: &mut super::Connection, stream_id: u64,
+        headers: &[Header], priority: &str, fin: bool,
+    ) -> Result<()> {
+        if !self.streams.contains_key(&stream_id) {
+            return Err(Error::FrameUnexpected);
+        }
+
+        let mut urgency = 3;
+        let mut incremental = false;
+
+        for param in priority.split(',') {
+            if param.trim() == "i" {
+                incremental = true;
+                continue;
+            }
+
+            if param.trim().starts_with("u=") {
+                // u is an sh-integer (an i64) but it has a constrained range of
+                // 0-7. So detect anything outside that range and clamp it to
+                // the lowest urgency in order to avoid it interfering with
+                // valid items.
+                //
+                // TODO: this also detects when u is not an sh-integer and
+                // clamps it in the same way. A real structured header parser
+                // would actually fail to parse.
+                let mut u =
+                    i64::from_str_radix(param.rsplit('=').next().unwrap(), 10)
+                        .unwrap_or(7);
+
+                if u < 0 || u > 7 {
+                    u = 7;
+                }
+
+                // The HTTP/3 urgency needs to be shifted into the quiche
+                // urgency range.
+                urgency = (u as u8).saturating_add(PRIORITY_URGENCY_OFFSET);
+            }
+        }
+
+        conn.stream_priority(stream_id, urgency, incremental)?;
+
         self.send_headers(conn, stream_id, headers, fin)?;
 
         Ok(())
@@ -751,7 +814,7 @@
             None => {
                 return Err(Error::FrameUnexpected);
             },
-        }
+        };
 
         let overhead = octets::varint_len(frame::DATA_FRAME_TYPE_ID) +
             octets::varint_len(body.len() as u64);
@@ -899,6 +962,23 @@
         let mut d = [0; 8];
         let mut b = octets::OctetsMut::with_slice(&mut d);
 
+        match ty {
+            // Control and QPACK streams are the most important to schedule.
+            stream::HTTP3_CONTROL_STREAM_TYPE_ID |
+            stream::QPACK_ENCODER_STREAM_TYPE_ID |
+            stream::QPACK_DECODER_STREAM_TYPE_ID => {
+                conn.stream_priority(stream_id, 0, true)?;
+            },
+
+            // TODO: Server push
+            stream::HTTP3_PUSH_STREAM_TYPE_ID => (),
+
+            // Anything else is a GREASE stream, so make it the least important.
+            _ => {
+                conn.stream_priority(stream_id, 255, true)?;
+            },
+        }
+
         conn.stream_send(stream_id, b.put_varint(ty)?, false)?;
 
         // To avoid skipping stream IDs, we only calculate the next available
diff --git a/tools/apps/src/lib.rs b/tools/apps/src/lib.rs
index 7eae3fc..3e637ee 100644
--- a/tools/apps/src/lib.rs
+++ b/tools/apps/src/lib.rs
@@ -677,12 +677,13 @@
     /// Builds an HTTP/3 response given a request.
     fn build_h3_response(
         root: &str, index: &str, request: &[quiche::h3::Header],
-    ) -> (Vec<quiche::h3::Header>, Vec<u8>) {
+    ) -> (Vec<quiche::h3::Header>, Vec<u8>, String) {
         let mut file_path = path::PathBuf::from(root);
         let mut scheme = "";
         let mut host = "";
         let mut path = "";
         let mut method = "";
+        let mut priority = "";
 
         // Parse some of the request headers.
         for hdr in request {
@@ -703,6 +704,10 @@
                     method = hdr.value();
                 },
 
+                "priority" => {
+                    priority = hdr.value();
+                },
+
                 _ => (),
             }
         }
@@ -713,7 +718,7 @@
                 quiche::h3::Header::new("server", "quiche"),
             ];
 
-            return (headers, b"Invalid scheme".to_vec());
+            return (headers, b"Invalid scheme".to_vec(), priority.to_string());
         }
 
         let url = format!("{}://{}{}", scheme, host, path);
@@ -722,6 +727,23 @@
         let pathbuf = path::PathBuf::from(url.path());
         let pathbuf = autoindex(pathbuf, index);
 
+        // Priority query string takes precedence over the header.
+        // So replace the header with one built here.
+        let mut query_priority = "".to_string();
+        for param in url.query_pairs() {
+            if param.0 == "u" {
+                query_priority.push_str(&format!("{}={},", param.0, param.1));
+            }
+
+            if param.0 == "i" && param.1 == "1" {
+                query_priority.push_str("i,");
+            }
+        }
+
+        if !query_priority.is_empty() {
+            priority = &query_priority;
+        }
+
         let (status, body) = match method {
             "GET" => {
                 for c in pathbuf.components() {
@@ -744,9 +766,10 @@
             quiche::h3::Header::new(":status", &status.to_string()),
             quiche::h3::Header::new("server", "quiche"),
             quiche::h3::Header::new("content-length", &body.len().to_string()),
+            quiche::h3::Header::new("priority", &priority),
         ];
 
-        (headers, body)
+        (headers, body, priority.to_string())
     }
 }
 
@@ -918,13 +941,12 @@
                     conn.stream_shutdown(stream_id, quiche::Shutdown::Read, 0)
                         .unwrap();
 
-                    let (headers, body) =
+                    let (headers, body, priority) =
                         Http3Conn::build_h3_response(root, index, &list);
 
-                    match self
-                        .h3_conn
-                        .send_response(conn, stream_id, &headers, false)
-                    {
+                    match self.h3_conn.send_response_with_priority(
+                        conn, stream_id, &headers, &priority, false,
+                    ) {
                         Ok(v) => v,
 
                         Err(quiche::h3::Error::StreamBlocked) => {