Coverage Report

Created: 2025-11-16 07:09

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
7.11M
fn mime_parse_value_delimited(input: &[u8]) -> IResult<&[u8], &[u8]> {
34
7.11M
    let (input, _) = char('"').parse(input)?;
35
21.1k
    let mut escaping = false;
36
13.2M
    for i in 0..input.len() {
37
13.2M
        if input[i] == b'\\' {
38
20.1k
            escaping = true;
39
20.1k
        } else {
40
13.2M
            if input[i] == b'"' && !escaping {
41
12.6k
                return Ok((&input[i + 1..], &input[..i]));
42
13.2M
            }
43
            // unescape can be processed later
44
13.2M
            escaping = false;
45
        }
46
    }
47
    // should fail
48
8.51k
    let (input, value) = take_until("\"").parse(input)?;
49
1.27k
    let (input, _) = char('"').parse(input)?;
50
1.27k
    return Ok((input, value));
51
7.11M
}
52
53
7.09M
fn mime_parse_value_until_semicolon(input: &[u8]) -> IResult<&[u8], &[u8]> {
54
53.9M
    let (input, value) = alt((take_till(|ch: u8| ch == b';'), rest)).parse(input)?;
55
7.09M
    for i in 0..value.len() {
56
2.45M
        if !is_mime_space(value[value.len() - i - 1]) {
57
2.43M
            return Ok((input, &value[..value.len() - i]));
58
21.5k
        }
59
    }
60
4.66M
    return Ok((input, value));
61
7.09M
}
62
63
#[inline]
64
17.7M
fn is_mime_space(ch: u8) -> bool {
65
17.7M
    ch == 0x20 || ch == 0x09 || ch == 0x0a || ch == 0x0d
66
17.7M
}
67
68
7.22M
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
7.22M
    let (input, _) = take_while(is_mime_space).parse(input)?;
71
7.22M
    let (input, name) = take_until("=").parse(input)?;
72
7.11M
    let (input, _) = char('=').parse(input)?;
73
7.11M
    let (input, value) =
74
7.11M
        alt((mime_parse_value_delimited, mime_parse_value_until_semicolon)).parse(input)?;
75
7.11M
    let (input, _) = take_while(is_mime_space).parse(input)?;
76
7.11M
    let (input, _) = opt(complete(char(';'))).parse(input)?;
77
7.11M
    return Ok((input, (name, value)));
78
7.22M
}
79
80
2.19M
fn mime_parse_header_tokens(input: &[u8]) -> IResult<&[u8], HeaderTokens<'_>> {
81
2.19M
    let (mut input, _) = take_until_and_consume(b";").parse(input)?;
82
1.97M
    let mut tokens = HashMap::new();
83
9.08M
    while !input.is_empty() {
84
7.22M
        match mime_parse_header_token(input) {
85
7.11M
            Ok((rem, t)) => {
86
7.11M
                tokens.insert(t.0, t.1);
87
                // should never happen
88
7.11M
                debug_validate_bug_on!(input.len() == rem.len());
89
7.11M
                if input.len() == rem.len() {
90
                    //infinite loop
91
0
                    return Err(Err::Error(make_error(input, ErrorKind::Eof)));
92
7.11M
                }
93
7.11M
                input = rem;
94
            }
95
            Err(_) => {
96
                // keep first tokens is error in remaining buffer
97
110k
                break;
98
            }
99
        }
100
    }
101
1.97M
    return Ok((input, HeaderTokens { tokens }));
102
2.19M
}
103
104
2.19M
pub fn mime_find_header_token<'a>(
105
2.19M
    header: &'a [u8], token: &[u8], sections_values: &'a mut Vec<u8>,
106
2.19M
) -> Option<&'a [u8]> {
107
2.19M
    match mime_parse_header_tokens(header) {
108
1.97M
        Ok((_rem, t)) => {
109
            // in case of multiple sections for the parameter cf RFC2231
110
1.97M
            let mut current_section_slice = Vec::new();
111
112
            // look for the specific token
113
1.97M
            match t.tokens.get(token) {
114
                // easy nominal case
115
675k
                Some(value) => return Some(value),
116
                None => {
117
                    // check for initial section of a parameter
118
1.29M
                    current_section_slice.extend_from_slice(token);
119
1.29M
                    current_section_slice.extend_from_slice(b"*0");
120
1.29M
                    match t.tokens.get(&current_section_slice[..]) {
121
191k
                        Some(value) => {
122
191k
                            sections_values.extend_from_slice(value);
123
191k
                            let l = current_section_slice.len();
124
191k
                            current_section_slice[l - 1] = b'1';
125
191k
                        }
126
1.10M
                        None => return None,
127
                    }
128
                }
129
            }
130
131
191k
            let mut current_section_seen = 1;
132
            // we have at least the initial section
133
            // try looping until we do not find anymore a next section
134
            loop {
135
541k
                match t.tokens.get(&current_section_slice[..]) {
136
350k
                    Some(value) => {
137
350k
                        sections_values.extend_from_slice(value);
138
350k
                        current_section_seen += 1;
139
350k
                        let nbdigits = current_section_slice.len() - token.len() - 1;
140
350k
                        current_section_slice.truncate(current_section_slice.len() - nbdigits);
141
350k
                        current_section_slice
142
350k
                            .extend_from_slice(current_section_seen.to_string().as_bytes());
143
350k
                    }
144
191k
                    None => return Some(sections_values),
145
                }
146
            }
147
        }
148
        Err(_) => {
149
223k
            return None;
150
        }
151
    }
152
2.19M
}
153
154
pub(crate) const RS_MIME_MAX_TOKEN_LEN: usize = 255;
155
156
#[derive(Debug)]
157
enum MimeParserState {
158
    Start,
159
    Header,
160
    HeaderEnd,
161
    Chunk,
162
    BoundaryWaitingForEol,
163
}
164
165
impl Default for MimeParserState {
166
10.6k
    fn default() -> Self {
167
10.6k
        MimeParserState::Start
168
10.6k
    }
169
}
170
171
#[derive(Debug, Default)]
172
pub struct MimeStateHTTP {
173
    boundary: Vec<u8>,
174
    filename: Vec<u8>,
175
    state: MimeParserState,
176
}
177
178
#[repr(u8)]
179
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
180
pub enum MimeParserResult {
181
    MimeNeedsMore = 0,
182
    MimeFileOpen = 1,
183
    MimeFileChunk = 2,
184
    MimeFileClose = 3,
185
}
186
187
48.9k
fn mime_parse_skip_line(input: &[u8]) -> IResult<&[u8], MimeParserState> {
188
1.81M
    let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
189
48.9k
    let (input, _) = char('\n')(input)?;
190
45.0k
    return Ok((input, MimeParserState::Start));
191
48.9k
}
192
193
60.0k
fn mime_parse_boundary_regular<'a>(
194
60.0k
    boundary: &[u8], input: &'a [u8],
195
60.0k
) -> IResult<&'a [u8], MimeParserState> {
196
60.0k
    let (input, _) = tag(boundary)(input)?;
197
691k
    let (input, _) = take_till(|ch: u8| ch == b'\n')(input)?;
198
12.8k
    let (input, _) = char('\n')(input)?;
199
11.8k
    return Ok((input, MimeParserState::Header));
200
60.0k
}
201
202
// Number of characters after boundary, without end of line, before changing state to streaming
203
const MIME_BOUNDARY_MAX_BEFORE_EOL: usize = 128;
204
const MIME_HEADER_MAX_LINE: usize = 4096;
205
206
3.20k
fn mime_parse_boundary_missing_eol<'a>(
207
3.20k
    boundary: &[u8], input: &'a [u8],
208
3.20k
) -> IResult<&'a [u8], MimeParserState> {
209
3.20k
    let (input, _) = tag(boundary)(input)?;
210
1.03k
    let (input, _) = take(MIME_BOUNDARY_MAX_BEFORE_EOL)(input)?;
211
154
    return Ok((input, MimeParserState::BoundaryWaitingForEol));
212
3.20k
}
213
214
60.0k
fn mime_parse_boundary<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], MimeParserState> {
215
60.0k
    let r = mime_parse_boundary_regular(boundary, input);
216
60.0k
    if r.is_ok() {
217
11.8k
        return r;
218
48.2k
    }
219
48.2k
    let r2 = mime_parse_skip_line(input);
220
48.2k
    if r2.is_ok() {
221
45.0k
        return r2;
222
3.20k
    }
223
3.20k
    return mime_parse_boundary_missing_eol(boundary, input);
224
60.0k
}
225
226
750
fn mime_consume_until_eol(input: &[u8]) -> IResult<&[u8], bool> {
227
750
    return alt((value(true, mime_parse_skip_line), value(false, rest))).parse(input);
228
750
}
229
230
8.74M
pub fn mime_parse_header_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
231
44.2M
    let (input, name) = take_till(|ch: u8| ch == b':').parse(input)?;
232
8.74M
    let (input, _) = char(':').parse(input)?;
233
8.43M
    let (input, _) = take_while(is_mime_space).parse(input)?;
234
8.43M
    return Ok((input, name));
235
8.74M
}
236
237
// s2 is already lower case
238
22.7M
pub fn slice_equals_lowercase(s1: &[u8], s2: &[u8]) -> bool {
239
22.7M
    if s1.len() == s2.len() {
240
16.3M
        for i in 0..s1.len() {
241
16.3M
            if s1[i].to_ascii_lowercase() != s2[i] {
242
284k
                return false;
243
16.1M
            }
244
        }
245
1.19M
        return true;
246
21.2M
    }
247
21.2M
    return false;
248
22.7M
}
249
250
174k
fn mime_parse_headers<'a>(
251
174k
    ctx: &mut MimeStateHTTP, i: &'a [u8],
252
174k
) -> IResult<&'a [u8], (MimeParserState, bool, bool)> {
253
174k
    let mut fileopen = false;
254
174k
    let mut errored = false;
255
174k
    let mut input = i;
256
240k
    while !input.is_empty() {
257
200k
        if let Ok((input2, line)) = take_until::<_, &[u8], nom8::error::Error<&[u8]>>("\r\n").parse(input)
258
        {
259
73.7k
            if let Ok((value, name)) = mime_parse_header_line(line) {
260
49.6k
                if slice_equals_lowercase(name, "content-disposition".as_bytes()) {
261
12.7k
                    let mut sections_values = Vec::new();
262
8.20k
                    if let Some(filename) =
263
12.7k
                        mime_find_header_token(value, "filename".as_bytes(), &mut sections_values)
264
                    {
265
8.20k
                        if !filename.is_empty() {
266
8.20k
                            ctx.filename = Vec::with_capacity(filename.len());
267
8.20k
                            fileopen = true;
268
507k
                            for c in filename {
269
                                // unescape
270
499k
                                if *c != b'\\' {
271
497k
                                    ctx.filename.push(*c);
272
497k
                                }
273
                            }
274
0
                        }
275
4.52k
                    }
276
36.9k
                }
277
49.6k
                if value.is_empty() {
278
14.2k
                    errored = true;
279
35.4k
                }
280
24.1k
            } else if !line.is_empty() {
281
16.8k
                errored = true;
282
16.8k
            }
283
73.7k
            let (input3, _) = tag("\r\n")(input2)?;
284
73.7k
            input = input3;
285
73.7k
            if line.is_empty() || (line.len() == 1 && line[0] == b'\r') {
286
8.01k
                return Ok((input, (MimeParserState::HeaderEnd, fileopen, errored)));
287
65.7k
            }
288
        } else {
289
            // guard against too long header lines
290
126k
            if input.len() > MIME_HEADER_MAX_LINE {
291
0
                return Ok((
292
0
                    input,
293
0
                    (MimeParserState::BoundaryWaitingForEol, fileopen, errored),
294
0
                ));
295
126k
            }
296
126k
            if input.len() < i.len() {
297
3.00k
                return Ok((input, (MimeParserState::Header, fileopen, errored)));
298
123k
            } // else only an incomplete line, ask for more
299
123k
            return Err(Err::Error(make_error(input, ErrorKind::Eof)));
300
        }
301
    }
302
40.3k
    return Ok((input, (MimeParserState::Header, fileopen, errored)));
303
174k
}
304
305
type NomTakeError<'a> = Err<nom8::error::Error<&'a [u8]>>;
306
307
46.0k
fn mime_consume_chunk<'a>(boundary: &[u8], input: &'a [u8]) -> IResult<&'a [u8], bool> {
308
46.0k
    let r: Result<(&[u8], &[u8]), NomTakeError> = take_until("\r\n").parse(input);
309
46.0k
    if let Ok((input, line)) = r {
310
32.8k
        let (next_line, _) = tag("\r\n").parse(input)?;
311
32.8k
        if next_line.len() < boundary.len() {
312
18.6k
            if next_line == &boundary[..next_line.len()] {
313
16.4k
                if !line.is_empty() {
314
                    // consume as chunk up to eol (not consuming eol)
315
6.14k
                    return Ok((input, false));
316
10.2k
                }
317
                // new line beignning like boundary, with nothin to consume as chunk : request more
318
10.2k
                return Err(Err::Error(make_error(input, ErrorKind::Eof)));
319
2.19k
            }
320
            // not like boundary : consume everything as chunk
321
2.19k
            return Ok((&input[input.len()..], false));
322
14.1k
        } // else
323
14.1k
        if &next_line[..boundary.len()] == boundary {
324
            // end of file with boundary, consume eol but do not consume boundary
325
2.95k
            return Ok((next_line, true));
326
11.2k
        }
327
        // not like boundary : consume everything as chunk
328
11.2k
        return Ok((next_line, false));
329
    } else {
330
13.2k
        return Ok((&input[input.len()..], false));
331
    }
332
46.0k
}
333
334
pub const MIME_EVENT_FLAG_INVALID_HEADER: u32 = 0x01;
335
pub const MIME_EVENT_FLAG_NO_FILEDATA: u32 = 0x02;
336
337
245k
fn mime_process(ctx: &mut MimeStateHTTP, i: &[u8]) -> (MimeParserResult, u32, u32) {
338
245k
    let mut input = i;
339
245k
    let mut consumed = 0;
340
245k
    let mut warnings = 0;
341
359k
    while !input.is_empty() {
342
289k
        match ctx.state {
343
            MimeParserState::Start => {
344
60.0k
                if let Ok((rem, next)) = mime_parse_boundary(&ctx.boundary, input) {
345
57.0k
                    ctx.state = next;
346
57.0k
                    consumed += (input.len() - rem.len()) as u32;
347
57.0k
                    input = rem;
348
57.0k
                } else {
349
3.04k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
350
                }
351
            }
352
            MimeParserState::BoundaryWaitingForEol => {
353
750
                if let Ok((rem, found)) = mime_consume_until_eol(input) {
354
750
                    if found {
355
30
                        ctx.state = MimeParserState::Header;
356
720
                    }
357
750
                    consumed += (input.len() - rem.len()) as u32;
358
750
                    input = rem;
359
                } else {
360
                    // should never happen
361
0
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
362
                }
363
            }
364
            MimeParserState::Header => {
365
174k
                if let Ok((rem, (next, fileopen, err))) = mime_parse_headers(ctx, input) {
366
51.3k
                    ctx.state = next;
367
51.3k
                    consumed += (input.len() - rem.len()) as u32;
368
51.3k
                    input = rem;
369
51.3k
                    if err {
370
26.4k
                        warnings |= MIME_EVENT_FLAG_INVALID_HEADER;
371
26.4k
                    }
372
51.3k
                    if fileopen {
373
7.49k
                        return (MimeParserResult::MimeFileOpen, consumed, warnings);
374
43.8k
                    }
375
                } else {
376
123k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
377
                }
378
            }
379
            MimeParserState::HeaderEnd => {
380
                // check if we start with the boundary
381
                // and transition to chunk, or empty file and back to start
382
8.01k
                if input.len() < ctx.boundary.len() {
383
618
                    if input == &ctx.boundary[..input.len()] {
384
60
                        return (MimeParserResult::MimeNeedsMore, consumed, warnings);
385
558
                    }
386
558
                    ctx.state = MimeParserState::Chunk;
387
7.39k
                } else if input[..ctx.boundary.len()] == ctx.boundary {
388
3.93k
                    ctx.state = MimeParserState::Start;
389
3.93k
                    if !ctx.filename.is_empty() {
390
779
                        warnings |= MIME_EVENT_FLAG_NO_FILEDATA;
391
3.15k
                    }
392
3.93k
                    ctx.filename.clear();
393
3.93k
                    return (MimeParserResult::MimeFileClose, consumed, warnings);
394
3.45k
                } else {
395
3.45k
                    ctx.state = MimeParserState::Chunk;
396
3.45k
                }
397
            }
398
            MimeParserState::Chunk => {
399
46.0k
                if let Ok((rem, eof)) = mime_consume_chunk(&ctx.boundary, input) {
400
35.7k
                    consumed += (input.len() - rem.len()) as u32;
401
35.7k
                    if eof {
402
2.95k
                        ctx.state = MimeParserState::Start;
403
2.95k
                        ctx.filename.clear();
404
2.95k
                        return (MimeParserResult::MimeFileClose, consumed, warnings);
405
                    } else {
406
                        // + 2 for \r\n
407
32.8k
                        if rem.len() < ctx.boundary.len() + 2 {
408
24.3k
                            return (MimeParserResult::MimeFileChunk, consumed, warnings);
409
8.51k
                        }
410
8.51k
                        input = rem;
411
                    }
412
                } else {
413
10.2k
                    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
414
                }
415
            }
416
        }
417
    }
418
69.9k
    return (MimeParserResult::MimeNeedsMore, consumed, warnings);
419
245k
}
420
421
19.6k
pub fn mime_state_init(i: &[u8]) -> Option<MimeStateHTTP> {
422
19.6k
    let mut sections_values = Vec::new();
423
19.6k
    if let Some(value) = mime_find_header_token(i, "boundary".as_bytes(), &mut sections_values) {
424
10.6k
        if value.len() <= RS_MIME_MAX_TOKEN_LEN {
425
10.6k
            let mut r = MimeStateHTTP {
426
10.6k
                boundary: Vec::with_capacity(2 + value.len()),
427
10.6k
                ..Default::default()
428
10.6k
            };
429
            // start wih 2 additional hyphens
430
10.6k
            r.boundary.push(b'-');
431
10.6k
            r.boundary.push(b'-');
432
120k
            for c in value {
433
                // unescape
434
109k
                if *c != b'\\' {
435
108k
                    r.boundary.push(*c);
436
108k
                }
437
            }
438
10.6k
            return Some(r);
439
27
        }
440
8.98k
    }
441
9.01k
    return None;
442
19.6k
}
443
444
#[no_mangle]
445
19.6k
pub unsafe extern "C" fn SCMimeStateInit(input: *const u8, input_len: u32) -> *mut MimeStateHTTP {
446
19.6k
    let slice = build_slice!(input, input_len as usize);
447
448
19.6k
    if let Some(ctx) = mime_state_init(slice) {
449
10.6k
        let boxed = Box::new(ctx);
450
10.6k
        return Box::into_raw(boxed) as *mut _;
451
9.01k
    }
452
9.01k
    return std::ptr::null_mut();
453
19.6k
}
454
455
#[no_mangle]
456
245k
pub unsafe extern "C" fn SCMimeParse(
457
245k
    ctx: &mut MimeStateHTTP, input: *const u8, input_len: u32, consumed: *mut u32,
458
245k
    warnings: *mut u32,
459
245k
) -> MimeParserResult {
460
245k
    let slice = build_slice!(input, input_len as usize);
461
245k
    let (r, c, w) = mime_process(ctx, slice);
462
245k
    *consumed = c;
463
245k
    *warnings = w;
464
245k
    return r;
465
245k
}
466
467
#[no_mangle]
468
7.49k
pub unsafe extern "C" fn SCMimeStateGetFilename(
469
7.49k
    ctx: &mut MimeStateHTTP, buffer: *mut *const u8, filename_len: *mut u16,
470
7.49k
) {
471
7.49k
    if !ctx.filename.is_empty() {
472
7.49k
        *buffer = ctx.filename.as_ptr();
473
7.49k
        if ctx.filename.len() < usize::from(u16::MAX) {
474
7.49k
            *filename_len = ctx.filename.len() as u16;
475
7.49k
        } else {
476
0
            *filename_len = u16::MAX;
477
0
        }
478
0
    } else {
479
0
        *buffer = std::ptr::null_mut();
480
0
        *filename_len = 0;
481
0
    }
482
7.49k
}
483
484
#[no_mangle]
485
10.6k
pub unsafe extern "C" fn SCMimeStateFree(ctx: &mut MimeStateHTTP) {
486
10.6k
    std::mem::drop(Box::from_raw(ctx));
487
10.6k
}
488
489
#[cfg(test)]
490
mod test {
491
    use super::*;
492
493
    #[test]
494
    fn test_mime_find_header_token() {
495
        let mut outvec = Vec::new();
496
        let undelimok = mime_find_header_token(
497
            "attachment; filename=test;".as_bytes(),
498
            "filename".as_bytes(),
499
            &mut outvec,
500
        );
501
        assert_eq!(undelimok, Some("test".as_bytes()));
502
503
        let delimok = mime_find_header_token(
504
            "attachment; filename=\"test2\";".as_bytes(),
505
            "filename".as_bytes(),
506
            &mut outvec,
507
        );
508
        assert_eq!(delimok, Some("test2".as_bytes()));
509
510
        let escaped = mime_find_header_token(
511
            "attachment; filename=\"test\\\"2\";".as_bytes(),
512
            "filename".as_bytes(),
513
            &mut outvec,
514
        );
515
        assert_eq!(escaped, Some("test\\\"2".as_bytes()));
516
517
        let evasion_othertoken = mime_find_header_token(
518
            "attachment; dummy=\"filename=wrong\"; filename=real;".as_bytes(),
519
            "filename".as_bytes(),
520
            &mut outvec,
521
        );
522
        assert_eq!(evasion_othertoken, Some("real".as_bytes()));
523
524
        let evasion_suffixtoken = mime_find_header_token(
525
            "attachment; notafilename=wrong; filename=good;".as_bytes(),
526
            "filename".as_bytes(),
527
            &mut outvec,
528
        );
529
        assert_eq!(evasion_suffixtoken, Some("good".as_bytes()));
530
531
        let badending = mime_find_header_token(
532
            "attachment; filename=oksofar; badending".as_bytes(),
533
            "filename".as_bytes(),
534
            &mut outvec,
535
        );
536
        assert_eq!(badending, Some("oksofar".as_bytes()));
537
538
        let missend = mime_find_header_token(
539
            "attachment; filename=test".as_bytes(),
540
            "filename".as_bytes(),
541
            &mut outvec,
542
        );
543
        assert_eq!(missend, Some("test".as_bytes()));
544
545
        let spaces = mime_find_header_token(
546
            "attachment; filename=test me wrong".as_bytes(),
547
            "filename".as_bytes(),
548
            &mut outvec,
549
        );
550
        assert_eq!(spaces, Some("test me wrong".as_bytes()));
551
552
        assert_eq!(outvec.len(), 0);
553
        let multi = mime_find_header_token(
554
            "attachment; filename*0=abc; filename*1=\"def\";".as_bytes(),
555
            "filename".as_bytes(),
556
            &mut outvec,
557
        );
558
        assert_eq!(multi, Some("abcdef".as_bytes()));
559
        outvec.clear();
560
561
        let multi = mime_find_header_token(
562
            "attachment; filename*1=456; filename*0=\"123\"".as_bytes(),
563
            "filename".as_bytes(),
564
            &mut outvec,
565
        );
566
        assert_eq!(multi, Some("123456".as_bytes()));
567
        outvec.clear();
568
    }
569
}