1use annotate_snippets::{
3 renderer::{AnsiColor, Style},
4 Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
5};
6use anyhow::{bail, Context, Result};
7use forc_tracing::{println_action_green, println_error, println_red_err, println_yellow_err};
8use std::{
9 collections::{hash_map, HashSet},
10 fmt::Display,
11 fs::File,
12 hash::{Hash, Hasher},
13 path::{Path, PathBuf},
14 process::Termination,
15 str,
16};
17use sway_core::language::parsed::TreeType;
18use sway_error::{
19 diagnostic::{Diagnostic, Issue, Label, LabelType, Level, ToDiagnostic},
20 error::CompileError,
21 warning::CompileWarning,
22};
23use sway_types::{LineCol, LineColRange, SourceEngine, Span};
24use sway_utils::constants;
25
26pub mod bytecode;
27pub mod fs_locking;
28pub mod restricted;
29
30#[macro_use]
31pub mod cli;
32
33pub use ansiterm;
34pub use paste;
35pub use regex::Regex;
36
37pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
38pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
39pub const DEFAULT_SUCCESS_EXIT_CODE: u8 = 0;
40
41pub type ForcResult<T, E = ForcError> = Result<T, E>;
44
45#[derive(Debug)]
48pub struct ForcCliResult<T> {
49 result: ForcResult<T>,
50}
51
52#[derive(Debug)]
55pub struct ForcError {
56 error: anyhow::Error,
57 exit_code: u8,
58}
59
60impl ForcError {
61 pub fn new(error: anyhow::Error, exit_code: u8) -> Self {
62 Self { error, exit_code }
63 }
64
65 pub fn exit_code(self, exit_code: u8) -> Self {
67 Self {
68 error: self.error,
69 exit_code,
70 }
71 }
72}
73
74impl AsRef<anyhow::Error> for ForcError {
75 fn as_ref(&self) -> &anyhow::Error {
76 &self.error
77 }
78}
79
80impl From<&str> for ForcError {
81 fn from(value: &str) -> Self {
82 Self {
83 error: anyhow::anyhow!("{value}"),
84 exit_code: DEFAULT_ERROR_EXIT_CODE,
85 }
86 }
87}
88
89impl From<anyhow::Error> for ForcError {
90 fn from(value: anyhow::Error) -> Self {
91 Self {
92 error: value,
93 exit_code: DEFAULT_ERROR_EXIT_CODE,
94 }
95 }
96}
97
98impl From<std::io::Error> for ForcError {
99 fn from(value: std::io::Error) -> Self {
100 Self {
101 error: value.into(),
102 exit_code: DEFAULT_ERROR_EXIT_CODE,
103 }
104 }
105}
106
107impl Display for ForcError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 self.error.fmt(f)
110 }
111}
112
113impl<T> Termination for ForcCliResult<T> {
114 fn report(self) -> std::process::ExitCode {
115 match self.result {
116 Ok(_) => DEFAULT_SUCCESS_EXIT_CODE.into(),
117 Err(e) => {
118 println_error(&format!("{}", e));
119 e.exit_code.into()
120 }
121 }
122 }
123}
124
125impl<T> From<ForcResult<T>> for ForcCliResult<T> {
126 fn from(value: ForcResult<T>) -> Self {
127 Self { result: value }
128 }
129}
130
131#[macro_export]
132macro_rules! forc_result_bail {
133 ($msg:literal $(,)?) => {
134 return $crate::ForcResult::Err(anyhow::anyhow!($msg).into())
135 };
136 ($err:expr $(,)?) => {
137 return $crate::ForcResult::Err(anyhow::anyhow!($err).into())
138 };
139 ($fmt:expr, $($arg:tt)*) => {
140 return $crate::ForcResult::Err(anyhow::anyhow!($fmt, $($arg)*).into())
141 };
142}
143
144#[cfg(feature = "tx")]
145pub mod tx_utils {
146
147 use anyhow::Result;
148 use clap::Args;
149 use fuels_core::{codec::ABIDecoder, types::param_types::ParamType};
150 use serde::{Deserialize, Serialize};
151 use std::collections::HashMap;
152 use sway_core::{asm_generation::ProgramABI, fuel_prelude::fuel_tx};
153
154 #[derive(Debug, Args, Default, Deserialize, Serialize)]
156 pub struct Salt {
157 #[clap(long = "salt")]
162 pub salt: Option<fuel_tx::Salt>,
163 }
164
165 pub fn format_log_receipts(
167 receipts: &[fuel_tx::Receipt],
168 pretty_print: bool,
169 ) -> Result<String> {
170 let mut receipt_to_json_array = serde_json::to_value(receipts)?;
171 for (rec_index, receipt) in receipts.iter().enumerate() {
172 let rec_value = receipt_to_json_array.get_mut(rec_index).ok_or_else(|| {
173 anyhow::anyhow!(
174 "Serialized receipts does not contain {} th index",
175 rec_index
176 )
177 })?;
178 match receipt {
179 fuel_tx::Receipt::LogData {
180 data: Some(data), ..
181 } => {
182 if let Some(v) = rec_value.pointer_mut("/LogData/data") {
183 *v = hex::encode(data).into();
184 }
185 }
186 fuel_tx::Receipt::ReturnData {
187 data: Some(data), ..
188 } => {
189 if let Some(v) = rec_value.pointer_mut("/ReturnData/data") {
190 *v = hex::encode(data).into();
191 }
192 }
193 _ => {}
194 }
195 }
196 if pretty_print {
197 Ok(serde_json::to_string_pretty(&receipt_to_json_array)?)
198 } else {
199 Ok(serde_json::to_string(&receipt_to_json_array)?)
200 }
201 }
202
203 pub struct DecodedLog {
205 pub value: String,
206 }
207
208 pub fn decode_log_data(
209 log_id: &str,
210 log_data: &[u8],
211 program_abi: &ProgramABI,
212 ) -> anyhow::Result<DecodedLog> {
213 let program_abi = match program_abi {
214 ProgramABI::Fuel(fuel_abi) => Some(
215 fuel_abi_types::abi::unified_program::UnifiedProgramABI::from_counterpart(
216 fuel_abi,
217 )?,
218 ),
219 _ => None,
220 }
221 .ok_or_else(|| anyhow::anyhow!("only fuelvm is supported for log decoding"))?;
222 let type_lookup = program_abi
224 .types
225 .iter()
226 .map(|decl| (decl.type_id, decl.clone()))
227 .collect::<HashMap<_, _>>();
228
229 let logged_type_lookup: HashMap<_, _> = program_abi
230 .logged_types
231 .iter()
232 .flatten()
233 .map(|logged_type| (logged_type.log_id.as_str(), logged_type.application.clone()))
234 .collect();
235
236 let type_application = logged_type_lookup
237 .get(&log_id)
238 .ok_or_else(|| anyhow::anyhow!("log id is missing"))?;
239
240 let abi_decoder = ABIDecoder::default();
241 let param_type = ParamType::try_from_type_application(type_application, &type_lookup)?;
242 let decoded_str = abi_decoder.decode_as_debug_str(¶m_type, log_data)?;
243 let decoded_log = DecodedLog { value: decoded_str };
244
245 Ok(decoded_log)
246 }
247}
248
249pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> {
250 let mut file_path = manifest_dir.to_path_buf();
251 file_path.pop();
252 let file_name = match entry_path.strip_prefix(file_path.clone()) {
253 Ok(o) => o,
254 Err(err) => bail!(err),
255 };
256 Ok(file_name)
257}
258
259pub fn lock_path(manifest_dir: &Path) -> PathBuf {
260 manifest_dir.join(constants::LOCK_FILE_NAME)
261}
262
263pub fn validate_project_name(name: &str) -> Result<()> {
264 restricted::is_valid_project_name_format(name)?;
265 validate_name(name, "project name")
266}
267
268pub fn validate_name(name: &str, use_case: &str) -> Result<()> {
272 restricted::contains_invalid_char(name, use_case)?;
274
275 if restricted::is_keyword(name) {
276 bail!("the name `{name}` cannot be used as a {use_case}, it is a Sway keyword");
277 }
278 if restricted::is_conflicting_artifact_name(name) {
279 bail!(
280 "the name `{name}` cannot be used as a {use_case}, \
281 it conflicts with Forc's build directory names"
282 );
283 }
284 if name.to_lowercase() == "test" {
285 bail!(
286 "the name `test` cannot be used as a {use_case}, \
287 it conflicts with Sway's built-in test library"
288 );
289 }
290 if restricted::is_conflicting_suffix(name) {
291 bail!(
292 "the name `{name}` is part of Sway's standard library\n\
293 It is recommended to use a different name to avoid problems."
294 );
295 }
296 if restricted::is_windows_reserved(name) {
297 if cfg!(windows) {
298 bail!("cannot use name `{name}`, it is a reserved Windows filename");
299 } else {
300 bail!(
301 "the name `{name}` is a reserved Windows filename\n\
302 This package will not work on Windows platforms."
303 );
304 }
305 }
306 if restricted::is_non_ascii_name(name) {
307 bail!("the name `{name}` contains non-ASCII characters which are unsupported");
308 }
309 Ok(())
310}
311
312pub fn kebab_to_snake_case(s: &str) -> String {
314 s.replace('-', "_")
315}
316
317pub fn default_output_directory(manifest_dir: &Path) -> PathBuf {
318 manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY)
319}
320
321pub fn user_forc_directory() -> PathBuf {
323 dirs::home_dir()
324 .expect("unable to find the user home directory")
325 .join(constants::USER_FORC_DIRECTORY)
326}
327
328pub fn git_checkouts_directory() -> PathBuf {
330 user_forc_directory().join("git").join("checkouts")
331}
332
333fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
343 const LOCKS_DIR_NAME: &str = ".locks";
344 const LOCK_EXT: &str = "forc-lock";
345 let file_name = hash_path(path);
346 user_forc_directory()
347 .join(LOCKS_DIR_NAME)
348 .join(file_name)
349 .with_extension(LOCK_EXT)
350}
351
352fn hash_path<X: AsRef<Path>>(path: X) -> String {
355 let path = path.as_ref();
356 let mut hasher = hash_map::DefaultHasher::default();
357 path.hash(&mut hasher);
358 let hash = hasher.finish();
359 let file_name = match path.file_stem().and_then(|s| s.to_str()) {
360 None => format!("{hash:X}"),
361 Some(stem) => format!("{hash:X}-{stem}"),
362 };
363 file_name
364}
365
366pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
370 let lock_path = fd_lock_path(path);
371 let lock_dir = lock_path
372 .parent()
373 .expect("lock path has no parent directory");
374 std::fs::create_dir_all(lock_dir).context("failed to create forc advisory lock directory")?;
375 let lock_file = File::create(&lock_path).context("failed to create advisory lock file")?;
376 Ok(fd_lock::RwLock::new(lock_file))
377}
378
379pub fn program_type_str(ty: &TreeType) -> &'static str {
380 match ty {
381 TreeType::Script => "script",
382 TreeType::Contract => "contract",
383 TreeType::Predicate => "predicate",
384 TreeType::Library => "library",
385 }
386}
387
388pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Display) {
389 let ty = match ty {
392 Some(ty) => format!("{} ", program_type_str(ty)),
393 None => "".to_string(),
394 };
395 println_action_green(
396 "Compiling",
397 &format!("{ty}{} ({src})", ansiterm::Style::new().bold().paint(name)),
398 );
399}
400
401pub fn print_warnings(
402 source_engine: &SourceEngine,
403 terse_mode: bool,
404 proj_name: &str,
405 warnings: &[CompileWarning],
406 tree_type: &TreeType,
407) {
408 if warnings.is_empty() {
409 return;
410 }
411 let type_str = program_type_str(tree_type);
412
413 if !terse_mode {
414 warnings
415 .iter()
416 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
417 }
418
419 println_yellow_err(&format!(
420 " Compiled {} {:?} with {} {}.",
421 type_str,
422 proj_name,
423 warnings.len(),
424 if warnings.len() > 1 {
425 "warnings"
426 } else {
427 "warning"
428 }
429 ));
430}
431
432pub fn print_on_failure(
433 source_engine: &SourceEngine,
434 terse_mode: bool,
435 warnings: &[CompileWarning],
436 errors: &[CompileError],
437 reverse_results: bool,
438) {
439 let e_len = errors.len();
440 let w_len = warnings.len();
441
442 if !terse_mode {
443 if reverse_results {
444 warnings
445 .iter()
446 .rev()
447 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
448 errors
449 .iter()
450 .rev()
451 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
452 } else {
453 warnings
454 .iter()
455 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
456 errors
457 .iter()
458 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
459 }
460 }
461
462 if e_len == 0 && w_len > 0 {
463 println_red_err(&format!(
464 " Aborting. {} warning(s) treated as error(s).",
465 warnings.len()
466 ));
467 } else {
468 println_red_err(&format!(
469 " Aborting due to {} {}.",
470 e_len,
471 if e_len > 1 { "errors" } else { "error" }
472 ));
473 }
474}
475
476pub fn create_diagnostics_renderer() -> Renderer {
481 Renderer::styled()
485 .warning(
486 Style::new()
487 .bold()
488 .fg_color(Some(AnsiColor::BrightYellow.into())),
489 )
490 .error(
491 Style::new()
492 .bold()
493 .fg_color(Some(AnsiColor::BrightRed.into())),
494 )
495}
496
497pub fn format_diagnostic(diagnostic: &Diagnostic) {
498 const SHOW_DIAGNOSTIC_CODE: bool = false;
501
502 if diagnostic.is_old_style() {
503 format_old_style_diagnostic(diagnostic.issue());
504 return;
505 }
506
507 let mut label = String::new();
508 get_title_label(diagnostic, &mut label);
509
510 let snippet_title = Some(Annotation {
511 label: Some(label.as_str()),
512 id: if SHOW_DIAGNOSTIC_CODE {
513 diagnostic.reason().map(|reason| reason.code())
514 } else {
515 None
516 },
517 annotation_type: diagnostic_level_to_annotation_type(diagnostic.level()),
518 });
519
520 let mut snippet_slices = Vec::<Slice<'_>>::new();
521
522 if diagnostic.issue().is_in_source() {
524 snippet_slices.push(construct_slice(diagnostic.labels_in_issue_source()))
525 }
526
527 for source_path in diagnostic.related_sources(false) {
529 snippet_slices.push(construct_slice(diagnostic.labels_in_source(source_path)))
530 }
531
532 let mut snippet_footer = Vec::<Annotation<'_>>::new();
533 for help in diagnostic.help() {
534 snippet_footer.push(Annotation {
535 id: None,
536 label: Some(help),
537 annotation_type: AnnotationType::Help,
538 });
539 }
540
541 let snippet = Snippet {
542 title: snippet_title,
543 slices: snippet_slices,
544 footer: snippet_footer,
545 };
546
547 let renderer = create_diagnostics_renderer();
548 match diagnostic.level() {
549 Level::Info => tracing::info!("{}\n____\n", renderer.render(snippet)),
550 Level::Warning => tracing::warn!("{}\n____\n", renderer.render(snippet)),
551 Level::Error => tracing::error!("{}\n____\n", renderer.render(snippet)),
552 }
553
554 fn format_old_style_diagnostic(issue: &Issue) {
555 let annotation_type = label_type_to_annotation_type(issue.label_type());
556
557 let snippet_title = Some(Annotation {
558 label: if issue.is_in_source() {
559 None
560 } else {
561 Some(issue.text())
562 },
563 id: None,
564 annotation_type,
565 });
566
567 let mut snippet_slices = vec![];
568 if issue.is_in_source() {
569 let span = issue.span();
570 let input = span.input();
571 let mut start_pos = span.start();
572 let mut end_pos = span.end();
573 let LineColRange { mut start, end } = span.line_col_one_index();
574 let input = construct_window(&mut start, end, &mut start_pos, &mut end_pos, input);
575
576 let slice = Slice {
577 source: input,
578 line_start: start.line,
579 origin: Some(issue.source_path().unwrap().as_str()),
581 fold: false,
582 annotations: vec![SourceAnnotation {
583 label: issue.text(),
584 annotation_type,
585 range: (start_pos, end_pos),
586 }],
587 };
588
589 snippet_slices.push(slice);
590 }
591
592 let snippet = Snippet {
593 title: snippet_title,
594 footer: vec![],
595 slices: snippet_slices,
596 };
597
598 let renderer = create_diagnostics_renderer();
599 tracing::error!("{}\n____\n", renderer.render(snippet));
600 }
601
602 fn get_title_label(diagnostics: &Diagnostic, label: &mut String) {
603 label.clear();
604 if let Some(reason) = diagnostics.reason() {
605 label.push_str(reason.description());
606 }
607 }
608
609 fn diagnostic_level_to_annotation_type(level: Level) -> AnnotationType {
610 match level {
611 Level::Info => AnnotationType::Info,
612 Level::Warning => AnnotationType::Warning,
613 Level::Error => AnnotationType::Error,
614 }
615 }
616}
617
618fn construct_slice(labels: Vec<&Label>) -> Slice {
619 debug_assert!(
620 !labels.is_empty(),
621 "To construct slices, at least one label must be provided."
622 );
623
624 debug_assert!(
625 labels.iter().all(|label| label.is_in_source()),
626 "Slices can be constructed only for labels that are related to a place in source code."
627 );
628
629 debug_assert!(
630 HashSet::<&str>::from_iter(labels.iter().map(|label| label.source_path().unwrap().as_str())).len() == 1,
631 "Slices can be constructed only for labels that are related to places in the same source code."
632 );
633
634 let source_file = labels[0].source_path().map(|path| path.as_str());
635 let source_code = labels[0].span().input();
636
637 let span = Span::join_all(labels.iter().map(|label| label.span().clone()));
639
640 let (source, line_start, shift_in_bytes) = construct_code_snippet(&span, source_code);
641
642 let mut annotations = vec![];
643
644 for message in labels {
645 annotations.push(SourceAnnotation {
646 label: message.text(),
647 annotation_type: label_type_to_annotation_type(message.label_type()),
648 range: get_annotation_range(message.span(), source_code, shift_in_bytes),
649 });
650 }
651
652 return Slice {
653 source,
654 line_start,
655 origin: source_file,
656 fold: true,
657 annotations,
658 };
659
660 fn get_annotation_range(
661 span: &Span,
662 source_code: &str,
663 shift_in_bytes: usize,
664 ) -> (usize, usize) {
665 let mut start_pos = span.start();
666 let mut end_pos = span.end();
667
668 let start_ix_bytes = start_pos - std::cmp::min(shift_in_bytes, start_pos);
669 let end_ix_bytes = end_pos - std::cmp::min(shift_in_bytes, end_pos);
670
671 start_pos = source_code[shift_in_bytes..(shift_in_bytes + start_ix_bytes)]
673 .chars()
674 .count();
675 end_pos = source_code[shift_in_bytes..(shift_in_bytes + end_ix_bytes)]
676 .chars()
677 .count();
678
679 (start_pos, end_pos)
680 }
681}
682
683fn label_type_to_annotation_type(label_type: LabelType) -> AnnotationType {
684 match label_type {
685 LabelType::Info => AnnotationType::Info,
686 LabelType::Help => AnnotationType::Help,
687 LabelType::Warning => AnnotationType::Warning,
688 LabelType::Error => AnnotationType::Error,
689 }
690}
691
692fn construct_code_snippet<'a>(span: &Span, input: &'a str) -> (&'a str, usize, usize) {
701 const NUM_LINES_BUFFER: usize = 2;
703
704 let LineColRange { start, end } = span.line_col_one_index();
705
706 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
707 debug_assert!(end.line >= start.line);
708 let total_lines_of_highlight = end.line - start.line;
709 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
710
711 let mut current_line = 0;
712 let mut lines_to_start_of_snippet = 0;
713 let mut calculated_start_ix = None;
714 let mut calculated_end_ix = None;
715 let mut pos = 0;
716 for character in input.chars() {
717 if character == '\n' {
718 current_line += 1
719 }
720
721 if current_line + NUM_LINES_BUFFER >= start.line && calculated_start_ix.is_none() {
722 calculated_start_ix = Some(pos);
723 lines_to_start_of_snippet = current_line;
724 }
725
726 if current_line >= end.line + NUM_LINES_BUFFER && calculated_end_ix.is_none() {
727 calculated_end_ix = Some(pos);
728 }
729
730 if calculated_start_ix.is_some() && calculated_end_ix.is_some() {
731 break;
732 }
733 pos += character.len_utf8();
734 }
735 let calculated_start_ix = calculated_start_ix.unwrap_or(0);
736 let calculated_end_ix = calculated_end_ix.unwrap_or(input.len());
737
738 (
739 &input[calculated_start_ix..calculated_end_ix],
740 lines_to_start_of_snippet,
741 calculated_start_ix,
742 )
743}
744
745fn construct_window<'a>(
754 start: &mut LineCol,
755 end: LineCol,
756 start_ix: &mut usize,
757 end_ix: &mut usize,
758 input: &'a str,
759) -> &'a str {
760 const NUM_LINES_BUFFER: usize = 2;
762
763 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
764 debug_assert!(end.line >= start.line);
765 let total_lines_of_highlight = end.line - start.line;
766 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
767
768 let mut current_line = 1usize;
769
770 let mut chars = input.char_indices().map(|(char_offset, character)| {
771 let r = (current_line, char_offset);
772 if character == '\n' {
773 current_line += 1;
774 }
775 r
776 });
777
778 let first_char = chars
780 .by_ref()
781 .find(|(current_line, _)| current_line + NUM_LINES_BUFFER >= start.line);
782
783 let last_char = chars
785 .by_ref()
786 .find(|(current_line, _)| *current_line > end.line + NUM_LINES_BUFFER)
787 .map(|x| x.1);
788
789 drop(chars);
791
792 let (first_char_line, first_char_offset, last_char_offset) = match (first_char, last_char) {
793 (Some((first_char_line, first_char_offset)), Some(last_char_offset)) => {
795 (first_char_line, first_char_offset, last_char_offset)
796 }
797 (Some((first_char_line, first_char_offset)), None) => {
799 (first_char_line, first_char_offset, input.len())
800 }
801 _ => (current_line, input.len(), input.len()),
803 };
804
805 start.line = first_char_line;
807 *start_ix = start_ix.saturating_sub(first_char_offset);
808 *end_ix = end_ix.saturating_sub(first_char_offset);
809
810 &input[first_char_offset..last_char_offset]
811}
812
813#[test]
814fn ok_construct_window() {
815 fn t(
816 start_line: usize,
817 start_col: usize,
818 end_line: usize,
819 end_col: usize,
820 start_char: usize,
821 end_char: usize,
822 input: &str,
823 ) -> (usize, usize, &str) {
824 let mut s = LineCol {
825 line: start_line,
826 col: start_col,
827 };
828 let mut start = start_char;
829 let mut end = end_char;
830 let r = construct_window(
831 &mut s,
832 LineCol {
833 line: end_line,
834 col: end_col,
835 },
836 &mut start,
837 &mut end,
838 input,
839 );
840 (start, end, r)
841 }
842
843 assert_eq!(t(0, 0, 0, 0, 0, 0, ""), (0, 0, ""));
845
846 assert_eq!(t(1, 1, 1, 1, 0, 0, ""), (0, 0, ""));
848
849 assert_eq!(t(1, 7, 1, 7, 6, 6, "script"), (6, 6, "script"));
851
852 let eight_lines = "1\n2\n3\n4\n5\n6\n7\n8";
854
855 assert_eq!(t(1, 1, 1, 1, 0, 1, eight_lines), (0, 1, "1\n2\n3\n"));
856 assert_eq!(t(2, 1, 2, 1, 2, 3, eight_lines), (2, 3, "1\n2\n3\n4\n"));
857 assert_eq!(t(3, 1, 3, 1, 4, 5, eight_lines), (4, 5, "1\n2\n3\n4\n5\n"));
858 assert_eq!(t(4, 1, 4, 1, 6, 7, eight_lines), (4, 5, "2\n3\n4\n5\n6\n"));
859 assert_eq!(t(5, 1, 5, 1, 8, 9, eight_lines), (4, 5, "3\n4\n5\n6\n7\n"));
860 assert_eq!(t(6, 1, 6, 1, 10, 11, eight_lines), (4, 5, "4\n5\n6\n7\n8"));
861 assert_eq!(t(7, 1, 7, 1, 12, 13, eight_lines), (4, 5, "5\n6\n7\n8"));
862 assert_eq!(t(8, 1, 8, 1, 14, 15, eight_lines), (4, 5, "6\n7\n8"));
863
864 assert_eq!(t(9, 1, 9, 1, 14, 15, eight_lines), (2, 3, "7\n8"));
866 assert_eq!(t(10, 1, 10, 1, 14, 15, eight_lines), (0, 1, "8"));
867 assert_eq!(t(11, 1, 11, 1, 14, 15, eight_lines), (0, 0, ""));
868}