loona_hpack/lib.rs
1//! A module implementing HPACK functionality. Exposes a simple API for
2//! performing the encoding and decoding of header sets, according to the
3//! HPACK spec.
4
5use std::collections::VecDeque;
6use std::fmt;
7
8use tracing::debug;
9
10// Re-export the main HPACK API entry points.
11pub use self::decoder::Decoder;
12pub use self::encoder::Encoder;
13
14pub mod decoder;
15pub mod encoder;
16pub mod huffman;
17
18/// A struct representing the dynamic table that needs to be maintained by the
19/// coder.
20///
21/// The dynamic table contains a number of recently used headers. The size of
22/// the table is constrained to a certain number of octets. If on insertion of
23/// a new header into the table, the table would exceed the maximum size,
24/// headers are evicted in a FIFO fashion until there is enough room for the
25/// new header to be inserted. (Therefore, it is possible that though all
26/// elements end up being evicted, there is still not enough space for the new
27/// header: when the size of this individual header exceeds the maximum size of
28/// the table.)
29///
30/// The current size of the table is calculated, based on the IETF definition,
31/// as the sum of sizes of each header stored within the table, where the size
32/// of an individual header is
33/// `len_in_octets(header_name) + len_in_octets(header_value) + 32`.
34///
35/// Note: the maximum size of the dynamic table does not have to be equal to
36/// the maximum header table size as defined by a "higher level" protocol
37/// (such as the `SETTINGS_HEADER_TABLE_SIZE` setting in HTTP/2), since HPACK
38/// can choose to modify the dynamic table size on the fly (as long as it keeps
39/// it below the maximum value set by the protocol). So, the `DynamicTable`
40/// only cares about the maximum size as set by the HPACK {en,de}coder and lets
41/// *it* worry about making certain that the changes are valid according to
42/// the (current) constraints of the protocol.
43struct DynamicTable {
44 table: VecDeque<(Vec<u8>, Vec<u8>)>,
45 size: usize,
46 max_size: usize,
47}
48
49impl DynamicTable {
50 /// Creates a new empty dynamic table with a default size.
51 fn new() -> DynamicTable {
52 // The default maximum size corresponds to the default HTTP/2
53 // setting
54 DynamicTable::with_size(4096)
55 }
56
57 /// Creates a new empty dynamic table with the given maximum size.
58 fn with_size(max_size: usize) -> DynamicTable {
59 DynamicTable {
60 table: VecDeque::new(),
61 size: 0,
62 max_size,
63 }
64 }
65
66 /// Returns the current size of the table in octets, as defined by the IETF
67 /// HPACK spec.
68 fn get_size(&self) -> usize {
69 self.size
70 }
71
72 /// Returns an `Iterator` through the headers stored in the `DynamicTable`.
73 ///
74 /// The iterator will yield elements of type `(&[u8], &[u8])`,
75 /// corresponding to a single header name and value. The name and value
76 /// slices are borrowed from their representations in the `DynamicTable`
77 /// internal implementation, which means that it is possible only to
78 /// iterate through the headers, not mutate them.
79 fn iter(&self) -> impl Iterator<Item = (&[u8], &[u8])> {
80 self.table.iter().map(|(a, b)| (&a[..], &b[..]))
81 }
82
83 /// Sets the new maximum table size.
84 ///
85 /// If the current size of the table is larger than the new maximum size,
86 /// existing headers are evicted in a FIFO fashion until the size drops
87 /// below the new maximum.
88 fn set_max_table_size(&mut self, new_max_size: usize) {
89 self.max_size = new_max_size;
90 // Make the table size fit within the new constraints.
91 self.consolidate_table();
92 }
93
94 /// Returns the maximum size of the table in octets.
95 #[cfg(test)]
96 fn get_max_table_size(&self) -> usize {
97 self.max_size
98 }
99
100 /// Add a new header to the dynamic table.
101 ///
102 /// The table automatically gets resized, if necessary.
103 ///
104 /// Do note that, under the HPACK rules, it is possible the given header
105 /// is not found in the dynamic table after this operation finishes, in
106 /// case the total size of the given header exceeds the maximum size of the
107 /// dynamic table.
108 fn add_header(&mut self, name: Vec<u8>, value: Vec<u8>) {
109 // This is how the HPACK spec makes us calculate the size. The 32 is
110 // a magic number determined by them (under reasonable assumptions of
111 // how the table is stored).
112 self.size += name.len() + value.len() + 32;
113 debug!("New dynamic table size {}", self.size);
114 // Now add it to the internal buffer
115 self.table.push_front((name, value));
116 // ...and make sure we're not over the maximum size.
117 self.consolidate_table();
118 debug!("After consolidation dynamic table size {}", self.size);
119 }
120
121 /// Consolidates the table entries so that the table size is below the
122 /// maximum allowed size, by evicting headers from the table in a FIFO
123 /// fashion.
124 fn consolidate_table(&mut self) {
125 while self.size > self.max_size {
126 {
127 let last_header = match self.table.back() {
128 Some(x) => x,
129 None => {
130 // Can never happen as the size of the table must reach
131 // 0 by the time we've exhausted all elements.
132 panic!("Size of table != 0, but no headers left!");
133 }
134 };
135 self.size -= last_header.0.len() + last_header.1.len() + 32;
136 }
137 self.table.pop_back();
138 }
139 }
140
141 /// Returns the number of headers in the dynamic table.
142 ///
143 /// This is different than the size of the dynamic table.
144 fn len(&self) -> usize {
145 self.table.len()
146 }
147
148 /// Converts the current state of the table to a `Vec`
149 #[cfg(test)]
150 fn to_vec(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
151 let mut ret: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
152 for elem in self.table.iter() {
153 ret.push(elem.clone());
154 }
155
156 ret
157 }
158
159 /// Returns a reference to the header at the given index, if found in the
160 /// dynamic table.
161 fn get(&self, index: usize) -> Option<&(Vec<u8>, Vec<u8>)> {
162 self.table.get(index)
163 }
164}
165
166impl fmt::Debug for DynamicTable {
167 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
168 write!(formatter, "{:?}", self.table)
169 }
170}
171
172/// Represents the type of the static table, as defined by the HPACK spec.
173type StaticTable<'a> = &'a [(&'a [u8], &'a [u8])];
174
175/// The struct represents the header table obtained by merging the static and
176/// dynamic tables into a single index address space, as described in section
177/// `2.3.3.` of the HPACK spec.
178struct HeaderTable<'a> {
179 static_table: StaticTable<'a>,
180 dynamic_table: DynamicTable,
181}
182
183impl<'a> HeaderTable<'a> {
184 /// Creates a new header table where the static part is initialized with
185 /// the given static table.
186 pub fn with_static_table(static_table: StaticTable<'a>) -> HeaderTable<'a> {
187 HeaderTable {
188 static_table,
189 dynamic_table: DynamicTable::new(),
190 }
191 }
192
193 /// Returns an iterator through *all* headers stored in the header table,
194 /// i.e. it includes both the ones found in the static table and the
195 /// dynamic table, in the order of their indices in the single address
196 /// space (first the headers in the static table, followed by headers in
197 /// the dynamic table).
198 ///
199 /// The type yielded by the iterator is `(&[u8], &[u8])`, where the tuple
200 /// corresponds to the header name, value pairs in the described order.
201 pub fn iter(&self) -> impl Iterator<Item = (&[u8], &[u8])> + '_ {
202 self.static_table
203 .iter()
204 .copied()
205 .chain(self.dynamic_table.iter())
206 }
207
208 /// Adds the given header to the table. Of course, this means that the new
209 /// header is added to the dynamic part of the table.
210 ///
211 /// If the size of the new header is larger than the current maximum table
212 /// size of the dynamic table, the effect will be that the dynamic table
213 /// gets emptied and the new header does *not* get inserted into it.
214 #[inline]
215 pub fn add_header(&mut self, name: Vec<u8>, value: Vec<u8>) {
216 self.dynamic_table.add_header(name, value);
217 }
218
219 /// Returns a reference to the header (a `(name, value)` pair) with the
220 /// given index in the table.
221 ///
222 /// The table is 1-indexed and constructed in such a way that the first
223 /// entries belong to the static table, followed by entries in the dynamic
224 /// table. They are merged into a single index address space, though.
225 ///
226 /// This is according to the [HPACK spec, section 2.3.3.]
227 /// <http://http2.github.io/http2-spec/compression.html#index.address.space>
228 pub fn get_from_table(&self, index: usize) -> Option<(&[u8], &[u8])> {
229 // The IETF defined table indexing as 1-based.
230 // So, before starting, make sure the given index is within the proper
231 // bounds.
232 let real_index = if index > 0 { index - 1 } else { return None };
233
234 if real_index < self.static_table.len() {
235 // It is in the static table so just return that...
236 Some(self.static_table[real_index])
237 } else {
238 // Maybe it's in the dynamic table then?
239 let dynamic_index = real_index - self.static_table.len();
240 if dynamic_index < self.dynamic_table.len() {
241 self.dynamic_table
242 .get(dynamic_index)
243 .map(|(name, value)| (&name[..], &value[..]))
244 } else {
245 // Index out of bounds!
246 None
247 }
248 }
249 }
250
251 /// Finds the given header in the header table. Tries to match both the
252 /// header name and value to one of the headers in the table. If no such
253 /// header exists, then falls back to returning one that matched only the
254 /// name.
255 ///
256 /// # Returns
257 ///
258 /// An `Option`, where `Some` corresponds to a tuple representing the index
259 /// of the header in the header tables (the 1-based index that HPACK uses)
260 /// and a `bool` indicating whether the value of the header also matched.
261 pub fn find_header(&self, header: (&[u8], &[u8])) -> Option<(usize, bool)> {
262 // Just does a simple scan of the entire table, searching for a header
263 // that matches both the name and the value of the given header.
264 // If no such header is found, then any one of the headers that had a
265 // matching name is returned, with the appropriate return flag set.
266 //
267 // The tables are so small that it is unlikely that the linear scan
268 // would be a major performance bottlneck. If it does prove to be,
269 // though, a more efficient lookup/header representation method could
270 // be devised.
271 let mut matching_name: Option<usize> = None;
272 for (i, h) in self.iter().enumerate() {
273 if header.0 == h.0 {
274 if header.1 == h.1 {
275 // Both name and value matched: returns it immediately
276 return Some((i + 1, true));
277 }
278 // If only the name was valid, we continue scanning, hoping to
279 // find one where both the name and value match. We remember
280 // this one, in case such a header isn't found after all.
281 matching_name = Some(i + 1);
282 }
283 }
284
285 // Finally, if there's no header with a matching name and value,
286 // return one that matched only the name, if that *was* found.
287 matching_name.map(|i| (i, false))
288 }
289}
290
291/// The table represents the static header table defined by the HPACK spec.
292/// (HPACK, Appendix A)
293static STATIC_TABLE: &[(&[u8], &[u8])] = &[
294 (b":authority", b""),
295 (b":method", b"GET"),
296 (b":method", b"POST"),
297 (b":path", b"/"),
298 (b":path", b"/index.html"),
299 (b":scheme", b"http"),
300 (b":scheme", b"https"),
301 (b":status", b"200"),
302 (b":status", b"204"),
303 (b":status", b"206"),
304 (b":status", b"304"),
305 (b":status", b"400"),
306 (b":status", b"404"),
307 (b":status", b"500"),
308 (b"accept-charset", b""),
309 (b"accept-encoding", b"gzip, deflate"),
310 (b"accept-language", b""),
311 (b"accept-ranges", b""),
312 (b"accept", b""),
313 (b"access-control-allow-origin", b""),
314 (b"age", b""),
315 (b"allow", b""),
316 (b"authorization", b""),
317 (b"cache-control", b""),
318 (b"content-disposition", b""),
319 (b"content-encoding", b""),
320 (b"content-language", b""),
321 (b"content-length", b""),
322 (b"content-location", b""),
323 (b"content-range", b""),
324 (b"content-type", b""),
325 (b"cookie", b""),
326 (b"date", b""),
327 (b"etag", b""),
328 (b"expect", b""),
329 (b"expires", b""),
330 (b"from", b""),
331 (b"host", b""),
332 (b"if-match", b""),
333 (b"if-modified-since", b""),
334 (b"if-none-match", b""),
335 (b"if-range", b""),
336 (b"if-unmodified-since", b""),
337 (b"last-modified", b""),
338 (b"link", b""),
339 (b"location", b""),
340 (b"max-forwards", b""),
341 (b"proxy-authenticate", b""),
342 (b"proxy-authorization", b""),
343 (b"range", b""),
344 (b"referer", b""),
345 (b"refresh", b""),
346 (b"retry-after", b""),
347 (b"server", b""),
348 (b"set-cookie", b""),
349 (b"strict-transport-security", b""),
350 (b"transfer-encoding", b""),
351 (b"user-agent", b""),
352 (b"vary", b""),
353 (b"via", b""),
354 (b"www-authenticate", b""),
355];
356
357#[cfg(test)]
358mod tests {
359 use super::DynamicTable;
360 use super::HeaderTable;
361 use super::STATIC_TABLE;
362
363 #[test]
364 fn test_dynamic_table_size_calculation_simple() {
365 let mut table = DynamicTable::new();
366 // Sanity check
367 assert_eq!(0, table.get_size());
368
369 table.add_header(b"a".to_vec(), b"b".to_vec());
370
371 assert_eq!(32 + 2, table.get_size());
372 }
373
374 #[test]
375 fn test_dynamic_table_size_calculation() {
376 let mut table = DynamicTable::new();
377
378 table.add_header(b"a".to_vec(), b"b".to_vec());
379 table.add_header(b"123".to_vec(), b"456".to_vec());
380 table.add_header(b"a".to_vec(), b"b".to_vec());
381
382 assert_eq!(3 * 32 + 2 + 6 + 2, table.get_size());
383 }
384
385 /// Tests that the `DynamicTable` gets correctly resized (by evicting old
386 /// headers) if it exceeds the maximum size on an insertion.
387 #[test]
388 fn test_dynamic_table_auto_resize() {
389 let mut table = DynamicTable::with_size(38);
390 table.add_header(b"a".to_vec(), b"b".to_vec());
391 assert_eq!(32 + 2, table.get_size());
392
393 table.add_header(b"123".to_vec(), b"456".to_vec());
394
395 // Resized?
396 assert_eq!(32 + 6, table.get_size());
397 // Only has the second header?
398 assert_eq!(table.to_vec(), vec![(b"123".to_vec(), b"456".to_vec())]);
399 }
400
401 /// Tests that when inserting a new header whose size is larger than the
402 /// size of the entire table, the table is fully emptied.
403 #[test]
404 fn test_dynamic_table_auto_resize_into_empty() {
405 let mut table = DynamicTable::with_size(38);
406 table.add_header(b"a".to_vec(), b"b".to_vec());
407 assert_eq!(32 + 2, table.get_size());
408
409 table.add_header(b"123".to_vec(), b"4567".to_vec());
410
411 // Resized and empty?
412 assert_eq!(0, table.get_size());
413 assert_eq!(0, table.to_vec().len());
414 }
415
416 /// Tests that when changing the maximum size of the `DynamicTable`, the
417 /// headers are correctly evicted in order to keep its size below the new
418 /// max.
419 #[test]
420 fn test_dynamic_table_change_max_size() {
421 let mut table = DynamicTable::new();
422 table.add_header(b"a".to_vec(), b"b".to_vec());
423 table.add_header(b"123".to_vec(), b"456".to_vec());
424 table.add_header(b"c".to_vec(), b"d".to_vec());
425 assert_eq!(3 * 32 + 2 + 6 + 2, table.get_size());
426
427 table.set_max_table_size(38);
428
429 assert_eq!(32 + 2, table.get_size());
430 assert_eq!(table.to_vec(), vec![(b"c".to_vec(), b"d".to_vec())]);
431 }
432
433 /// Tests that setting the maximum table size to 0 clears the dynamic
434 /// table.
435 #[test]
436 fn test_dynamic_table_clear() {
437 let mut table = DynamicTable::new();
438 table.add_header(b"a".to_vec(), b"b".to_vec());
439 table.add_header(b"123".to_vec(), b"456".to_vec());
440 table.add_header(b"c".to_vec(), b"d".to_vec());
441 assert_eq!(3 * 32 + 2 + 6 + 2, table.get_size());
442
443 table.set_max_table_size(0);
444
445 assert_eq!(0, table.len());
446 assert_eq!(0, table.to_vec().len());
447 assert_eq!(0, table.get_size());
448 assert_eq!(0, table.get_max_table_size());
449 }
450
451 /// Tests that when the initial max size of the table is 0, nothing
452 /// can be added to the table.
453 #[test]
454 fn test_dynamic_table_max_size_zero() {
455 let mut table = DynamicTable::with_size(0);
456
457 table.add_header(b"a".to_vec(), b"b".to_vec());
458
459 assert_eq!(0, table.len());
460 assert_eq!(0, table.to_vec().len());
461 assert_eq!(0, table.get_size());
462 assert_eq!(0, table.get_max_table_size());
463 }
464
465 /// Tests that the iterator through the `DynamicTable` works when there are
466 /// some elements in the dynamic table.
467 #[test]
468 fn test_dynamic_table_iter_with_elems() {
469 let mut table = DynamicTable::new();
470 table.add_header(b"a".to_vec(), b"b".to_vec());
471 table.add_header(b"123".to_vec(), b"456".to_vec());
472 table.add_header(b"c".to_vec(), b"d".to_vec());
473
474 let iter_res: Vec<(&[u8], &[u8])> = table.iter().collect();
475
476 let expected: Vec<(&[u8], &[u8])> = vec![(b"c", b"d"), (b"123", b"456"), (b"a", b"b")];
477 assert_eq!(iter_res, expected);
478 }
479
480 /// Tests that the iterator through the `DynamicTable` works when there are
481 /// no elements in the dynamic table.
482 #[test]
483 fn test_dynamic_table_iter_no_elems() {
484 let table = DynamicTable::new();
485
486 let iter_res: Vec<(&[u8], &[u8])> = table.iter().collect();
487
488 let expected = vec![];
489 assert_eq!(iter_res, expected);
490 }
491
492 /// Tests that indexing the header table with indices that correspond to
493 /// entries found in the static table works.
494 #[test]
495 fn test_header_table_index_static() {
496 let table = HeaderTable::with_static_table(STATIC_TABLE);
497
498 for (index, entry) in STATIC_TABLE.iter().enumerate() {
499 assert_eq!(table.get_from_table(index + 1).unwrap(), *entry);
500 }
501 }
502
503 /// Tests that when the given index is out of bounds, the `HeaderTable`
504 /// returns a `None`
505 #[test]
506 fn test_header_table_index_out_of_bounds() {
507 let table = HeaderTable::with_static_table(STATIC_TABLE);
508
509 assert!(table.get_from_table(0).is_none());
510 assert!(table.get_from_table(STATIC_TABLE.len() + 1).is_none());
511 }
512
513 /// Tests that adding entries to the dynamic table through the
514 /// `HeaderTable` interface works.
515 #[test]
516 fn test_header_table_add_to_dynamic() {
517 let mut table = HeaderTable::with_static_table(STATIC_TABLE);
518 let header = (b"a".to_vec(), b"b".to_vec());
519
520 table.add_header(header.0.clone(), header.1.clone());
521
522 assert_eq!(table.dynamic_table.to_vec(), vec![header]);
523 }
524
525 /// Tests that indexing the header table with indices that correspond to
526 /// entries found in the dynamic table works.
527 #[test]
528 fn test_header_table_index_dynamic() {
529 let mut table = HeaderTable::with_static_table(STATIC_TABLE);
530 let header = (b"a".to_vec(), b"b".to_vec());
531
532 table.add_header(header.0.clone(), header.1.clone());
533
534 assert_eq!(
535 table.get_from_table(STATIC_TABLE.len() + 1).unwrap(),
536 (&header.0[..], &header.1[..])
537 );
538 }
539
540 /// Tests that the `iter` method of the `HeaderTable` returns an iterator
541 /// through *all* the headers found in the header table (static and dynamic
542 /// tables both included)
543 #[test]
544 fn test_header_table_iter() {
545 let mut table = HeaderTable::with_static_table(STATIC_TABLE);
546 let headers: [(&[u8], &[u8]); 2] = [(b"a", b"b"), (b"c", b"d")];
547 for header in headers.iter() {
548 table.add_header(header.0.to_vec(), header.1.to_vec());
549 }
550
551 let iterated: Vec<(&[u8], &[u8])> = table.iter().collect();
552
553 assert_eq!(iterated.len(), headers.len() + STATIC_TABLE.len());
554 // Part of the static table correctly iterated through
555 for (h1, h2) in iterated.iter().zip(STATIC_TABLE.iter()) {
556 assert_eq!(h1, h2);
557 }
558 // Part of the dynamic table correctly iterated through: the elements
559 // are in reversed order of insertion in the dynamic table.
560 for (h1, h2) in iterated
561 .iter()
562 .skip(STATIC_TABLE.len())
563 .zip(headers.iter().rev())
564 {
565 assert_eq!(h1, h2);
566 }
567 }
568
569 /// Tests that searching for an entry in the header table, which should be
570 /// fully in the static table (both name and value), works correctly.
571 #[test]
572 fn test_find_header_static_full() {
573 let table = HeaderTable::with_static_table(STATIC_TABLE);
574
575 for (i, h) in STATIC_TABLE.iter().enumerate() {
576 assert_eq!(table.find_header(*h).unwrap(), (i + 1, true));
577 }
578 }
579
580 /// Tests that searching for an entry in the header table, which should be
581 /// only partially in the static table (only the name), works correctly.
582 #[test]
583 fn test_find_header_static_partial() {
584 {
585 let table = HeaderTable::with_static_table(STATIC_TABLE);
586 let h: (&[u8], &[u8]) = (b":method", b"PUT");
587
588 if let (index, false) = table.find_header(h).unwrap() {
589 assert_eq!(h.0, STATIC_TABLE[index - 1].0);
590 // The index is the last one with the corresponding name
591 assert_eq!(3, index);
592 } else {
593 panic!("The header should have matched only partially");
594 }
595 }
596 {
597 let table = HeaderTable::with_static_table(STATIC_TABLE);
598 let h: (&[u8], &[u8]) = (b":status", b"333");
599
600 if let (index, false) = table.find_header(h).unwrap() {
601 assert_eq!(h.0, STATIC_TABLE[index - 1].0);
602 // The index is the last one with the corresponding name
603 assert_eq!(14, index);
604 } else {
605 panic!("The header should have matched only partially");
606 }
607 }
608 {
609 let table = HeaderTable::with_static_table(STATIC_TABLE);
610 let h: (&[u8], &[u8]) = (b":authority", b"example.com");
611
612 if let (index, false) = table.find_header(h).unwrap() {
613 assert_eq!(h.0, STATIC_TABLE[index - 1].0);
614 } else {
615 panic!("The header should have matched only partially");
616 }
617 }
618 {
619 let table = HeaderTable::with_static_table(STATIC_TABLE);
620 let h: (&[u8], &[u8]) = (b"www-authenticate", b"asdf");
621
622 if let (index, false) = table.find_header(h).unwrap() {
623 assert_eq!(h.0, STATIC_TABLE[index - 1].0);
624 } else {
625 panic!("The header should have matched only partially");
626 }
627 }
628 }
629
630 /// Tests that searching for an entry in the header table, which should be
631 /// fully in the dynamic table (both name and value), works correctly.
632 #[test]
633 fn test_find_header_dynamic_full() {
634 let mut table = HeaderTable::with_static_table(STATIC_TABLE);
635 let h: (&[u8], &[u8]) = (b":method", b"PUT");
636 table.add_header(h.0.to_vec(), h.1.to_vec());
637
638 if let (index, true) = table.find_header(h).unwrap() {
639 assert_eq!(index, STATIC_TABLE.len() + 1);
640 } else {
641 panic!("The header should have matched fully");
642 }
643 }
644
645 /// Tests that searching for an entry in the header table, which should be
646 /// only partially in the dynamic table (only the name), works correctly.
647 #[test]
648 fn test_find_header_dynamic_partial() {
649 let mut table = HeaderTable::with_static_table(STATIC_TABLE);
650 // First add it to the dynamic table
651 {
652 let h = (b"X-Custom-Header", b"stuff");
653 table.add_header(h.0.to_vec(), h.1.to_vec());
654 }
655 // Prepare a search
656 let h: (&[u8], &[u8]) = (b"X-Custom-Header", b"different-stuff");
657
658 // It must match only partially
659 if let (index, false) = table.find_header(h).unwrap() {
660 // The index must be the first one in the dynamic table
661 // segment of the header table.
662 assert_eq!(index, STATIC_TABLE.len() + 1);
663 } else {
664 panic!("The header should have matched only partially");
665 }
666 }
667}