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