Skip to content

Commit 78fe17b

Browse files
committed
add TxtRecord: container to create DNS-SD TXT RDATA
1 parent a32a31c commit 78fe17b

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed

src/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
//! * [`DNSServiceRefSockFD`] used for integration with tokio (event loop)
4040
//! * [`DNSServiceRemoveRecord`] called when dropping [`Record`](struct.Record.html)
4141
//!
42+
//! The `TXTRecord*` "TXT Record Construction Functions" are not
43+
//! wrapped; [`TxtRecord`] provides a native rust implementation with
44+
//! similar functionality.
45+
//!
4246
//! [`DNSServiceAddRecord`]: https://developer.apple.com/documentation/dnssd/1804730-dnsserviceaddrecord
4347
//! [`DNSServiceBrowse`]: https://developer.apple.com/documentation/dnssd/1804742-dnsservicebrowse
4448
//! [`DNSServiceConstructFullName`]: https://developer.apple.com/documentation/dnssd/1804753-dnsserviceconstructfullname
@@ -68,6 +72,7 @@
6872
//! [`Record::update_record`]: struct.Record.html#method.update_record
6973
//! [`RegisterRecord::update_record`]: struct.RegisterRecord.html#method.update_record
7074
//! [`TimeoutStream`]: struct.TimeoutStream.html
75+
//! [`TxtRecord`]: struct.TxtRecord.html
7176
7277
#![warn(missing_docs)]
7378

@@ -91,6 +96,7 @@ pub use self::interface::*;
9196
pub use self::remote::*;
9297
pub use self::service::*;
9398
pub use self::timeout_stream::*;
99+
pub use self::txt_record::*;
94100

95101
mod flags_macro;
96102

@@ -106,6 +112,7 @@ mod remote;
106112
mod service;
107113
mod stream;
108114
mod timeout_stream;
115+
mod txt_record;
109116

110117
fn init() {
111118
#[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))]

src/service/register.rs

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ pub struct RegisterData<'a> {
150150
pub host: Option<&'a str>,
151151
/// The TXT record rdata. Empty RDATA is treated like `b"\0"`, i.e.
152152
/// a TXT record with a single empty string.
153+
///
154+
/// You can use [`TxtRecord`] to create the value for this field
155+
/// (both [`TxtRecord::data`] and [`TxtRecord::rdata`] produce
156+
/// appropriate values).
157+
///
158+
/// [`TxtRecord`]: struct.TxtRecord.html
159+
/// [`TxtRecord::data`]: struct.TxtRecord.html#method.data
160+
/// [`TxtRecord::rdata`]: struct.TxtRecord.html#method.rdata
153161
pub txt: &'a [u8],
154162
}
155163

src/txt_record.rs

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
use std::ops::Range;
2+
3+
/// Key-Value container that uses DNS `TXT` RDATA as representation
4+
///
5+
/// The binary representation can be used as RDATA for `DNS-SD TXT
6+
/// Records` (see [RFC 6763, section 6]).
7+
///
8+
/// Each entry results in one string in the `TXT` represenation; `TXT`
9+
/// RDATA contains many (but at least one) possibly empty strings, each
10+
/// up to 255 bytes.
11+
///
12+
/// Key and value are separated by the first `=` in an entry, and the
13+
/// key must consist of printable ASCII characters (0x20...0x7E) apart
14+
/// from `=`. Keys should be 9 characters or fewer.
15+
///
16+
/// Values can be any binary string (but the total length of an entry
17+
/// cannot exceed 255 bytes).
18+
///
19+
/// An entry also can have no value at all (which is different from
20+
/// having an empty value) if there is no `=` separator in the entry.
21+
///
22+
/// [RFC 6763, section 6]: https://tools.ietf.org/html/rfc6763#section-6
23+
/// "RFC 6763, 6. Data Syntax for DNS-SD TXT Records"
24+
#[derive(Clone)]
25+
pub struct TxtRecord(Vec<u8>);
26+
27+
impl TxtRecord {
28+
/// Constructs a new, empty `TxtRecord`.
29+
pub fn new() -> Self {
30+
TxtRecord(Vec::new())
31+
}
32+
33+
/// Constructs a new, empty `TxtRecord` with the specified capacity.
34+
///
35+
/// The inserting operations will still reallocate if necessary.
36+
pub fn with_capacity(capacity: usize) -> Self {
37+
TxtRecord(Vec::with_capacity(capacity))
38+
}
39+
40+
/// Reserves capacity for at least `additional` more bytes to be
41+
/// used by inserting operations.
42+
///
43+
/// Each entry requires 1 byte for the total length, the length
44+
/// of the key for the key; if there is a value 1 byte for the
45+
/// separator `=` and the length of the value for the value.
46+
pub fn reserve(&mut self, additional: usize) {
47+
self.0.reserve(additional);
48+
}
49+
50+
/// Returns `true` if the `TxtRecord` contains no elements (both in
51+
/// bytes and key-value entries).
52+
pub fn is_empty(&self) -> bool {
53+
self.0.is_empty()
54+
}
55+
56+
/// Clears the `TxtRecord`, removing all entries.
57+
pub fn clear(&mut self) {
58+
self.0.clear();
59+
}
60+
61+
/// if not empty this returns valid TXT RDATA, otherwise just an
62+
/// empty slice.
63+
pub fn data(&self) -> &[u8] {
64+
&self.0
65+
}
66+
67+
/// always returns valid TXT RDATA; when the container is empty it
68+
/// will return a TXT record with a single empty string (i.e.
69+
/// `&[0x00]`).
70+
pub fn rdata(&self) -> &[u8] {
71+
if self.0.is_empty() {
72+
&[0x00] // empty RDATA not allowed, use single empty chunk instead
73+
} else {
74+
&self.0
75+
}
76+
}
77+
78+
fn _position_keys(&self) -> PositionKeyIter {
79+
PositionKeyIter {
80+
pos: 0,
81+
data: &self.0,
82+
}
83+
}
84+
85+
/// Iterate over all `(key, value)` pairs.
86+
pub fn iter(&self) -> TxtRecordIter {
87+
TxtRecordIter {
88+
pos: 0,
89+
data: &self.0,
90+
}
91+
}
92+
93+
/// Get value for entry with given key
94+
///
95+
/// Returns `None` if there is no such entry, `Some(None)` if the
96+
/// entry exists but has no value, and `Some(Some(value))` if the
97+
/// entry exists and has a value.
98+
#[cfg_attr(feature = "cargo-clippy", allow(option_option))]
99+
pub fn get(&self, key: &[u8]) -> Option<Option<&[u8]>> {
100+
self.iter().find(|&(k, _)| key == k).map(|(_, value)| value)
101+
}
102+
103+
/// Remove entry with given key (if it exists)
104+
pub fn remove(&mut self, key: &[u8]) {
105+
if let Some((loc, _)) = self._position_keys().find(|&(_, k)| key == k) {
106+
self.0.drain(loc);
107+
}
108+
}
109+
110+
/// Insert or update the entry with `key` to have the given value or on value
111+
pub fn set(&mut self, key: &[u8], value: Option<&[u8]>) -> Result<(), TxtRecordError> {
112+
for &k in key {
113+
if k == b'=' || k < 0x20 || k > 0x7e {
114+
return Err(TxtRecordError::InvalidKey);
115+
}
116+
}
117+
let entry_len = key.len() + value.map(|v| v.len() + 1).unwrap_or(0);
118+
if entry_len > 255 {
119+
return Err(TxtRecordError::EntryTooLong);
120+
}
121+
self.remove(key);
122+
123+
self.0.push(entry_len as u8);
124+
self.0.extend_from_slice(key);
125+
if let Some(value) = value {
126+
self.0.push(b'=');
127+
self.0.extend_from_slice(value);
128+
}
129+
130+
Ok(())
131+
}
132+
133+
/// Insert or update the entry with `key` to have no value
134+
pub fn set_no_value(&mut self, key: &[u8]) -> Result<(), TxtRecordError> {
135+
self.set(key, None)
136+
}
137+
138+
/// Insert or update the entry with `key` to have the given value
139+
pub fn set_value(&mut self, key: &[u8], value: &[u8]) -> Result<(), TxtRecordError> {
140+
self.set(key, Some(value))
141+
}
142+
}
143+
144+
impl Default for TxtRecord {
145+
fn default() -> Self {
146+
TxtRecord::new()
147+
}
148+
}
149+
150+
impl<'a> IntoIterator for &'a TxtRecord {
151+
type Item = (&'a [u8], Option<&'a [u8]>);
152+
type IntoIter = TxtRecordIter<'a>;
153+
154+
fn into_iter(self) -> Self::IntoIter {
155+
self.iter()
156+
}
157+
}
158+
159+
/// Error returned when inserting new entries failed
160+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
161+
pub enum TxtRecordError {
162+
/// Key contained invalid characters
163+
InvalidKey,
164+
/// Total entry would be longer than 255 bytes
165+
EntryTooLong,
166+
}
167+
168+
struct PositionKeyIter<'a> {
169+
pos: usize,
170+
data: &'a [u8],
171+
}
172+
173+
impl<'a> Iterator for PositionKeyIter<'a> {
174+
// (start..end, key)
175+
type Item = (Range<usize>, &'a [u8]);
176+
177+
fn next(&mut self) -> Option<Self::Item> {
178+
if self.data.is_empty() {
179+
return None;
180+
}
181+
let len = self.data[0] as usize;
182+
let entry_pos = self.pos;
183+
let entry = &self.data[1..][..len];
184+
self.data = &self.data[len+1..];
185+
self.pos += len + 1;
186+
187+
Some(match entry.iter().position(|&b| b == b'=') {
188+
Some(pos) => (entry_pos..self.pos, &entry[..pos]),
189+
None => (entry_pos..self.pos, entry),
190+
})
191+
}
192+
}
193+
194+
/// Iterator for entries in `TxtRecord`
195+
///
196+
/// Items are `(key, value)` pairs.
197+
pub struct TxtRecordIter<'a> {
198+
pos: usize,
199+
data: &'a [u8],
200+
}
201+
202+
impl<'a> Iterator for TxtRecordIter<'a> {
203+
// key, value
204+
type Item = (&'a [u8], Option<&'a [u8]>);
205+
206+
fn next(&mut self) -> Option<Self::Item> {
207+
if self.data.is_empty() {
208+
return None;
209+
}
210+
let len = self.data[0] as usize;
211+
let entry = &self.data[1..][..len];
212+
self.data = &self.data[len+1..];
213+
self.pos += len + 1;
214+
215+
Some(match entry.iter().position(|&b| b == b'=') {
216+
Some(pos) => (&entry[..pos], Some(&entry[pos+1..])),
217+
None => (entry, None),
218+
})
219+
}
220+
}
221+
222+
#[cfg(test)]
223+
mod tests {
224+
use super::TxtRecord;
225+
226+
#[test]
227+
fn modifications() {
228+
let mut r = TxtRecord::new();
229+
assert!(r.is_empty());
230+
assert_eq!(r.data(), b"");
231+
assert_eq!(r.rdata(), b"\x00");
232+
233+
r.set(b"foo", Some(b"bar")).unwrap();
234+
assert!(!r.is_empty());
235+
assert_eq!(r.data(), b"\x07foo=bar");
236+
assert_eq!(r.rdata(), b"\x07foo=bar");
237+
238+
r.set(b"u", Some(b"vw")).unwrap();
239+
assert!(!r.is_empty());
240+
assert_eq!(r.data(), b"\x07foo=bar\x04u=vw");
241+
assert_eq!(r.rdata(), b"\x07foo=bar\x04u=vw");
242+
assert_eq!(r.iter().collect::<Vec<_>>(), vec![
243+
(b"foo" as &[u8], Some(b"bar" as &[u8])),
244+
(b"u", Some(b"vw")),
245+
]);
246+
247+
r.set(b"foo", None).unwrap();
248+
assert!(!r.is_empty());
249+
assert_eq!(r.data(), b"\x04u=vw\x03foo");
250+
assert_eq!(r.rdata(), b"\x04u=vw\x03foo");
251+
assert_eq!(r.iter().collect::<Vec<_>>(), vec![
252+
(b"u" as &[u8], Some(b"vw" as &[u8])),
253+
(b"foo", None),
254+
]);
255+
256+
r.set(b"foo", Some(b"bar")).unwrap();
257+
assert!(!r.is_empty());
258+
assert_eq!(r.data(), b"\x04u=vw\x07foo=bar");
259+
assert_eq!(r.rdata(), b"\x04u=vw\x07foo=bar");
260+
261+
r.remove(b"foo");
262+
assert!(!r.is_empty());
263+
assert_eq!(r.data(), b"\x04u=vw");
264+
assert_eq!(r.rdata(), b"\x04u=vw");
265+
}
266+
}

0 commit comments

Comments
 (0)