1use std::{fmt, io, path::Path};
2
3use zng_txt::{ToTxt, Txt};
4
5#[derive(Clone, PartialEq, Debug)]
7pub struct SfxClient {
8 sfx_args: Box<[Txt]>,
9 manifest: Box<[SfxDataInfo]>,
10}
11impl SfxClient {
12 pub fn can_connect() -> bool {
16 match std::env::var("SFX_ARGS") {
17 Ok(_) => true,
18 Err(e) => !SfxError::from(e).is_no_sfx(),
19 }
20 }
21
22 pub fn connect_blocking() -> Result<Self, SfxError> {
24 let args = std::env::var("SFX_ARGS")?;
26 let mut sfx_args = vec![];
27 for arg in args.split('\n') {
28 let arg = arg.trim();
29 if arg.is_empty() {
30 continue;
31 }
32 sfx_args.push(Txt::from_str(arg));
33 }
34
35 if sfx_args.is_empty() {
36 return Err(io::Error::new(io::ErrorKind::InvalidFilename, "SFX_ARGS is empty").into());
37 }
38
39 #[cfg(feature = "test_util")]
40 if sfx_args[0] == test_util::FAKE_EXE {
41 return Ok(Self {
42 sfx_args: sfx_args.into_boxed_slice(),
43 manifest: test_util::test_manifest(),
44 });
45 }
46
47 let out = std::process::Command::new(&sfx_args[0]).env("SFX_GET_MANIFEST", "").output()?;
49 if !out.status.success() {
50 let err = String::from_utf8_lossy(&out.stderr);
51 return Err(io::Error::other(format!("SFX_GET_MANIFEST failed\ncode: {:?}\nstderr:\n{}", out.status, err)).into());
52 }
53 let stdout = match String::from_utf8(out.stdout) {
54 Ok(s) => s,
55 Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e).into()),
56 };
57 let mut manifest = vec![];
58 for line in stdout.lines() {
59 let mut ok = line.is_empty();
60 if let Some((name, len)) = line.rsplit_once(':') {
61 if let Ok(len) = len.parse() {
62 ok = true;
63 manifest.push(SfxDataInfo {
64 name: name.to_txt(),
65 exact_len: Some(len),
66 });
67 } else if len == "unknown" {
68 ok = true;
69 manifest.push(SfxDataInfo {
70 name: name.to_txt(),
71 exact_len: None,
72 });
73 }
74 }
75 if !ok {
76 return Err(io::Error::new(io::ErrorKind::InvalidData, format!("unexpected manifest format, {line:?}")).into());
77 }
78 }
79 manifest.sort_by(|a, b| a.name.cmp(&b.name));
80 manifest.dedup_by(|a, b| a.name == b.name);
81
82 Ok(Self {
83 sfx_args: sfx_args.into_boxed_slice(),
84 manifest: manifest.into_boxed_slice(),
85 })
86 }
87
88 pub async fn connect() -> Result<Self, SfxError> {
90 zng_task::wait(Self::connect_blocking).await
91 }
92
93 pub fn sfx_exe(&self) -> &Path {
97 Path::new(&self.sfx_args[0])
98 }
99
100 pub fn sfx_args(&self) -> &[Txt] {
107 &self.sfx_args
108 }
109
110 pub fn manifest(&self) -> &[SfxDataInfo] {
114 &self.manifest
115 }
116
117 pub fn info(&self, name: &str) -> Option<&SfxDataInfo> {
119 let i = self.manifest.binary_search_by_key(&name, |n| n.name.as_str()).ok()?;
120 Some(&self.manifest[i])
121 }
122
123 pub fn read_blocking(&self, name: &str) -> Result<SfxReadBlocking, SfxError> {
127 if let Some(info) = self.info(name) {
128 #[cfg(feature = "test_util")]
129 if self.sfx_args[0] == test_util::FAKE_EXE {
130 let data = test_util::req_data(name);
131 return Ok(SfxReadBlocking(SfxReadBlockingImpl::Test(std::io::Cursor::new(data))));
132 }
133
134 let mut r = std::process::Command::new(self.sfx_exe()).env("SFX_GET_DATA", name).spawn()?;
135 r.stdin = None;
136 Ok(SfxReadBlocking(SfxReadBlockingImpl::Default(DefaultSfxReadBlocking {
137 server: r,
138 exact_len: info.exact_len,
139 })))
140 } else {
141 Err(SfxError::NotFound(Txt::from_str(name)))
142 }
143 }
144
145 pub async fn read(&self, name: &str) -> Result<SfxRead, SfxError> {
149 if let Some(info) = self.info(name) {
150 #[cfg(feature = "test_util")]
151 if self.sfx_args[0] == test_util::FAKE_EXE {
152 let data = test_util::req_data(name);
153 let inner = SfxReadBlocking(SfxReadBlockingImpl::Test(std::io::Cursor::new(data)));
154 return Ok(SfxRead {
155 inner: zng_task::io::Unblock::new(inner),
156 });
157 }
158
159 let mut cmd = std::process::Command::new(self.sfx_exe());
160 cmd.env("SFX_GET_DATA", name);
161 let exact_len = info.exact_len;
162 zng_task::wait(move || {
163 let mut r = cmd.spawn()?;
164 r.stdin = None;
165 let inner = SfxReadBlocking(SfxReadBlockingImpl::Default(DefaultSfxReadBlocking { server: r, exact_len }));
166 Ok(SfxRead {
167 inner: zng_task::io::Unblock::new(inner),
168 })
169 })
170 .await
171 } else {
172 Err(SfxError::NotFound(Txt::from_str(name)))
173 }
174 }
175}
176#[cfg(feature = "test_util")]
177mod test_util {
178 use super::*;
179
180 pub const FAKE_EXE: &str = "test:/fake-sfx";
181
182 impl SfxClient {
183 pub fn setup_test(test_args: Vec<Txt>, mut test_data: Vec<(Txt, Vec<u8>)>) {
195 test_data.sort_by(|a, b| a.0.cmp(&b.0));
196 test_data.dedup_by(|a, b| a.0 == b.0);
197
198 zng_unique_id::lazy_static_init(&TEST_DATA, Some(test_data)).unwrap_or_else(|_| panic!("SfxClient already used"));
199
200 let mut args = test_args;
201 args.insert(0, FAKE_EXE.into());
202 unsafe {
203 std::env::set_var("SFX_ARGS", args.join("\n"));
204 }
205 }
206 }
207
208 type TestData = Vec<(Txt, Vec<u8>)>;
209 zng_unique_id::lazy_static! {
210 static ref TEST_DATA: Option<TestData> = None;
211 }
212
213 pub(super) fn test_manifest() -> Box<[SfxDataInfo]> {
214 if let Some(data) = &*TEST_DATA {
215 data.iter()
216 .map(|(n, b)| SfxDataInfo {
217 name: n.clone(),
218 exact_len: Some(b.len() as u64),
219 })
220 .collect()
221 } else {
222 Box::new([])
223 }
224 }
225
226 pub(super) fn req_data(name: &str) -> &'static [u8] {
227 if let Some(data) = &*TEST_DATA {
228 for (n, entry) in data {
229 if n == name {
230 return entry;
231 }
232 }
233 }
234 panic!()
235 }
236}
237
238#[derive(Clone, PartialEq, Debug)]
240#[non_exhaustive]
241pub struct SfxDataInfo {
242 pub name: Txt,
244
245 pub exact_len: Option<u64>,
250}
251
252pub struct SfxReadBlocking(SfxReadBlockingImpl);
256enum SfxReadBlockingImpl {
257 Default(DefaultSfxReadBlocking),
258 #[cfg(feature = "test_util")]
259 Test(std::io::Cursor<&'static [u8]>),
260}
261impl SfxReadBlocking {
262 pub fn exact_len(&self) -> Option<u64> {
266 match &self.0 {
267 SfxReadBlockingImpl::Default(b) => b.exact_len,
268 #[cfg(feature = "test_util")]
269 SfxReadBlockingImpl::Test(c) => Some(c.get_ref().len() as u64 - c.position()),
270 }
271 }
272}
273struct DefaultSfxReadBlocking {
274 server: std::process::Child,
276 exact_len: Option<u64>,
277}
278impl io::Read for DefaultSfxReadBlocking {
279 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
280 if let Some(s) = &mut self.server.stdout {
281 let n = s.read(buf)?;
282 if n == 0 {
283 self.server.stdout = None;
284 self.server.kill()?;
285 let status = self.server.wait()?;
286 if !status.success() {
287 let mut stderr = String::new();
288 if let Some(mut s) = self.server.stderr.take() {
289 s.read_to_string(&mut stderr)?;
290 }
291 return Err(io::Error::new(
292 io::ErrorKind::ConnectionAborted,
293 format!("server failed\nexit code: {status:?}\nstderr:\n{stderr}"),
294 ));
295 }
296 if let Some(l) = self.exact_len
297 && l > 0
298 {
299 return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "stdout ended before exact length"));
300 }
301 } else if let Some(l) = &mut self.exact_len {
302 let n = n as u64;
303 if *l >= n {
304 *l -= n;
305 } else {
306 self.server.stdout = None;
307 let _ = self.server.kill();
308 return Err(io::Error::other("stdout longer than expected exact length"));
309 }
310 }
311 Ok(n)
312 } else {
313 Ok(0)
314 }
315 }
316}
317impl io::Read for SfxReadBlocking {
318 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
319 match &mut self.0 {
320 SfxReadBlockingImpl::Default(r) => r.read(buf),
321 #[cfg(feature = "test_util")]
322 SfxReadBlockingImpl::Test(c) => c.read(buf),
323 }
324 }
325
326 fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
327 if let Some(l) = self.exact_len() {
328 let l = l.min(usize::MAX as u64) as usize;
329 buf.try_reserve(l)?;
330 }
331 match &mut self.0 {
332 SfxReadBlockingImpl::Default(r) => r.read_to_end(buf),
333 #[cfg(feature = "test_util")]
334 SfxReadBlockingImpl::Test(c) => c.read_to_end(buf),
335 }
336 }
337
338 fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
339 if let Some(l) = self.exact_len() {
340 let l = l.min(usize::MAX as u64) as usize;
341 buf.try_reserve(l)?;
342 }
343 match &mut self.0 {
344 SfxReadBlockingImpl::Default(r) => r.read_to_string(buf),
345 #[cfg(feature = "test_util")]
346 SfxReadBlockingImpl::Test(c) => c.read_to_string(buf),
347 }
348 }
349}
350impl Drop for SfxReadBlocking {
351 fn drop(&mut self) {
352 if let SfxReadBlockingImpl::Default(d) = &mut self.0
353 && let Err(e) = d.server.kill()
354 {
355 tracing::error!("cannot kill server on drop, {e}");
356 }
357 }
358}
359
360pub struct SfxRead {
364 inner: zng_task::io::Unblock<SfxReadBlocking>,
365}
366impl SfxRead {
367 pub async fn exact_len(&mut self) -> Option<u64> {
371 self.inner.get_mut().await.exact_len()
372 }
373
374 pub async fn into_blocking(self) -> SfxReadBlocking {
376 self.inner.into_inner().await
377 }
378}
379impl zng_task::io::AsyncRead for SfxRead {
380 fn poll_read(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut [u8]) -> std::task::Poll<io::Result<usize>> {
381 std::pin::pin!(&mut self.get_mut().inner).poll_read(cx, buf)
382 }
383}
384
385#[derive(Debug)]
387#[non_exhaustive]
388pub enum SfxError {
389 Var(std::env::VarError),
391 Io(io::Error),
393 NotFound(Txt),
395}
396impl SfxError {
397 pub fn is_no_sfx(&self) -> bool {
401 matches!(self, Self::Var(std::env::VarError::NotPresent))
402 }
403}
404impl From<std::env::VarError> for SfxError {
405 fn from(var: std::env::VarError) -> Self {
406 Self::Var(var)
407 }
408}
409impl From<io::Error> for SfxError {
410 fn from(e: io::Error) -> Self {
411 Self::Io(e)
412 }
413}
414impl fmt::Display for SfxError {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 match self {
417 SfxError::Var(e) => fmt::Display::fmt(e, f),
418 SfxError::Io(e) => fmt::Display::fmt(e, f),
419 SfxError::NotFound(name) => write!(f, "no {name:?} data on the server catalog"),
420 }
421 }
422}
423impl std::error::Error for SfxError {
424 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
425 match self {
426 SfxError::Var(e) => Some(e),
427 SfxError::Io(e) => Some(e),
428 SfxError::NotFound(_) => None,
429 }
430 }
431}