1use std::io::{self, Read, Write};
24use std::panic::{AssertUnwindSafe, catch_unwind};
25use std::path::Path;
26
27use ktrs_fmt::{FormatError, FormattingOptions, TrailingCommaManagementStrategy};
28
29use crate::ktfmt::editor_config_resolver;
30use crate::ktrs::style;
31
32pub const PROTOCOL_VERSION: u32 = 1;
33const MAX_FRAME: usize = 256 << 20;
35const UTF8_BOM: char = '\u{feff}';
36
37pub fn run(input: impl Read, output: impl Write) -> i32 {
38 let (mut input, mut output) = (io::BufReader::new(input), io::BufWriter::new(output));
39 let hello = format!("ktrs-serve {PROTOCOL_VERSION} {}", env!("CARGO_PKG_VERSION"));
40 let result = write_frame(&mut output, hello.as_bytes()).and_then(|()| {
41 while let Some(request) = read_frame(&mut input)? {
42 write_frame(&mut output, &respond(&request))?;
43 }
44 Ok(())
45 });
46 match result {
47 Ok(()) => 0,
48 Err(e) => {
49 eprintln!("ktrs serve: {e}");
50 2
51 }
52 }
53}
54
55fn read_frame(input: &mut impl Read) -> io::Result<Option<Vec<u8>>> {
56 let mut length = [0; 4];
57 match input.read_exact(&mut length) {
58 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
59 result => result?,
60 }
61 let length = u32::from_be_bytes(length) as usize;
62 if length > MAX_FRAME {
63 return Err(io::Error::new(io::ErrorKind::InvalidData, format!("frame of {length} bytes")));
64 }
65 let mut frame = vec![0; length];
66 input.read_exact(&mut frame)?;
67 Ok(Some(frame))
68}
69
70fn write_frame(output: &mut impl Write, payload: &[u8]) -> io::Result<()> {
71 output.write_all(&(payload.len() as u32).to_be_bytes())?;
72 output.write_all(payload)?;
73 output.flush()
74}
75
76fn respond(request: &[u8]) -> Vec<u8> {
77 match format_request(&String::from_utf8_lossy(request)) {
78 Ok((formatted, changed)) => format!("status=ok\nchanged={changed}\n\n{formatted}").into_bytes(),
79 Err(message) => format!("status=error\n\n{message}").into_bytes(),
80 }
81}
82
83fn format_request(request: &str) -> Result<(String, bool), String> {
85 let (header, code) = match request.strip_prefix('\n') {
86 Some(code) => ("", code),
87 None => request.split_once("\n\n").ok_or("request has no empty line after its header")?,
88 };
89 let request = Request::parse(header)?;
90 let code = code.strip_prefix(UTF8_BOM).unwrap_or(code);
91 let options = match &request.path {
92 Some(path) if request.editorconfig => editor_config_resolver::resolve_formatting_options(Path::new(path), &request.options),
93 _ => request.options,
94 };
95 let name = request.path.as_deref();
96 match catch_unwind(AssertUnwindSafe(|| ktrs_fmt::format(code, &options))) {
97 Ok(Ok(formatted)) => {
98 let changed = formatted != code;
99 Ok((formatted, changed))
100 }
101 Ok(Err(e)) => Err(error_message(name, &e)),
102 Err(_) => Err(located(name, " internal error in ktrs (please report it with this file)")),
103 }
104}
105
106struct Request {
107 options: FormattingOptions,
108 path: Option<String>,
109 editorconfig: bool,
110}
111
112impl Request {
113 fn parse(header: &str) -> Result<Request, String> {
114 let fields: Vec<(&str, &str)> = header
115 .lines()
116 .map(|line| line.split_once('=').ok_or_else(|| format!("header line without '=': {line}")))
117 .collect::<Result<_, _>>()?;
118 let style_name = fields.iter().rev().find(|(key, _)| *key == "style").map_or("meta", |(_, value)| value);
120 let mut request = Request { options: style(style_name)?, path: None, editorconfig: false };
121 for (key, value) in fields {
122 let options = &mut request.options;
123 match key {
124 "style" => {}
125 "max-width" => options.max_width = positive(key, value)?,
126 "block-indent" => options.block_indent = positive(key, value)?,
127 "continuation-indent" => options.continuation_indent = positive(key, value)?,
128 "remove-unused-imports" => options.remove_unused_imports = boolean(key, value)?,
129 "trailing-commas" => options.trailing_comma_management_strategy = trailing_commas(value)?,
130 "path" => request.path = Some(value.to_owned()),
131 "editorconfig" => request.editorconfig = boolean(key, value)?,
132 _ => return Err(format!("unknown request key '{key}'")),
133 }
134 }
135 Ok(request)
136 }
137}
138
139fn positive(key: &str, value: &str) -> Result<i32, String> {
140 value.parse().ok().filter(|&n: &i32| n > 0).ok_or_else(|| format!("{key} must be a positive integer, got '{value}'"))
141}
142
143fn boolean(key: &str, value: &str) -> Result<bool, String> {
144 value.parse().map_err(|_| format!("{key} must be true or false, got '{value}'"))
145}
146
147fn trailing_commas(value: &str) -> Result<TrailingCommaManagementStrategy, String> {
148 match value {
149 "none" => Ok(TrailingCommaManagementStrategy::None),
150 "only_add" => Ok(TrailingCommaManagementStrategy::OnlyAdd),
151 "complete" => Ok(TrailingCommaManagementStrategy::Complete),
152 _ => Err(format!("trailing-commas must be none, only_add or complete, got '{value}'")),
153 }
154}
155
156fn error_message(name: Option<&str>, error: &FormatError) -> String {
159 match error {
160 FormatError::Parse(e) => located(name, &e.to_string()),
161 FormatError::Formatting(e) => e.diagnostics().iter().map(|d| located(name, &d.to_string())).collect::<Vec<_>>().join("\n"),
162 FormatError::Formatter(_) | FormatError::Runtime(_) => located(name, &format!(" {error}")),
163 }
164}
165
166fn located(name: Option<&str>, message: &str) -> String {
167 match name {
168 Some(name) => format!("{name}:{message}"),
169 None => message.trim_start().to_string(),
170 }
171}