Coverage Report

Created: 2026-08-14 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata/rust/src/mime/mime.rs
Line
Count
Source
1
/* Copyright (C) 2024 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
use crate::common::nom8::take_until_and_consume;
19
use nom8::branch::alt;
20
use nom8::bytes::complete::{tag, take, take_till, take_until, take_while};
21
use nom8::character::complete::char;
22
use nom8::combinator::{complete, opt, rest, value};
23
use nom8::error::{make_error, ErrorKind};
24
use nom8::{Err, IResult, Parser};
25
use std;
26
use std::collections::HashMap;
27
28
#[derive(Clone)]
29
pub struct HeaderTokens<'a> {
30
    pub tokens: HashMap<&'a [u8], &'a [u8]>,
31
}
32
33
9.72M
fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> {
34
9.72M
    let (input, _) = char('"').parse(input)?;
35
74.4k
    let mut escaping = false;
36
16.9M
    for i in 0..input.len() {
37
16.9M
        if input[i] == b'\\' {
38
41.3k
            escaping = true;
39
41.3k
        } else {
40
16.8M
            if input[i] == b'"' && !escaping {
41
66.6k
                return Ok((&input[i + 1..], &input[..i]));
42
16.8M
            }
43
            // unescape can be processed later
44
16.8M
            escaping = false;
45
        }
46
    }
47
    // should fail
48
7.82k
    let (input, value) = take_until("\"").parse(input)?;
49
1.55k
    let (input, _) = char('"').parse(input)?;
50
1.55k
    return Ok((input, value));
51
9.72M
}
52
53
9.66M
fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> {
54
58.1M
    let (input, value) = alt((take_till(|ch: u8| ch == b';'), rest)).parse(input)?;
55
9.66M
    for i in 0..value.len() {
56
2.63M
        if !is_mime_space(value[value.len() - i - 1]) {
57
2.61M
            return Ok((input, &value[..value.len() - i]));
58
21.4k
        }
59
    }
60
7.04M
    return Ok((input, value));
61
9.66M
}
62
63
#[inline]
64
23.7M
fn is_mime_space(ch: u8) -> bool {
65
23.7M
    ch == 0x20 || ch == 0x09 || ch == 0x0a || ch == 0x0d
66
23.7M
}
67
68
9.85M
pub fn mime_parse_header_token(input: &[u8]) -> IResult<&[u8], (&'_ [u8], &'_ [u8])> {
69
    // from RFC2047 : like ch.is_ascii_whitespace but without 0x0c FORM-FEED
70
9.85M
    let (input, _) = take_while(is_mime_space).parse(input)?;
71
9.85M
    let (input, name) = take_until("=").parse(input)?;
72
9.72M
    let (input, _) = char('=').parse(input)?;
73
9.72M
    let (input, value) =
74
9.72M
        alt((mime_parse_value_delimited, mime_parse_value_until_semicolon)).parse(input)?;
75
9.72M
    let (input, _) = take_while(is_mime_space).parse(input)?;
76
9.72M
    let (input, _) = opt(complete(char(';'))).parse(input)?;
77
9.72M
    return Ok((input, (name, value)));
78
9.85M
}
79
80
2.56M
fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], HeaderTokens<'_>> {
81
2.56M
    let (mut input, _) = take_until_and_consume(b";").parse(input)?;
82
2.49M
    let mut tokens = HashMap::new();
83
12.2M
    while !input.is_empty() {
84
9.85M
        match mime_parse_header_token(input) {
85
9.72M
            Ok((rem, t)) => {
86
9.72M
                tokens.insert(t.0, t.1);
87
                // should never happen
88
9.72M
                debug_validate_bug_on!(input.len() == rem.len());
89
9.72M
                if input.len() == rem.len() {
90
                    //infinite loop
91
0
                    return Err(Err::Error(make_error(input, ErrorKind::Eof)));
92
9.72M
                }
93
9.72M
                input = rem;
94
            }
95
            Err(_) => {
96
                // keep first tokens is error in remaining buffer
97
122k
                break;
98
            }
99
        }
100
    }
101
2.49M
    return Ok((input, HeaderTokens { tokens }));
102
2.56M
}
103
104
2.56M
pub fn mime_find_header_token<'a>(
105
2.56M
    header: &'a [u8], token: &[u8], sections_values: &'a mut Vec<u8>,
106
2.56M
) -> Option<&'a [u8]> {
107
2.56M
    match mime_parse_header_tokens(header) {
108
2.49M
        Ok((_rem, t)) => {
109
            // in case of multiple sections for the parameter cf RFC2231
110
2.49M
            let mut current_section_slice = Vec::new();
111
112
            // look for the specific token
113
2.49M
            match t.tokens.get(token) {
114
                // easy nominal case
115
910k
                Some(value) => return Some(value),
116
                None => {
117
                    // check for initial section of a parameter
118
1.58M
                    current_section_slice.extend_from_slice(token);
119
1.58M
                    current_section_slice.extend_from_slice(b"*0");
120
                    {
121
1.58M
                        let value = t.tokens.get(&current_section_slice[..])?;
122
207k
                        sections_values.extend_from_slice(value);
123
207k
                        let l = current_section_slice.len();
124
207k
                        current_section_slice[l - 1] = b'1';
125
                    }
126
                }
127
            }
128
129
207k
            let mut current_section_seen = 1;
130
            // we have at least the initial section
131
            // try looping until we do not find anymore a next section
132
            loop {
133
516k
                match t.tokens.get(&current_section_slice[..]) {
134
309k
                    Some(value) => {
135
309k
                        sections_values.extend_from_slice(value);
136
309k
                        current_section_seen += 1;
137
309k
                        let nbdigits = current_section_slice.len() - token.len() - 1;
138
309k
                        current_section_slice.truncate(current_section_slice.len() - nbdigits);
139
309k
                        current_section_slice
140
309k
                            .extend_from_slice(current_section_seen.to_string().as_bytes());
141
309k
                    }
142
207k
                    None => return Some(sections_values),
143
                }
144
            }
145
        }
146
        Err(_) => {
147
69.3k
            return None;
148
        }
149
    }
150
2.56M
}
151
152
pub(crate) const RS_MIME_MAX_TOKEN_LEN: usize = 255;
153
154
#[derive(Debug)]
155
enum MimeParserState {
156
    Start,
157
    Header,
158
    HeaderEnd,
159
    Chunk,
160
    BoundaryWaitingForEol,
161
}
162
163
impl Default for MimeParserState {
164
11.0k
    fn default() -> Self {
165
11.0k
        MimeParserState::Start
166
11.0k
    }
167
}
168
169
#[derive(Debug, Default)]
170
pub struct MimeStateHTTP {
171
    boundary: Vec<u8>,
172
    filename: Vec<u8>,
173
    state: MimeParserState,
174
}
175
176
#[repr(u8)]
177
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
178
pub enum MimeParserResult {
179
    MimeNeedsMore = 0,
180
    MimeFileOpen = 1,
181
    MimeFileChunk = 2,
182
    MimeFileClose = 3,
183
}
184
185
48.1k
fn mime_parse_skip_line(input: &[u8]) -> IResult<&[u8], MimeParserState> {
186
3.77M
    let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
187
48.1k
    let (input, _) = char('\n')(input)?;
188
42.9k
    return Ok((input, MimeParserState::Start));
189
48.1k
}
190
191
70.9k
fn mime_parse_boundary_regular<'a>(
192
70.9k
    boundary: &[u8], input: &'a [u8],
193
70.9k
) -> IResult<&'a [u8], MimeParserState> {
194
70.9k
    let (input, _) = tag(boundary)(input)?;
195
1.33M
    let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
196
26.2k
    let (input, _) = char('\n')(input)?;
197
25.1k
    return Ok((input, MimeParserState::Header));
198
70.9k
}
199
200
// Number of characters after boundary, without end of line, before changing state to streaming
201
const MIME_BOUNDARY_MAX_BEFORE_EOL: usize = 128;
202
const MIME_HEADER_MAX_LINE: usize = 4096;
203
204
3.28k
fn mime_parse_boundary_missing_eol<'a>(
205
3.28k
    boundary: &[u8], input: &'a [u8],
206
3.28k
) -> IResult<&'a [u8], MimeParserState> {
207
3.28k
    let (input, _) = tag(boundary)(input)?;
208
1.10k
    let (input, _) = take(MIME_BOUNDARY_MAX_BEFORE_EOL)(input)?;
209
541
    return Ok((input, MimeParserState::BoundaryWaitingForEol));
210
3.28k
}
211
212
70.9k
fn mime_parse_boundary<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], MimeParserState> {
213
70.9k
    let r = mime_parse_boundary_regular(boundary, input);
214
70.9k
    if r.is_ok() {
215
25.1k
        return r;
216
45.8k
    }
217
45.8k
    let r2 = mime_parse_skip_line(input);
218
45.8k
    if r2.is_ok() {
219
42.5k
        return r2;
220
3.28k
    }
221
3.28k
    return mime_parse_boundary_missing_eol(boundary, input);
222
70.9k
}
223
224
2.27k
fn mime_consume_until_eol(input: &[u8]) -> IResult<&[u8], bool> {
225
2.27k
    return alt((value(true, mime_parse_skip_line), value(false, rest))).parse(input);
226
2.27k
}
227
228
10.1M
pub fn mime_parse_header_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
229
46.5M
    let (input, name) = take_till(|ch: u8| ch == b':').parse(input)?;
230
10.1M
    let (input, _) = char(':').parse(input)?;
231
9.76M
    let (input, _) = take_while(is_mime_space).parse(input)?;
232
9.76M
    return Ok((input, name));
233
10.1M
}
234
235
// s2 is already lower case
236
25.7M
pub fn slice_equals_lowercase(s1: &[u8], s2: &[u8]) -> bool {
237
25.7M
    if s1.len() == s2.len() {
238
20.1M
        for i in 0..s1.len() {
239
20.1M
            if s1[i].to_ascii_lowercase() != s2[i] {
240
325k
                return false;
241
19.8M
            }
242
        }
243
1.42M
        return true;
244
23.9M
    }
245
23.9M
    return false;
246
25.7M
}
247
248
163k
fn mime_parse_headers<'a>(
249
163k
    ctx: &mut MimeStateHTTP, i: &'a [u8],
250
163k
) -> IResult<&'a [u8], (MimeParserState, bool, bool)> {
251
163k
    let mut fileopen = false;
252
163k
    let mut errored = false;
253
163k
    let mut input = i;
254
222k
    while !input.is_empty() {
255
79.4k
        if let Ok((input2, line)) =
256
175k
            take_until::<_, &[u8], nom8::error::Error<&[u8]>>("\r\n").parse(input)
257
        {
258
79.4k
            if let Ok((value, name)) = mime_parse_header_line(line) {
259
31.2k
                if slice_equals_lowercase(name, "content-disposition".as_bytes()) {
260
9.92k
                    let mut sections_values = Vec::new();
261
7.11k
                    if let Some(filename) =
262
9.92k
                        mime_find_header_token(value, "filename".as_bytes(), &mut sections_values)
263
                    {
264
7.11k
                        if !filename.is_empty() {
265
7.10k
                            ctx.filename = Vec::with_capacity(filename.len());
266
7.10k
                            fileopen = true;
267
213k
                            for c in filename {
268
                                // unescape
269
206k
                                if *c != b'\\' {
270
206k
                                    ctx.filename.push(*c);
271
206k
                                }
272
                            }
273
7
                        }
274
2.80k
                    }
275
21.2k
                }
276
31.2k
                if value.is_empty() {
277
7.19k
                    errored = true;
278
24.0k
                }
279
48.2k
            } else if !line.is_empty() {
280
33.7k
                errored = true;
281
33.7k
            }
282
79.4k
            let (input3, _) = tag("\r\n")(input2)?;
283
79.4k
            input = input3;
284
79.4k
            if line.is_empty() || (line.len() == 1 && line[0] == b'\r') {
285
21.2k
                return Ok((input, (MimeParserState::HeaderEnd, fileopen, errored)));
286
58.2k
            }
287
        } else {
288
            // guard against too long header lines
289
96.5k
            if input.len() > MIME_HEADER_MAX_LINE {
290
0
                return Ok((
291
0
                    input,
292
0
                    (MimeParserState::BoundaryWaitingForEol, fileopen, errored),
293
0
                ));
294
96.5k
            }
295
96.5k
            if input.len() < i.len() {
296
1.66k
                return Ok((input, (MimeParserState::Header, fileopen, errored)));
297
94.8k
            } // else only an incomplete line, ask for more
298
94.8k
            return Err(Err::Error(make_error(input, ErrorKind::Eof)));
299
        }
300
    }
301
46.0k
    return Ok((input, (MimeParserState::Header, fileopen, errored)));
302
163k
}
303
304
type NomTakeError<'a> = Err<nom8::error::Error<&'a [u8]>>;
305
306
152k
fn mime_consume_chunk<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], bool> {
307
152k
    let r: Result<(&[u8], &[u8]), NomTakeError> = take_until("\r\n").parse(input);
308
152k
    if let Ok((input, line)) = r {
309
113k
        let (next_line, _) = tag("\r\n").parse(input)?;
310
113k
        if next_line.len() < boundary.len() {
311
76.4k
            if next_line == &boundary[..next_line.len()] {
312
68.2k
                if !line.is_empty() {
313
                    // consume as chunk up to eol (not consuming eol)
314
25.0k
                    return Ok((input, false));
315
43.1k
                }
316
                // new line beignning like boundary, with nothin to consume as chunk : request more
317
43.1k
                return Err(Err::Error(make_error(input, ErrorKind::Eof)));
318
8.20k
            }
319
            // not like boundary : consume everything as chunk
320
8.20k
            return Ok((&input[input.len()..], false));
321
37.1k
        } // else
322
37.1k
        if &next_line[..boundary.len()] == boundary {
323
            // end of file with boundary, consume eol but do not consume boundary
324
11.8k
            return Ok((next_line, true));
325
25.3k
        }
326
        // not like boundary : consume everything as chunk
327
25.3k
        return Ok((next_line, false));
328
    } else {
329
39.1k
        return Ok((&input[input.len()..], false));
330
    }
331
152k
}
332
333
pub const MIME_EVENT_FLAG_INVALID_HEADER: u32 = 0x01;
334
pub const MIME_EVENT_FLAG_NO_FILEDATA: u32 = 0x02;
335
336
356k
fn mime_process(ctx: &mut MimeStateHTTP, i: &[u8]) -> (MimeParserResult, u32, u32) {
337
356k
    let mut input = i;
338
356k
    let mut consumed = 0;
339
356k
    let mut warnings = 0;
340
522k
    while !input.is_empty() {
341
410k
        match ctx.state {
342
            MimeParserState::Start => {
343
70.9k
                if let Ok((rem, next)) = mime_parse_boundary(&ctx.boundary, input) {
344
68.2k
                    ctx.state = next;
345
68.2k
                    consumed += (input.len() - rem.len()) as u32;
346
68.2k
                    input = rem;
347
68.2k
                } else {
348
2.74k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
349
                }
350
            }
351
            MimeParserState::BoundaryWaitingForEol => {
352
2.27k
                if let Ok((rem, found)) = mime_consume_until_eol(input) {
353
2.27k
                    if found {
354
385
                        ctx.state = MimeParserState::Header;
355
1.88k
                    }
356
2.27k
                    consumed += (input.len() - rem.len()) as u32;
357
2.27k
                    input = rem;
358
                } else {
359
                    // should never happen
360
0
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
361
                }
362
            }
363
            MimeParserState::Header => {
364
163k
                if let Ok((rem, (next, fileopen, err))) = mime_parse_headers(ctx, input) {
365
68.9k
                    ctx.state = next;
366
68.9k
                    consumed += (input.len() - rem.len()) as u32;
367
68.9k
                    input = rem;
368
68.9k
                    if err {
369
37.5k
                        warnings |= MIME_EVENT_FLAG_INVALID_HEADER;
370
37.5k
                    }
371
68.9k
                    if fileopen {
372
7.08k
                        return (MimeParserResult::MimeFileOpen, consumed, warnings);
373
61.8k
                    }
374
                } else {
375
94.8k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
376
                }
377
            }
378
            MimeParserState::HeaderEnd => {
379
                // check if we start with the boundary
380
                // and transition to chunk, or empty file and back to start
381
21.3k
                if input.len() < ctx.boundary.len() {
382
1.25k
                    if input == &ctx.boundary[..input.len()] {
383
201
                        return (MimeParserResult::MimeNeedsMore, consumed, warnings);
384
1.05k
                    }
385
1.05k
                    ctx.state = MimeParserState::Chunk;
386
20.0k
                } else if input[..ctx.boundary.len()] == ctx.boundary {
387
7.69k
                    ctx.state = MimeParserState::Start;
388
7.69k
                    if !ctx.filename.is_empty() {
389
2.99k
                        warnings |= MIME_EVENT_FLAG_NO_FILEDATA;
390
4.70k
                    }
391
7.69k
                    ctx.filename.clear();
392
7.69k
                    return (MimeParserResult::MimeFileClose, consumed, warnings);
393
12.3k
                } else {
394
12.3k
                    ctx.state = MimeParserState::Chunk;
395
12.3k
                }
396
            }
397
            MimeParserState::Chunk => {
398
152k
                if let Ok((rem, eof)) = mime_consume_chunk(&ctx.boundary, input) {
399
109k
                    consumed += (input.len() - rem.len()) as u32;
400
109k
                    if eof {
401
11.8k
                        ctx.state = MimeParserState::Start;
402
11.8k
                        ctx.filename.clear();
403
11.8k
                        return (MimeParserResult::MimeFileClose, consumed, warnings);
404
                    } else {
405
                        // + 2 for \r\n
406
97.7k
                        if rem.len() < ctx.boundary.len() + 2 {
407
77.3k
                            return (MimeParserResult::MimeFileChunk, consumed, warnings);
408
20.3k
                        }
409
20.3k
                        input = rem;
410
                    }
411
                } else {
412
43.1k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
413
                }
414
            }
415
        }
416
    }
417
111k
    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
418
356k
}
419
420
26.0k
pub fn mime_state_init(i: &[u8]) -> Option<MimeStateHTTP> {
421
26.0k
    let mut sections_values = Vec::new();
422
26.0k
    if let Some(value) = mime_find_header_token(i, "boundary".as_bytes(), &mut sections_values) {
423
11.1k
        if value.len() <= RS_MIME_MAX_TOKEN_LEN {
424
11.0k
            let mut r = MimeStateHTTP {
425
11.0k
                boundary: Vec::with_capacity(2 + value.len()),
426
11.0k
                ..Default::default()
427
11.0k
            };
428
            // start wih 2 additional hyphens
429
11.0k
            r.boundary.push(b'-');
430
11.0k
            r.boundary.push(b'-');
431
104k
            for c in value {
432
                // unescape
433
93.6k
                if *c != b'\\' {
434
93.1k
                    r.boundary.push(*c);
435
93.1k
                }
436
            }
437
11.0k
            return Some(r);
438
38
        }
439
14.9k
    }
440
14.9k
    return None;
441
26.0k
}
442
443
#[no_mangle]
444
26.0k
pub unsafe extern "C" fn SCMimeStateInit(input: *const u8, input_len: u32) -> *mut MimeStateHTTP {
445
26.0k
    let slice = build_slice!(input, input_len as usize);
446
447
26.0k
    if let Some(ctx) = mime_state_init(slice) {
448
11.0k
        let boxed = Box::new(ctx);
449
11.0k
        return Box::into_raw(boxed) as *mut _;
450
14.9k
    }
451
14.9k
    return std::ptr::null_mut();
452
26.0k
}
453
454
#[no_mangle]
455
356k
pub unsafe extern "C" fn SCMimeParse(
456
356k
    ctx: &mut MimeStateHTTP, input: *const u8, input_len: u32, consumed: *mut u32,
457
356k
    warnings: *mut u32,
458
356k
) -> MimeParserResult {
459
356k
    let slice = build_slice!(input, input_len as usize);
460
356k
    let (r, c, w) = mime_process(ctx, slice);
461
356k
    *consumed = c;
462
356k
    *warnings = w;
463
356k
    return r;
464
356k
}
465
466
#[no_mangle]
467
7.08k
pub unsafe extern "C" fn SCMimeStateGetFilename(
468
7.08k
    ctx: &mut MimeStateHTTP, buffer: *mut *const u8, filename_len: *mut u16,
469
7.08k
) {
470
7.08k
    if !ctx.filename.is_empty() {
471
7.04k
        *buffer = ctx.filename.as_ptr();
472
7.04k
        if ctx.filename.len() < usize::from(u16::MAX) {
473
7.04k
            *filename_len = ctx.filename.len() as u16;
474
7.04k
        } else {
475
0
            *filename_len = u16::MAX;
476
0
        }
477
34
    } else {
478
34
        *buffer = std::ptr::null_mut();
479
34
        *filename_len = 0;
480
34
    }
481
7.08k
}
482
483
#[no_mangle]
484
11.0k
pub unsafe extern "C" fn SCMimeStateFree(ctx: &mut MimeStateHTTP) {
485
11.0k
    std::mem::drop(Box::from_raw(ctx));
486
11.0k
}
487
488
#[cfg(test)]
489
mod test {
490
    use super::*;
491
492
    #[test]
493
    fn test_mime_find_header_token() {
494
        let mut outvec = Vec::new();
495
        let undelimok = mime_find_header_token(
496
            "attachment; filename=test;".as_bytes(),
497
            "filename".as_bytes(),
498
            &mut outvec,
499
        );
500
        assert_eq!(undelimok, Some("test".as_bytes()));
501
502
        let delimok = mime_find_header_token(
503
            "attachment; filename=\"test2\";".as_bytes(),
504
            "filename".as_bytes(),
505
            &mut outvec,
506
        );
507
        assert_eq!(delimok, Some("test2".as_bytes()));
508
509
        let escaped = mime_find_header_token(
510
            "attachment; filename=\"test\\\"2\";".as_bytes(),
511
            "filename".as_bytes(),
512
            &mut outvec,
513
        );
514
        assert_eq!(escaped, Some("test\\\"2".as_bytes()));
515
516
        let evasion_othertoken = mime_find_header_token(
517
            "attachment; dummy=\"filename=wrong\"; filename=real;".as_bytes(),
518
            "filename".as_bytes(),
519
            &mut outvec,
520
        );
521
        assert_eq!(evasion_othertoken, Some("real".as_bytes()));
522
523
        let evasion_suffixtoken = mime_find_header_token(
524
            "attachment; notafilename=wrong; filename=good;".as_bytes(),
525
            "filename".as_bytes(),
526
            &mut outvec,
527
        );
528
        assert_eq!(evasion_suffixtoken, Some("good".as_bytes()));
529
530
        let badending = mime_find_header_token(
531
            "attachment; filename=oksofar; badending".as_bytes(),
532
            "filename".as_bytes(),
533
            &mut outvec,
534
        );
535
        assert_eq!(badending, Some("oksofar".as_bytes()));
536
537
        let missend = mime_find_header_token(
538
            "attachment; filename=test".as_bytes(),
539
            "filename".as_bytes(),
540
            &mut outvec,
541
        );
542
        assert_eq!(missend, Some("test".as_bytes()));
543
544
        let spaces = mime_find_header_token(
545
            "attachment; filename=test me wrong".as_bytes(),
546
            "filename".as_bytes(),
547
            &mut outvec,
548
        );
549
        assert_eq!(spaces, Some("test me wrong".as_bytes()));
550
551
        assert_eq!(outvec.len(), 0);
552
        let multi = mime_find_header_token(
553
            "attachment; filename*0=abc; filename*1=\"def\";".as_bytes(),
554
            "filename".as_bytes(),
555
            &mut outvec,
556
        );
557
        assert_eq!(multi, Some("abcdef".as_bytes()));
558
        outvec.clear();
559
560
        let multi = mime_find_header_token(
561
            "attachment; filename*1=456; filename*0=\"123\"".as_bytes(),
562
            "filename".as_bytes(),
563
            &mut outvec,
564
        );
565
        assert_eq!(multi, Some("123456".as_bytes()));
566
        outvec.clear();
567
    }
568
}