1use std::borrow::Cow;
2
3use crate::scan::{
4 CommentKind, Keyword, LineKind, LineScanner, classify_line, find_quoted_bounds, has_byte,
5 parse_plural_index, split_once_byte, trim_ascii,
6};
7use crate::text::{extract_quoted_bytes_cow, split_reference_comment};
8use crate::utf8::input_slice_as_str;
9use crate::{Header, MsgStr, ParseError, PoFile, PoItem};
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct BorrowedPoFile<'a> {
15 pub comments: Vec<Cow<'a, str>>,
17 pub extracted_comments: Vec<Cow<'a, str>>,
19 pub headers: Vec<BorrowedHeader<'a>>,
21 pub items: Vec<BorrowedPoItem<'a>>,
23}
24
25impl BorrowedPoFile<'_> {
26 #[must_use]
28 pub fn into_owned(self) -> PoFile {
29 PoFile {
30 comments: self.comments.into_iter().map(Cow::into_owned).collect(),
31 extracted_comments: self
32 .extracted_comments
33 .into_iter()
34 .map(Cow::into_owned)
35 .collect(),
36 headers: self
37 .headers
38 .into_iter()
39 .map(BorrowedHeader::into_owned)
40 .collect(),
41 items: self
42 .items
43 .into_iter()
44 .map(BorrowedPoItem::into_owned)
45 .collect(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct BorrowedHeader<'a> {
53 pub key: Cow<'a, str>,
55 pub value: Cow<'a, str>,
57}
58
59impl BorrowedHeader<'_> {
60 #[must_use]
62 pub fn into_owned(self) -> Header {
63 Header {
64 key: self.key.into_owned(),
65 value: self.value.into_owned(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Default)]
72pub struct BorrowedPoItem<'a> {
73 pub msgid: Cow<'a, str>,
75 pub msgctxt: Option<Cow<'a, str>>,
77 pub references: Vec<Cow<'a, str>>,
79 pub msgid_plural: Option<Cow<'a, str>>,
81 pub msgstr: BorrowedMsgStr<'a>,
83 pub comments: Vec<Cow<'a, str>>,
85 pub extracted_comments: Vec<Cow<'a, str>>,
87 pub flags: Vec<Cow<'a, str>>,
89 pub metadata: Vec<(Cow<'a, str>, Cow<'a, str>)>,
91 pub obsolete: bool,
93 pub nplurals: usize,
95}
96
97impl BorrowedPoItem<'_> {
98 fn new(nplurals: usize) -> Self {
99 Self {
100 nplurals,
101 ..Self::default()
102 }
103 }
104
105 #[must_use]
107 pub fn into_owned(self) -> PoItem {
108 PoItem {
109 msgid: self.msgid.into_owned(),
110 msgctxt: self.msgctxt.map(Cow::into_owned),
111 references: self.references.into_iter().map(Cow::into_owned).collect(),
112 msgid_plural: self.msgid_plural.map(Cow::into_owned),
113 msgstr: self.msgstr.into_owned(),
114 comments: self.comments.into_iter().map(Cow::into_owned).collect(),
115 extracted_comments: self
116 .extracted_comments
117 .into_iter()
118 .map(Cow::into_owned)
119 .collect(),
120 flags: self.flags.into_iter().map(Cow::into_owned).collect(),
121 metadata: self
122 .metadata
123 .into_iter()
124 .map(|(key, value)| (key.into_owned(), value.into_owned()))
125 .collect(),
126 obsolete: self.obsolete,
127 nplurals: self.nplurals,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Default)]
134pub enum BorrowedMsgStr<'a> {
135 #[default]
137 None,
138 Singular(Cow<'a, str>),
140 Plural(Vec<Cow<'a, str>>),
142}
143
144impl BorrowedMsgStr<'_> {
145 const fn is_empty(&self) -> bool {
146 matches!(self, Self::None)
147 }
148
149 fn len(&self) -> usize {
150 match self {
151 Self::None => 0,
152 Self::Singular(_) => 1,
153 Self::Plural(values) => values.len(),
154 }
155 }
156
157 #[must_use]
159 pub fn into_owned(self) -> MsgStr {
160 match self {
161 Self::None => MsgStr::None,
162 Self::Singular(value) => MsgStr::Singular(value.into_owned()),
163 Self::Plural(values) => {
164 MsgStr::Plural(values.into_iter().map(Cow::into_owned).collect())
165 }
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171enum Context {
172 Id,
173 IdPlural,
174 Str,
175 Ctxt,
176}
177
178#[derive(Debug)]
179struct ParserState<'a> {
180 item: BorrowedPoItem<'a>,
181 header_entries: Vec<BorrowedHeader<'a>>,
182 msgstr: BorrowedMsgStr<'a>,
183 context: Option<Context>,
184 plural_index: usize,
185 obsolete_line_count: usize,
186 content_line_count: usize,
187 has_keyword: bool,
188}
189
190impl<'a> ParserState<'a> {
191 fn new(nplurals: usize) -> Self {
192 Self {
193 item: BorrowedPoItem::new(nplurals),
194 header_entries: Vec::new(),
195 msgstr: BorrowedMsgStr::None,
196 context: None,
197 plural_index: 0,
198 obsolete_line_count: 0,
199 content_line_count: 0,
200 has_keyword: false,
201 }
202 }
203
204 fn reset(&mut self, nplurals: usize) {
205 *self = Self::new(nplurals);
206 }
207
208 fn set_msgstr(&mut self, plural_index: usize, value: Cow<'a, str>) {
209 match (&mut self.msgstr, plural_index) {
210 (BorrowedMsgStr::None, 0) => self.msgstr = BorrowedMsgStr::Singular(value),
211 (BorrowedMsgStr::Singular(existing), 0) => *existing = value,
212 (BorrowedMsgStr::Plural(values), 0) => {
213 if values.is_empty() {
214 values.push(Cow::Borrowed(""));
215 }
216 values[0] = value;
217 }
218 _ => {
219 let msgstr = self.promote_plural_msgstr(plural_index);
220 msgstr[plural_index] = value;
221 }
222 }
223 }
224
225 fn append_msgstr(&mut self, plural_index: usize, value: Cow<'a, str>) {
226 match (&mut self.msgstr, plural_index) {
227 (BorrowedMsgStr::None, 0) => self.msgstr = BorrowedMsgStr::Singular(value),
228 (BorrowedMsgStr::Singular(existing), 0) => existing.to_mut().push_str(value.as_ref()),
229 (BorrowedMsgStr::Plural(values), 0) => {
230 if values.is_empty() {
231 values.push(Cow::Borrowed(""));
232 }
233 values[0].to_mut().push_str(value.as_ref());
234 }
235 _ => {
236 let msgstr = self.promote_plural_msgstr(plural_index);
237 msgstr[plural_index].to_mut().push_str(value.as_ref());
238 }
239 }
240 }
241
242 fn materialize_msgstr(&mut self) {
243 debug_assert!(self.item.msgstr.is_empty());
244 self.item.msgstr = std::mem::take(&mut self.msgstr);
245 }
246
247 fn promote_plural_msgstr(&mut self, plural_index: usize) -> &mut Vec<Cow<'a, str>> {
248 if !matches!(self.msgstr, BorrowedMsgStr::Plural(_)) {
249 self.msgstr = match std::mem::take(&mut self.msgstr) {
250 BorrowedMsgStr::None => BorrowedMsgStr::Plural(Vec::with_capacity(2)),
251 BorrowedMsgStr::Singular(value) => {
252 let mut values = Vec::with_capacity(2);
253 values.push(value);
254 BorrowedMsgStr::Plural(values)
255 }
256 BorrowedMsgStr::Plural(values) => BorrowedMsgStr::Plural(values),
257 };
258 }
259 let BorrowedMsgStr::Plural(values) = &mut self.msgstr else {
260 unreachable!("plural msgstr promotion must yield plural storage");
261 };
262 if values.len() <= plural_index {
263 values.resize(plural_index + 1, Cow::Borrowed(""));
264 }
265 values
266 }
267}
268
269#[derive(Debug, Clone, Copy)]
270struct BorrowedLine<'a> {
271 trimmed: &'a [u8],
272 obsolete: bool,
273}
274
275pub fn parse_po_borrowed(input: &str) -> Result<BorrowedPoFile<'_>, ParseError> {
284 let input = input.strip_prefix('\u{feff}').unwrap_or(input);
285 if input.as_bytes().contains(&b'\r') {
286 return Err(ParseError::new(
287 "borrowed PO parsing currently requires LF-only input",
288 ));
289 }
290
291 let mut file = BorrowedPoFile::default();
292 file.items.reserve((input.len() / 96).max(1));
293 let mut current_nplurals = 2;
294 let mut state = ParserState::new(current_nplurals);
295
296 for line in LineScanner::new(input.as_bytes()) {
297 parse_line(
298 BorrowedLine {
299 trimmed: line.trimmed,
300 obsolete: line.obsolete,
301 },
302 &mut state,
303 &mut file,
304 &mut current_nplurals,
305 )?;
306 }
307
308 finish_item(&mut state, &mut file, &mut current_nplurals);
309
310 Ok(file)
311}
312
313fn parse_line<'a>(
314 line: BorrowedLine<'a>,
315 state: &mut ParserState<'a>,
316 file: &mut BorrowedPoFile<'a>,
317 current_nplurals: &mut usize,
318) -> Result<(), ParseError> {
319 match classify_line(line.trimmed) {
320 LineKind::Continuation => {
321 append_continuation(line.trimmed, line.obsolete, state)?;
322 Ok(())
323 }
324 LineKind::Comment(kind) => {
325 parse_comment_line(line.trimmed, kind, state, file, current_nplurals);
326 Ok(())
327 }
328 LineKind::Keyword(keyword) => parse_keyword_line(
329 line.trimmed,
330 line.obsolete,
331 keyword,
332 state,
333 file,
334 current_nplurals,
335 ),
336 LineKind::Other => Ok(()),
337 }
338}
339
340fn parse_comment_line<'a>(
341 line_bytes: &'a [u8],
342 kind: CommentKind,
343 state: &mut ParserState<'a>,
344 file: &mut BorrowedPoFile<'a>,
345 current_nplurals: &mut usize,
346) {
347 finish_item(state, file, current_nplurals);
348
349 match kind {
350 CommentKind::Reference => {
351 let reference_line = trimmed_str(&line_bytes[2..]);
352 state
353 .item
354 .references
355 .extend(split_reference_comment(reference_line));
356 }
357 CommentKind::Flags => {
358 for flag in trimmed_str(&line_bytes[2..]).split(',') {
359 state.item.flags.push(Cow::Borrowed(flag.trim()));
360 }
361 }
362 CommentKind::Extracted => state
363 .item
364 .extracted_comments
365 .push(trimmed_cow(&line_bytes[2..])),
366 CommentKind::Metadata => {
367 let trimmed = trim_ascii(&line_bytes[2..]);
368 if let Some((key_bytes, value_bytes)) = split_once_byte(trimmed, b':') {
369 let key = trimmed_cow(key_bytes);
370 if !key.is_empty() {
371 let value = trimmed_cow(value_bytes);
372 state.item.metadata.push((key, value));
373 }
374 }
375 }
376 CommentKind::Translator => state.item.comments.push(trimmed_cow(&line_bytes[1..])),
377 CommentKind::Other => {}
378 }
379}
380
381fn parse_keyword_line<'a>(
382 line_bytes: &'a [u8],
383 obsolete: bool,
384 keyword: Keyword,
385 state: &mut ParserState<'a>,
386 file: &mut BorrowedPoFile<'a>,
387 current_nplurals: &mut usize,
388) -> Result<(), ParseError> {
389 match keyword {
390 Keyword::IdPlural => {
391 state.obsolete_line_count += usize::from(obsolete);
392 state.item.msgid_plural = Some(extract_quoted_bytes_cow(line_bytes)?);
393 state.context = Some(Context::IdPlural);
394 state.content_line_count += 1;
395 state.has_keyword = true;
396 }
397 Keyword::Id => {
398 finish_item(state, file, current_nplurals);
399 state.obsolete_line_count += usize::from(obsolete);
400 state.item.msgid = extract_quoted_bytes_cow(line_bytes)?;
401 state.context = Some(Context::Id);
402 state.content_line_count += 1;
403 state.has_keyword = true;
404 }
405 Keyword::Str => {
406 let plural_index = parse_plural_index(line_bytes).unwrap_or(0);
407 state.plural_index = plural_index;
408 state.obsolete_line_count += usize::from(obsolete);
409 state.set_msgstr(plural_index, extract_quoted_bytes_cow(line_bytes)?);
410 if is_header_candidate(state) {
411 state
412 .header_entries
413 .extend(parse_header_fragment(line_bytes)?);
414 }
415 state.context = Some(Context::Str);
416 state.content_line_count += 1;
417 state.has_keyword = true;
418 }
419 Keyword::Ctxt => {
420 finish_item(state, file, current_nplurals);
421 state.obsolete_line_count += usize::from(obsolete);
422 state.item.msgctxt = Some(extract_quoted_bytes_cow(line_bytes)?);
423 state.context = Some(Context::Ctxt);
424 state.content_line_count += 1;
425 state.has_keyword = true;
426 }
427 }
428
429 Ok(())
430}
431
432fn append_continuation<'a>(
433 line_bytes: &'a [u8],
434 obsolete: bool,
435 state: &mut ParserState<'a>,
436) -> Result<(), ParseError> {
437 state.obsolete_line_count += usize::from(obsolete);
438 state.content_line_count += 1;
439 let value = extract_quoted_bytes_cow(line_bytes)?;
440
441 match state.context {
442 Some(Context::Str) => {
443 state.append_msgstr(state.plural_index, value);
444 if is_header_candidate(state) {
445 state
446 .header_entries
447 .extend(parse_header_fragment(line_bytes)?);
448 }
449 }
450 Some(Context::Id) => state.item.msgid.to_mut().push_str(value.as_ref()),
451 Some(Context::IdPlural) => {
452 let target = state.item.msgid_plural.get_or_insert(Cow::Borrowed(""));
453 target.to_mut().push_str(value.as_ref());
454 }
455 Some(Context::Ctxt) => {
456 let target = state.item.msgctxt.get_or_insert(Cow::Borrowed(""));
457 target.to_mut().push_str(value.as_ref());
458 }
459 None => {}
460 }
461
462 Ok(())
463}
464
465fn finish_item<'a>(
466 state: &mut ParserState<'a>,
467 file: &mut BorrowedPoFile<'a>,
468 current_nplurals: &mut usize,
469) {
470 if !state.has_keyword {
471 return;
472 }
473
474 if state.item.msgid.is_empty() && !is_header_state(state) {
475 return;
476 }
477
478 if state.obsolete_line_count >= state.content_line_count && state.content_line_count > 0 {
479 state.item.obsolete = true;
480 }
481
482 if is_header_state(state) && file.headers.is_empty() && file.items.is_empty() {
483 file.comments = std::mem::take(&mut state.item.comments);
484 file.extracted_comments = std::mem::take(&mut state.item.extracted_comments);
485 file.headers = std::mem::take(&mut state.header_entries);
486 *current_nplurals = parse_nplurals(&file.headers).unwrap_or(2);
487 state.reset(*current_nplurals);
488 return;
489 }
490
491 state.materialize_msgstr();
492
493 if state.item.msgstr.is_empty() {
494 state.item.msgstr = BorrowedMsgStr::Singular(Cow::Borrowed(""));
495 }
496 if state.item.msgid_plural.is_some() && state.item.msgstr.len() == 1 {
497 let mut values = match std::mem::take(&mut state.item.msgstr) {
498 BorrowedMsgStr::None => Vec::new(),
499 BorrowedMsgStr::Singular(value) => vec![value],
500 BorrowedMsgStr::Plural(values) => values,
501 };
502 values.resize(state.item.nplurals.max(1), Cow::Borrowed(""));
503 state.item.msgstr = BorrowedMsgStr::Plural(values);
504 }
505
506 state.item.nplurals = *current_nplurals;
507 file.items.push(std::mem::take(&mut state.item));
508 state.reset(*current_nplurals);
509}
510
511fn is_header_state(state: &ParserState<'_>) -> bool {
512 state.item.msgid.is_empty()
513 && state.item.msgctxt.is_none()
514 && state.item.msgid_plural.is_none()
515 && !state.msgstr.is_empty()
516}
517
518fn is_header_candidate(state: &ParserState<'_>) -> bool {
519 state.item.msgid.is_empty()
520 && state.item.msgctxt.is_none()
521 && state.item.msgid_plural.is_none()
522 && state.plural_index == 0
523}
524
525fn parse_header_fragment(line_bytes: &[u8]) -> Result<Vec<BorrowedHeader<'_>>, ParseError> {
526 let Some((start, end)) = find_quoted_bounds(line_bytes) else {
527 return Ok(Vec::new());
528 };
529 let raw = &line_bytes[start..end];
530
531 if header_fragment_is_borrowable(raw) {
532 return Ok(parse_header_fragment_borrowed(raw));
533 }
534
535 parse_header_fragment_owned(line_bytes)
536}
537
538fn parse_header_fragment_borrowed(raw: &[u8]) -> Vec<BorrowedHeader<'_>> {
539 let mut headers = Vec::new();
540 let mut start = 0usize;
541 let mut index = 0usize;
542
543 while index < raw.len() {
544 if raw[index] == b'\\' && raw.get(index + 1) == Some(&b'n') {
545 push_borrowed_header_segment(&raw[start..index], &mut headers);
546 index += 2;
547 start = index;
548 continue;
549 }
550 index += 1;
551 }
552
553 push_borrowed_header_segment(&raw[start..], &mut headers);
554 headers
555}
556
557fn push_borrowed_header_segment<'a>(segment: &'a [u8], out: &mut Vec<BorrowedHeader<'a>>) {
558 if segment.is_empty() {
559 return;
560 }
561 if let Some((key_bytes, value_bytes)) = split_once_byte(segment, b':') {
562 out.push(BorrowedHeader {
563 key: trimmed_cow(key_bytes),
564 value: trimmed_cow(value_bytes),
565 });
566 }
567}
568
569fn parse_header_fragment_owned(line_bytes: &[u8]) -> Result<Vec<BorrowedHeader<'_>>, ParseError> {
570 let decoded = extract_quoted_bytes_cow(line_bytes)?;
571 let mut headers = Vec::new();
572 for segment in decoded.split('\n') {
573 if segment.is_empty() {
574 continue;
575 }
576 if let Some((key, value)) = segment.split_once(':') {
577 headers.push(BorrowedHeader {
578 key: Cow::Owned(key.trim().to_owned()),
579 value: Cow::Owned(value.trim().to_owned()),
580 });
581 }
582 }
583 Ok(headers)
584}
585
586fn header_fragment_is_borrowable(raw: &[u8]) -> bool {
587 let mut index = 0usize;
588 while index < raw.len() {
589 if raw[index] == b'\\' {
590 if raw.get(index + 1) != Some(&b'n') {
591 return false;
592 }
593 index += 2;
594 continue;
595 }
596 index += 1;
597 }
598 !has_byte(b'"', raw)
599}
600
601fn parse_nplurals(headers: &[BorrowedHeader<'_>]) -> Option<usize> {
602 let plural_forms = headers
603 .iter()
604 .find(|header| header.key.as_ref() == "Plural-Forms")?
605 .value
606 .as_bytes();
607 let mut rest = plural_forms;
608
609 while !rest.is_empty() {
610 let (part, next) = match split_once_byte(rest, b';') {
611 Some((part, tail)) => (part, tail),
612 None => (rest, &b""[..]),
613 };
614 let trimmed = trim_ascii(part);
615 if let Some((key, value)) = split_once_byte(trimmed, b'=')
616 && trim_ascii(key) == b"nplurals"
617 && let value = bytes_to_str(trim_ascii(value))
618 && let Ok(parsed) = value.parse::<usize>()
619 {
620 return Some(parsed);
621 }
622 rest = next;
623 }
624
625 None
626}
627
628fn bytes_to_str(bytes: &[u8]) -> &str {
629 input_slice_as_str(bytes)
630}
631
632fn trimmed_str(bytes: &[u8]) -> &str {
633 bytes_to_str(trim_ascii(bytes))
634}
635
636fn trimmed_cow(bytes: &[u8]) -> Cow<'_, str> {
637 Cow::Borrowed(trimmed_str(bytes))
638}
639
640#[cfg(test)]
641mod tests {
642 use std::borrow::Cow;
643
644 use super::{BorrowedMsgStr, parse_po_borrowed};
645
646 #[test]
647 fn borrows_simple_fields() {
648 let input = r#"
649# translator
650msgid "hello"
651msgstr "world"
652"#;
653
654 let file = parse_po_borrowed(input).expect("borrowed parse");
655 assert_eq!(file.items[0].comments[0], Cow::Borrowed("translator"));
656 assert_eq!(file.items[0].msgid, Cow::Borrowed("hello"));
657 assert_eq!(
658 file.items[0].msgstr,
659 super::BorrowedMsgStr::Singular(Cow::Borrowed("world"))
660 );
661 }
662
663 #[test]
664 fn owns_unescaped_sequences_only_when_needed() {
665 let input = "msgid \"a\\n\"\nmsgstr \"b\\t\"\n";
666 let file = parse_po_borrowed(input).expect("borrowed parse with escapes");
667 assert_eq!(file.items[0].msgid, Cow::<str>::Owned("a\n".to_owned()));
668 assert_eq!(
669 file.items[0].msgstr,
670 super::BorrowedMsgStr::Singular(Cow::<str>::Owned("b\t".to_owned()))
671 );
672 }
673
674 #[test]
675 fn converts_borrowed_parse_to_owned() {
676 let input = "msgid \"hello\"\nmsgstr \"world\"\n";
677 let owned = parse_po_borrowed(input)
678 .expect("borrowed parse")
679 .into_owned();
680 assert_eq!(owned.items[0].msgid, "hello");
681 assert_eq!(owned.items[0].msgstr[0], "world");
682 }
683
684 #[test]
685 fn borrows_header_key_values_without_escapes() {
686 let input = concat!(
687 "msgid \"\"\n",
688 "msgstr \"\"\n",
689 "\"Language: de\\n\"\n",
690 "\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n",
691 );
692 let file = parse_po_borrowed(input).expect("borrowed parse with headers");
693 assert_eq!(file.headers[0].key, Cow::Borrowed("Language"));
694 assert_eq!(file.headers[0].value, Cow::Borrowed("de"));
695 }
696
697 #[test]
698 fn strips_utf8_bom_prefix() {
699 let input = "\u{feff}msgid \"foo\"\nmsgstr \"bar\"\n";
700 let file = parse_po_borrowed(input).expect("borrowed parse");
701
702 assert_eq!(file.items.len(), 1);
703 assert_eq!(file.items[0].msgid, Cow::Borrowed("foo"));
704 assert_eq!(
705 file.items[0].msgstr,
706 super::BorrowedMsgStr::Singular(Cow::Borrowed("bar"))
707 );
708 }
709
710 #[test]
711 fn rejects_crlf_input_for_borrowed_parsing() {
712 let error = parse_po_borrowed("msgid \"foo\"\r\nmsgstr \"bar\"\r\n")
713 .expect_err("crlf should be rejected");
714 assert!(error.to_string().contains("LF-only"));
715 }
716
717 #[test]
718 fn parses_plural_metadata_flags_and_obsolete_items() {
719 let input = concat!(
720 "# translator\n",
721 "#. extracted\n",
722 "#: src/app.rs:1 src/lib.rs:2\n",
723 "#, fuzzy, c-format\n",
724 "#@ domain: admin\n",
725 "msgctxt \"menu\"\n",
726 "msgid \"file\"\n",
727 "msgid_plural \"files\"\n",
728 "msgstr[0] \"Datei\"\n",
729 "msgstr[1] \"Dateien\"\n",
730 "\n",
731 "#~ msgid \"old\"\n",
732 "#~ msgstr \"alt\"\n",
733 );
734
735 let file = parse_po_borrowed(input).expect("borrowed plural parse");
736 assert_eq!(file.items.len(), 2);
737
738 let item = &file.items[0];
739 assert_eq!(item.msgctxt.as_deref(), Some("menu"));
740 assert_eq!(item.msgid_plural.as_deref(), Some("files"));
741 assert_eq!(
742 item.msgstr,
743 BorrowedMsgStr::Plural(vec![Cow::Borrowed("Datei"), Cow::Borrowed("Dateien"),])
744 );
745 assert_eq!(
746 item.references,
747 vec![Cow::Borrowed("src/app.rs:1"), Cow::Borrowed("src/lib.rs:2")]
748 );
749 assert_eq!(
750 item.flags,
751 vec![Cow::Borrowed("fuzzy"), Cow::Borrowed("c-format")]
752 );
753 assert_eq!(
754 item.metadata,
755 vec![(Cow::Borrowed("domain"), Cow::Borrowed("admin"))]
756 );
757
758 assert!(file.items[1].obsolete);
759 assert_eq!(file.items[1].msgid, Cow::Borrowed("old"));
760 }
761
762 #[test]
763 fn parses_owned_headers_and_multiline_fields_when_escapes_are_present() {
764 let input = concat!(
765 "msgid \"\"\n",
766 "msgstr \"\"\n",
767 "\"Project-Id-Version: Demo \\\"Suite\\\"\\n\"\n",
768 "\"Plural-Forms: nplurals=3; plural=(n > 1);\\n\"\n",
769 "\n",
770 "msgctxt \"cta\"\n",
771 "msgid \"hel\"\n",
772 "\"lo\"\n",
773 "msgstr \"wor\"\n",
774 "\"ld\"\n",
775 );
776
777 let file = parse_po_borrowed(input).expect("borrowed parse with owned headers");
778 assert_eq!(
779 file.headers[0],
780 super::BorrowedHeader {
781 key: Cow::Owned("Project-Id-Version".to_owned()),
782 value: Cow::Owned("Demo \"Suite\"".to_owned()),
783 }
784 );
785 assert_eq!(file.items[0].msgid, Cow::Borrowed("hello"));
786 assert_eq!(file.items[0].msgctxt.as_deref(), Some("cta"));
787 assert_eq!(
788 file.items[0].msgstr,
789 BorrowedMsgStr::Singular(Cow::Borrowed("world"))
790 );
791 assert_eq!(file.items[0].nplurals, 3);
792 }
793}