//! The `draft-ietf-moq-transport-16` vector corpus (`tests/vectors/draft-16/`). //! //! Built as `draft18_vectors.rs` builds -18's, with `joule_moq::draft16`. The official //! examples here: the integer examples of RFC 9000 Appendix A.1, which -16's `(i)` //! fields follow (§1.4 adopts RFC 9000 §1.3's conventions), and the first subgroup of //! -16's §10.5 (its second example gives a Length of 21 for a 14-byte value, so it //! describes no one set of bytes). //! //! The negative vectors include where -16 refuses what -18 or -22 read: -22's SETUP, //! -18's SUBSCRIBE_TRACKS and PUBLISH_STATE_NOTIFY, RENDEZVOUS_TIMEOUT, LARGEST_OBJECT //! without its length, a DELIVERY_TIMEOUT of 0, an empty Track Namespace, the //! FIRST_OBJECT and padding stream types, End of Timed-Out Range. //! //! `JOULE_MOQ_WRITE_VECTORS=1` rewrites the corpus; `vectors/draft-16/moqt16_check.py` //! decodes it independently. use joule_moq::draft16::codes::{PublishDoneCode, RequestErrorCode}; use joule_moq::draft16::control::{msg, Fetch, FetchKind, FetchOk, Ok_, Publish, PublishDone, PublishNamespace, PublishNamespaceCancel, RequestError, RequestUpdate, SubscribeNamespace, SubscribeOk, SubscribeOptions, TrackRequest, STREAM_COUNT_UNKNOWN}; use joule_moq::draft16::data::{ encode_fetch_stream, encode_subgroup_stream, DatagramContent, FetchCodec, FetchEntry, FetchHeader, FetchObject, ObjectDatagram, RangeEnd, StreamHeader, SubgroupCodec, SubgroupObject, UniEvent, UniStreamDecoder, }; use joule_moq::draft16::io::{get, put}; use joule_moq::draft16::kvp::{ext, setup, KeyValuePair, Properties, SetupParameters, Token}; use joule_moq::draft16::params::{GroupOrder, Parameter, Parameters, SubscriptionFilter}; use joule_moq::draft16::{json, ControlMessage}; use joule_moq::draft22::data::{ObjectStatus, SubgroupHeader, SubgroupId}; use joule_moq::draft22::names::Location; use joule_moq::draft22::WireError; use joule_moq::track::{TrackName, TrackNamespace}; use serde_json::{json, Value}; const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors/draft-16/vectors.json"); const PV: &str = "session:PROTOCOL_VIOLATION"; const KVF: &str = "session:KEY_VALUE_FORMATTING_ERROR"; fn hx(b: &[u8]) -> String { json::hex(b) } fn ns(f: &[&str]) -> TrackNamespace { TrackNamespace::from_strs(f) } fn name(s: &str) -> TrackName { TrackName::from_str(s) } fn kv(ty: u64, x: u64) -> KeyValuePair { KeyValuePair::varint(ty, x) } fn kb(ty: u64, b: &[u8]) -> KeyValuePair { KeyValuePair::bytes(ty, b.to_vec()) } fn v(x: u64) -> Vec { let mut o = Vec::new(); put(&mut o, x).unwrap(); o } fn cat(parts: &[&[u8]]) -> Vec { parts.concat() } fn frame(ty: u64, body: &[u8]) -> Vec { let mut out = v(ty); out.extend_from_slice(&(body.len() as u16).to_be_bytes()); out.extend_from_slice(body); out } fn sorted(mut ps: Vec) -> Parameters { ps.sort_by_key(|p| p.type_code()); Parameters(ps) } fn unhex(s: &str) -> Vec { (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() } struct Corpus { vectors: Vec, } impl Corpus { fn push(&mut self, name: &str, kind: &str, bytes: &[u8], canonical: bool, expect: Value) { self.vectors.push(json!({"name": name, "kind": kind, "hex": hx(bytes), "canonical": canonical, "expect": expect})); } fn control(&mut self, name: &str, m: ControlMessage) { let b = m.encode().expect(name); self.push(name, "control", &b, true, json::control(&m)); } fn stream(&mut self, name: &str, bytes: Vec, events: Vec) { self.push(name, "uni-stream", &bytes, true, Value::Array(events.iter().map(json::uni_event).collect())); } fn bad(&mut self, name: &str, kind: &str, bytes: &[u8], error: &str, consequence: &str) { self.push(name, kind, bytes, false, json!({"error": error, "consequence": consequence})); } } fn build() -> Vec { let mut c = Corpus { vectors: Vec::new() }; // ── the integer: RFC 9000 Appendix A.1 ── for (h, val, shortest) in [("c2197c5eff14e88c", 151_288_809_941_952_652u64, true), ("9d7f3e7d", 494_878_333, true), ("7bbd", 15_293, true), ("25", 37, true), ("4025", 37, false)] { let b = unhex(h); c.push(&format!("varint {h} (RFC 9000 A.1)"), "varint", &b, shortest, json!({"value": val, "length": b.len()})); } c.bad("varint cut short", "varint", &[0x80, 0x01], "truncated", PV); // ── §9.3 CLIENT_SETUP and SERVER_SETUP ── let mut p = SetupParameters::new(); p.push(kb(setup::PATH, b"/moq")) .push(kv(setup::MAX_REQUEST_ID, 100)) .push(kb(setup::AUTHORIZATION_TOKEN, &joule_moq::draft16::kvp::token_to_bytes(&Token::UseValue { token_type: 0, value: b"tok".to_vec() }).unwrap())) .push(kv(setup::MAX_AUTH_TOKEN_CACHE_SIZE, 1024)) .push(kb(setup::AUTHORITY, b"relay.example:443")) .push(kb(setup::MOQT_IMPLEMENTATION, b"example-impl/1.0")); c.control("CLIENT_SETUP", ControlMessage::ClientSetup(p)); let mut p = SetupParameters::new(); p.push(kv(setup::MAX_REQUEST_ID, 64)).push(kb(0x9d, b"g")).push(kb(0x9d, b"h")); c.control("SERVER_SETUP with an unknown parameter twice", ControlMessage::ServerSetup(p)); // ── §9.4-9.6 ── c.control("GOAWAY", ControlMessage::Goaway(b"moqt://relay.example/next".to_vec())); c.control("MAX_REQUEST_ID", ControlMessage::MaxRequestId(1000)); c.control("REQUESTS_BLOCKED", ControlMessage::RequestsBlocked(64)); // ── §9.9 SUBSCRIBE, every parameter it may carry, each a Key-Value-Pair ── let every = sorted(vec![ Parameter::DeliveryTimeout(250), Parameter::AuthorizationToken(Token::UseValue { token_type: 0, value: b"jwt".to_vec() }), Parameter::Forward(true), Parameter::SubscriberPriority(200), Parameter::SubscriptionFilter(SubscriptionFilter::AbsoluteRange { start: Location::new(5, 1), end_group: 7 }), Parameter::GroupOrder(GroupOrder::Descending), Parameter::NewGroupRequest(0), ]); c.control("SUBSCRIBE with every parameter", ControlMessage::Subscribe(TrackRequest { request_id: 0, namespace: ns(&["moq-test", "interop"]), name: name("test-track"), params: every })); for (label, f) in [("Next Group Start", SubscriptionFilter::NextGroupStart), ("Largest Object", SubscriptionFilter::LargestObject), ("AbsoluteStart", SubscriptionFilter::AbsoluteStart { start: Location::new(300, 0) })] { c.control(&format!("SUBSCRIBE with filter {label}"), ControlMessage::Subscribe(TrackRequest { request_id: 2, namespace: ns(&["a"]), name: name("b"), params: Parameters::new().with(Parameter::SubscriptionFilter(f)) })); } c.control( "SUBSCRIBE_OK", ControlMessage::SubscribeOk(SubscribeOk { request_id: 0, track_alias: 7, params: sorted(vec![Parameter::Expires(60_000), Parameter::LargestObject(Location::new(12, 3))]), track_extensions: Properties(vec![kv(ext::DEFAULT_PUBLISHER_PRIORITY, 64)]), }), ); c.control("REQUEST_UPDATE", ControlMessage::RequestUpdate(RequestUpdate { request_id: 4, existing_request_id: 0, params: sorted(vec![Parameter::Forward(false), Parameter::SubscriberPriority(3)]) })); c.control("UNSUBSCRIBE", ControlMessage::Unsubscribe(0)); c.control( "PUBLISH", ControlMessage::Publish(Publish { request_id: 1, namespace: ns(&["live", "a"]), name: name("cam"), track_alias: 10, params: sorted(vec![Parameter::Forward(false), Parameter::LargestObject(Location::new(0, 0))]), track_extensions: Properties(vec![kv(ext::DEFAULT_PUBLISHER_PRIORITY, 96)]), }), ); c.control( "PUBLISH_OK with the subscription's parameters", ControlMessage::PublishOk(Ok_ { request_id: 1, params: sorted(vec![Parameter::SubscriptionFilter(SubscriptionFilter::NextGroupStart), Parameter::DeliveryTimeout(100), Parameter::Expires(9)]) }), ); c.control("PUBLISH_DONE EXPIRED (0x5 in -16)", ControlMessage::PublishDone(PublishDone { request_id: 1, status: PublishDoneCode::EXPIRED, stream_count: STREAM_COUNT_UNKNOWN, reason: b"expired".to_vec() })); c.control("REQUEST_OK answering TRACK_STATUS", ControlMessage::RequestOk(Ok_ { request_id: 6, params: Parameters::new().with(Parameter::LargestObject(Location::new(9, 0))) })); c.control("REQUEST_ERROR", ControlMessage::RequestError(RequestError { request_id: 8, code: RequestErrorCode::INVALID_JOINING_REQUEST_ID, retry_interval: 1, reason: b"no".to_vec() })); // ── §9.16-9.18 FETCH ── c.control( "FETCH Standalone, groups 1 to 3 whole (End {4, 0})", ControlMessage::Fetch(Fetch { request_id: 10, kind: FetchKind::Standalone { namespace: ns(&["vod"]), name: name("ep1"), start: Location::new(1, 0), end: Location::new(4, 0) }, params: sorted(vec![Parameter::GroupOrder(GroupOrder::Descending), Parameter::SubscriberPriority(5)]), }), ); c.control("FETCH Relative Joining", ControlMessage::Fetch(Fetch { request_id: 12, kind: FetchKind::RelativeJoining { joining_request_id: 0, joining_start: 2 }, params: Parameters::new() })); c.control("FETCH Absolute Joining", ControlMessage::Fetch(Fetch { request_id: 14, kind: FetchKind::AbsoluteJoining { joining_request_id: 0, joining_start: 100 }, params: Parameters::new() })); c.control("FETCH_OK", ControlMessage::FetchOk(FetchOk { request_id: 10, end_of_track: false, end_location: Location::new(3, 5), params: Parameters::new(), track_extensions: Properties::new() })); c.control("FETCH_CANCEL", ControlMessage::FetchCancel(10)); // ── §9.19-9.25 ── c.control("TRACK_STATUS", ControlMessage::TrackStatus(TrackRequest { request_id: 16, namespace: ns(&["a"]), name: name("b"), params: Parameters::new() })); c.control("PUBLISH_NAMESPACE", ControlMessage::PublishNamespace(PublishNamespace { request_id: 18, namespace: ns(&["moq-test", "interop"]), params: Parameters::new() })); c.control("NAMESPACE", ControlMessage::Namespace(ns(&["x"]))); c.control("PUBLISH_NAMESPACE_DONE", ControlMessage::PublishNamespaceDone(18)); c.control("NAMESPACE_DONE with an empty suffix", ControlMessage::NamespaceDone(TrackNamespace(vec![]))); c.control("PUBLISH_NAMESPACE_CANCEL", ControlMessage::PublishNamespaceCancel(PublishNamespaceCancel { request_id: 18, code: RequestErrorCode::UNINTERESTED, reason: vec![] })); for (label, o) in [("PUBLISH", SubscribeOptions::Publish), ("NAMESPACE", SubscribeOptions::Namespace), ("both", SubscribeOptions::Both)] { c.control( &format!("SUBSCRIBE_NAMESPACE, Subscribe Options {label}"), ControlMessage::SubscribeNamespace(SubscribeNamespace { request_id: 20, prefix: ns(&["live"]), options: o, params: Parameters::new().with(Parameter::Forward(true)) }), ); } // ── §10.5's first example ── let h = SubgroupHeader { track_alias: 2, group_id: 0, subgroup_id: SubgroupId::Explicit(0), publisher_priority: Some(0), properties: false, end_of_group: false, first_object: false }; let objs = vec![ SubgroupObject { object_id: 0, extensions: Properties::new(), status: ObjectStatus::Normal, payload: b"abcd".to_vec() }, SubgroupObject { object_id: 1, extensions: Properties::new(), status: ObjectStatus::Normal, payload: b"efgh".to_vec() }, ]; let b = encode_subgroup_stream(&h, &objs).unwrap(); assert_eq!(b, cat(&[&[0x14, 2, 0, 0, 0, 0, 4], b"abcd", &[0, 4], b"efgh"]), "§10.5's first example, byte for byte"); let mut ev = vec![UniEvent::Header(StreamHeader::Subgroup(h))]; ev.extend(objs.into_iter().map(UniEvent::SubgroupObject)); c.stream("subgroup: §10.5's first example (type 0x14)", b, ev); // A subgroup with Immutable Extensions holding -16 pairs (this crate's receipt // property, 0x3A41, takes two bytes of -16's integer, `7a41`). let mut inner = Vec::new(); joule_moq::draft16::kvp::encode_pairs(&[kb(0x3a41, &[7; 40]), kv(0x3a44, 1)], &mut inner).unwrap(); let ex = Properties(vec![kb(ext::IMMUTABLE_EXTENSIONS, &inner)]); let h = SubgroupHeader { track_alias: 300, group_id: 70_000, subgroup_id: SubgroupId::Zero, publisher_priority: None, properties: true, end_of_group: true, first_object: false }; let o1 = SubgroupObject { object_id: 0, extensions: ex, status: ObjectStatus::Normal, payload: b"WAI1....".to_vec() }; let o2 = SubgroupObject { object_id: 4, extensions: Properties::new(), status: ObjectStatus::EndOfGroup, payload: vec![] }; let b = encode_subgroup_stream(&h, &[o1.clone(), o2.clone()]).unwrap(); c.stream("subgroup with Immutable Extensions and End of Group", b, vec![UniEvent::Header(StreamHeader::Subgroup(h)), UniEvent::SubgroupObject(o1), UniEvent::SubgroupObject(o2)]); // ── §10.4.4 a fetch stream: absolute IDs, both End of Range indicators ── let entries = vec![ FetchEntry::Object(FetchObject { group_id: 4, object_id: 0, subgroup_id: Some(0), publisher_priority: 7, extensions: None, payload: b"a".to_vec() }), FetchEntry::Object(FetchObject { group_id: 4, object_id: 2, subgroup_id: Some(1), publisher_priority: 7, extensions: Some(Properties::new()), payload: b"b".to_vec() }), FetchEntry::EndOfRange { kind: RangeEnd::NonExistent, location: Location::new(4, 9) }, FetchEntry::EndOfRange { kind: RangeEnd::Unknown, location: Location::new(1, 2) }, FetchEntry::Object(FetchObject { group_id: 1, object_id: 3, subgroup_id: None, publisher_priority: 9, extensions: None, payload: b"c".to_vec() }), ]; let b = encode_fetch_stream(&FetchHeader { request_id: 10 }, &entries).unwrap(); let mut ev = vec![UniEvent::Header(StreamHeader::Fetch(FetchHeader { request_id: 10 }))]; ev.extend(entries.into_iter().map(UniEvent::FetchEntry)); c.stream("fetch stream, Group IDs absolute", b, ev); // ── §10.3.1 datagrams ── let d = ObjectDatagram { track_alias: 1, group_id: 200, object_id: 3, publisher_priority: Some(9), extensions: Some(Properties(vec![kv(ext::PRIOR_OBJECT_ID_GAP, 1)])), end_of_group: true, content: DatagramContent::Payload(b"xyz".to_vec()) }; let b = d.encode().unwrap(); c.push("datagram with extensions and End of Group", "datagram", &b, true, json::datagram(&d)); let d = ObjectDatagram { track_alias: 1, group_id: 2, object_id: 0, publisher_priority: None, extensions: None, end_of_group: false, content: DatagramContent::Status(ObjectStatus::EndOfTrack) }; let b = d.encode().unwrap(); c.push("datagram with a status", "datagram", &b, true, json::datagram(&d)); // ── where -16 refuses what later drafts read ── c.bad("-22's SETUP (0x2F00)", "control", &cat(&[&joule_moq::draft22::varint::to_vec(0x2f00), &[0, 0]]), "unknown-message-type", PV); c.bad("SUBSCRIBE_TRACKS (0x51, -17 on)", "control", &frame(0x51, &[0]), "unknown-message-type", PV); c.bad("PUBLISH_STATE_NOTIFY (0x22)", "control", &frame(0x22, &[0]), "unknown-message-type", PV); let sub = |params: &[u8]| frame(msg::SUBSCRIBE, &cat(&[&[0, 1, 1, b'a', 1, b'b'], params])); c.bad("SUBSCRIBE: RENDEZVOUS_TIMEOUT (0x04) is unknown", "control", &sub(&[1, 0x04, 1]), "unknown-parameter", PV); c.bad("SUBSCRIBE: DELIVERY_TIMEOUT of 0", "control", &sub(&[1, 0x02, 0]), "invalid-value", PV); c.bad("SUBSCRIBE: SUBSCRIBER_PRIORITY of 256", "control", &sub(&cat(&[&[1, 0x20], &v(256)])), "invalid-value", PV); c.bad("SUBSCRIBE: a filter whose length runs past it", "control", &sub(&[1, 0x21, 2, 1, 0]), "trailing-bytes", PV); c.bad("SUBSCRIBE: FORWARD twice", "control", &sub(&[2, 0x10, 1, 0, 1]), "duplicate", PV); c.bad("SUBSCRIBE: an empty Track Namespace", "control", &frame(msg::SUBSCRIBE, &[0, 0, 1, b'b', 0]), "invalid-value", PV); c.bad("SUBSCRIBE_OK: LARGEST_OBJECT without its length (-18's form)", "control", &frame(msg::SUBSCRIBE_OK, &[0, 1, 1, 0x09, 1, 2]), "session-rule", KVF); c.bad("SUBSCRIBE_NAMESPACE: Subscribe Options of 3", "control", &frame(msg::SUBSCRIBE_NAMESPACE, &[0, 0, 3, 0]), "invalid-value", PV); c.bad("FETCH: Fetch Type 4", "control", &frame(msg::FETCH, &[0, 4, 0]), "invalid-value", PV); c.bad("subgroup: FIRST_OBJECT (0x50, -17 on)", "uni-stream", &[0x40, 0x50, 1, 1], "unknown-stream-type", PV); c.bad("subgroup: SUBGROUP_ID_MODE 0b11", "uni-stream", &[0x16, 0, 0, 0, 0], "invalid-flags", PV); c.bad("a padding stream (-17 on)", "uni-stream", &cat(&[&v(0x132b_3e28), &[0, 0]]), "unknown-stream-type", PV); let mut fh = Vec::new(); FetchHeader { request_id: 0 }.encode(&mut fh).unwrap(); c.bad("fetch: End of Timed-Out Range (0x20C)", "uni-stream", &cat(&[&fh, &v(0x20c), &[1, 1]]), "invalid-flags", PV); c.bad("fetch: a first object that refers back", "uni-stream", &cat(&[&fh, &[0x1d, 0, 0, 7, 1, b'a']]), "no-prior-object", PV); c.bad("datagram: STATUS and END_OF_GROUP", "datagram", &[0x22, 1, 2, 3, 0], "invalid-flags", PV); c.vectors } fn corpus_doc(vectors: Vec) -> Value { json!({ "about": "draft-ietf-moq-transport-16 wire vectors: varints (RFC 9000 Appendix A.1, which -16's (i) fields follow), every control message of Table 1, subgroup streams (with -16's §10.5 example) and fetch streams, datagrams, and negative vectors for what -16 refuses, each with -16's consequence. The JSON view is joule_moq::draft16::json's (the crate README describes it). Generated by tests/draft16_vectors.rs; regenerate with JOULE_MOQ_WRITE_VECTORS=1.", "draft": "draft-ietf-moq-transport-16", "vectors": vectors, }) } fn reencode(ev: &[UniEvent]) -> Option> { let mut out = Vec::new(); let mut sub: Option = None; let mut fetch: Option = None; for e in ev { match e { UniEvent::Header(StreamHeader::Subgroup(h)) => { joule_moq::draft16::data::encode_subgroup_header(h, &mut out).ok()?; sub = Some(SubgroupCodec::new(*h)); } UniEvent::Header(StreamHeader::Fetch(h)) => { h.encode(&mut out).ok()?; fetch = Some(FetchCodec::new()); } UniEvent::SubgroupObject(o) => sub.as_mut()?.encode_object(o, &mut out).ok()?, UniEvent::FetchEntry(f) => fetch.as_mut()?.encode(f, &mut out).ok()?, } } Some(out) } fn decode(x: &Value) -> Result<(Value, Option>), Value> { let b = unhex(x["hex"].as_str().unwrap()); let err = |e: WireError| json::error(&e); match x["kind"].as_str().unwrap() { "varint" => { let (val, n) = get(&b, "varint").map_err(err)?; if n != b.len() { return Err(json!({"error": "trailing-bytes"})); } Ok((json!({"value": val, "length": n}), Some(v(val)))) } "control" => { let m = ControlMessage::decode_exact(&b).map_err(err)?; Ok((json::control(&m), Some(m.encode().unwrap()))) } "uni-stream" => { let ev = joule_moq::draft16::decode_uni_stream(&b).map_err(err)?; Ok((Value::Array(ev.iter().map(json::uni_event).collect()), reencode(&ev))) } "datagram" => { let d = ObjectDatagram::decode(&b).map_err(err)?; Ok((json::datagram(&d), Some(d.encode().unwrap()))) } k => panic!("unknown kind {k}"), } } fn decode_incrementally(b: &[u8], step: usize) -> Result, WireError> { let mut d = UniStreamDecoder::new(usize::MAX); let mut out = Vec::new(); for chunk in b.chunks(step.max(1)) { d.push(chunk); while let Some(e) = d.next()? { out.push(e); } } d.finish()?; Ok(out) } #[test] fn the_draft16_corpus_is_current_and_holds() { let doc = corpus_doc(build()); let text = serde_json::to_string_pretty(&doc).unwrap() + "\n"; if std::env::var("JOULE_MOQ_WRITE_VECTORS").as_deref() == Ok("1") { std::fs::create_dir_all(std::path::Path::new(CORPUS).parent().unwrap()).unwrap(); std::fs::write(CORPUS, &text).unwrap(); } let committed = std::fs::read_to_string(CORPUS).unwrap_or_else(|e| panic!("read {CORPUS}: {e}")); assert!(committed == text, "{CORPUS} is not what tests/draft16_vectors.rs generates; regenerate it with JOULE_MOQ_WRITE_VECTORS=1"); let mut failures = Vec::new(); let (mut good, mut bad) = (0, 0); for x in doc["vectors"].as_array().unwrap() { let name = x["name"].as_str().unwrap(); let expect = &x["expect"]; let b = unhex(x["hex"].as_str().unwrap()); match (expect.get("error"), decode(x)) { (None, Ok((got, again))) => { good += 1; if &got != expect { failures.push(format!("{name}: read as {got}")); } if x["canonical"] == true && again.as_deref() != Some(&b[..]) { failures.push(format!("{name}: re-encodes to other bytes")); } } (None, Err(e)) => failures.push(format!("{name}: refused ({e}); the corpus reads it")), (Some(_), Ok((got, _))) => failures.push(format!("{name}: read as {got}; the corpus refuses it")), (Some(_), Err(e)) => { bad += 1; if &e != expect { failures.push(format!("{name}: refused as {e}, the corpus says {expect}")); } } } if x["kind"] == "uni-stream" { for step in [1, 3, b.len().max(1)] { match (expect.get("error"), decode_incrementally(&b, step)) { (None, Ok(ev)) => { let got = Value::Array(ev.iter().map(json::uni_event).collect()); if &got != expect { failures.push(format!("{name}: in pieces of {step}, read as {got}")); } } (None, Err(e)) => failures.push(format!("{name}: in pieces of {step}, refused ({e})")), (Some(_), Ok(_)) => failures.push(format!("{name}: in pieces of {step}, accepted")), (Some(_), Err(_)) => {} } } } } assert!(failures.is_empty(), "{} vector(s) disagree:\n{}", failures.len(), failures.join("\n")); assert!(good > 0 && bad > 0); }