1use ipp::attribute::{IppAttribute, IppAttributes};
4use ipp::model::DelimiterTag;
5use ipp::prelude::*;
6use ipp::request::IppRequestResponse;
7use ipp::value::IppValue;
8
9use crate::printer::{IppPrinterState, PrinterRecord};
10
11fn kw(s: &str) -> IppValue {
12 IppValue::Keyword(s.try_into().expect("keyword"))
13}
14
15fn mime(s: &str) -> IppValue {
16 IppValue::MimeMediaType(s.try_into().expect("mime"))
17}
18
19fn uri(s: &str) -> IppValue {
20 IppValue::Uri(s.try_into().expect("uri"))
21}
22
23fn charset(s: &str) -> IppValue {
24 IppValue::Charset(s.try_into().expect("charset"))
25}
26
27fn lang(s: &str) -> IppValue {
28 IppValue::NaturalLanguage(s.try_into().expect("language"))
29}
30
31fn attr(name: &str, value: IppValue) -> IppAttribute {
32 IppAttribute::new(name.try_into().expect("attr name"), value)
33}
34
35fn add(attrs: &mut IppAttributes, tag: DelimiterTag, name: &str, value: IppValue) {
36 attrs.add(tag, attr(name, value));
37}
38
39fn add_array_keyword(attrs: &mut IppAttributes, tag: DelimiterTag, name: &str, items: &[&str]) {
40 let values: Vec<IppValue> = items.iter().map(|s| kw(s)).collect();
41 add(attrs, tag, name, IppValue::Array(values));
42}
43
44fn advertise_host(host: &str) -> &str {
46 if host == "0.0.0.0" || host == "::" || host.is_empty() {
47 "localhost"
48 } else {
49 host
50 }
51}
52
53fn printer_uri(record: &PrinterRecord, host: &str, port: u16) -> String {
55 format!(
56 "ipp://{}:{}/ipp/print/{}",
57 advertise_host(host),
58 port,
59 record.config.name
60 )
61}
62
63pub fn get_printer_attributes(
65 version: IppVersion,
66 request_id: u32,
67 record: &PrinterRecord,
68 host: &str,
69 port: u16,
70) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
71 let mut resp =
72 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
73 let attrs = resp.attributes_mut();
74 let cfg = &record.config;
75 let printer_uri_str = printer_uri(record, host, port);
76 let more_info = format!(
77 "http://{}:{}/",
78 advertise_host(host),
79 port
80 );
81
82 let p = DelimiterTag::PrinterAttributes;
83 add(attrs, p, "printer-uri-supported", uri(&printer_uri_str));
84 add(attrs, p, "uri-authentication-supported", kw("none"));
85 add(attrs, p, "uri-security-supported", kw("none"));
86 add(
87 attrs,
88 p,
89 "printer-name",
90 IppValue::NameWithoutLanguage(cfg.name.as_str().try_into().unwrap()),
91 );
92 add(
93 attrs,
94 p,
95 "printer-location",
96 IppValue::TextWithoutLanguage("".try_into().unwrap()),
97 );
98 add(
99 attrs,
100 p,
101 "printer-info",
102 IppValue::TextWithoutLanguage(cfg.make_and_model.as_str().try_into().unwrap()),
103 );
104 add(
105 attrs,
106 p,
107 "printer-make-and-model",
108 IppValue::TextWithoutLanguage(cfg.make_and_model.as_str().try_into().unwrap()),
109 );
110 add(attrs, p, "printer-more-info", uri(&more_info));
111 add(
112 attrs,
113 p,
114 "printer-uuid",
115 uri(&format!("urn:uuid:{}", record.uuid)),
116 );
117 add(
118 attrs,
119 p,
120 "printer-up-time",
121 IppValue::Integer(uptime_secs() as i32),
122 );
123
124 add(attrs, p, "printer-state", IppValue::Enum(record.state as i32));
125 let reason_kws: Vec<&str> = record.reasons.ipp_keywords();
126 add_array_keyword(attrs, p, "printer-state-reasons", &reason_kws);
127 add(attrs, p, "printer-is-accepting-jobs", IppValue::Boolean(true));
128 add(attrs, p, "queued-job-count", IppValue::Integer(0));
129
130 add_array_keyword(attrs, p, "ipp-versions-supported", &["1.1", "2.0", "2.1"]);
131 add_array_keyword(
132 attrs,
133 p,
134 "operations-supported",
135 &[
136 "Print-Job",
137 "Validate-Job",
138 "Get-Printer-Attributes",
139 "Get-Jobs",
140 "Get-Job-Attributes",
141 "Cancel-Job",
142 ],
143 );
144 add(
145 attrs,
146 p,
147 "charset-configured",
148 charset("utf-8"),
149 );
150 add(
151 attrs,
152 p,
153 "charset-supported",
154 IppValue::Array(vec![charset("utf-8")]),
155 );
156 add(attrs, p, "natural-language-configured", lang("en"));
157 add(
158 attrs,
159 p,
160 "natural-language-supported",
161 IppValue::Array(vec![lang("en")]),
162 );
163 add(
164 attrs,
165 p,
166 "generated-natural-language-supported",
167 IppValue::Array(vec![lang("en")]),
168 );
169 add_array_keyword(attrs, p, "compression-supported", &["none"]);
170
171 add(
174 attrs,
175 p,
176 "document-format-supported",
177 IppValue::Array(vec![
178 mime("image/pwg-raster"),
179 mime("application/vnd.cups-raster"),
180 mime("application/octet-stream"),
181 ]),
182 );
183 add(
184 attrs,
185 p,
186 "document-format-default",
187 mime("image/pwg-raster"),
188 );
189 add_array_keyword(
191 attrs,
192 p,
193 "pwg-raster-document-type-supported",
194 &["black_1"],
195 );
196 add(
197 attrs,
198 p,
199 "pwg-raster-document-resolution-supported",
200 IppValue::Array(vec![IppValue::Resolution {
201 cross_feed: cfg.dpi,
202 feed: cfg.dpi,
203 units: 3,
204 }]),
205 );
206 add_array_keyword(
207 attrs,
208 p,
209 "urf-supported",
210 &["W8", "SRGB24", "CP1", "RS203"],
211 );
212
213 add(attrs, p, "color-supported", IppValue::Boolean(false));
214 add_array_keyword(attrs, p, "print-color-mode-supported", &["monochrome"]);
215 add(attrs, p, "print-color-mode-default", kw("monochrome"));
216 add_array_keyword(attrs, p, "sides-supported", &["one-sided"]);
217 add(attrs, p, "sides-default", kw("one-sided"));
218 add(attrs, p, "orientation-requested-default", IppValue::Enum(3));
219
220 add(
221 attrs,
222 p,
223 "printer-resolution-default",
224 IppValue::Resolution {
225 cross_feed: cfg.dpi,
226 feed: cfg.dpi,
227 units: 3,
228 },
229 );
230 add(
231 attrs,
232 p,
233 "printer-resolution-supported",
234 IppValue::Array(vec![IppValue::Resolution {
235 cross_feed: cfg.dpi,
236 feed: cfg.dpi,
237 units: 3,
238 }]),
239 );
240
241 let media_kws: Vec<&str> = cfg.media_names.iter().map(|s| s.as_str()).collect();
242 if !media_kws.is_empty() {
243 add(attrs, p, "media-default", kw(media_kws[0]));
244 add_array_keyword(attrs, p, "media-supported", &media_kws);
245
246 let default_size = cfg.media_sizes.first().copied().unwrap_or([4000, 3000]);
248 add(
249 attrs,
250 p,
251 "media-col-default",
252 media_col(media_kws[0], default_size),
253 );
254 let media_cols: Vec<IppValue> = media_kws
255 .iter()
256 .zip(cfg.media_sizes.iter().copied().chain(std::iter::repeat(default_size)))
257 .map(|(name, size)| media_col(name, size))
258 .collect();
259 add(attrs, p, "media-col-supported", IppValue::Array(media_cols));
260 }
261
262 add(
263 attrs,
264 p,
265 "copies-supported",
266 IppValue::RangeOfInteger { min: 1, max: 999 },
267 );
268 add(attrs, p, "copies-default", IppValue::Integer(1));
269 add(
270 attrs,
271 p,
272 "print-quality-supported",
273 IppValue::Array(vec![
274 IppValue::Enum(3),
275 IppValue::Enum(4),
276 IppValue::Enum(5),
277 ]),
278 );
279 add(attrs, p, "print-quality-default", IppValue::Enum(4));
280
281 Ok(resp)
282}
283
284pub fn validate_job(
286 version: IppVersion,
287 request_id: u32,
288 record: &PrinterRecord,
289 host: &str,
290 port: u16,
291) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
292 get_printer_attributes(version, request_id, record, host, port)
293}
294
295pub fn print_job_accepted(
297 version: IppVersion,
298 request_id: u32,
299 job: &crate::job::JobRecord,
300 printer_uri_str: &str,
301) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
302 let mut resp =
303 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
304 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
305 let j = DelimiterTag::JobAttributes;
306 add(resp.attributes_mut(), j, "job-uri", uri(&job_uri_str));
307 add(
308 resp.attributes_mut(),
309 j,
310 "job-id",
311 IppValue::Integer(job.id as i32),
312 );
313 add(
314 resp.attributes_mut(),
315 j,
316 "job-state",
317 IppValue::Enum(job.state as i32),
318 );
319 add_array_keyword(
320 resp.attributes_mut(),
321 j,
322 "job-state-reasons",
323 &job_state_reason_keywords(job),
324 );
325 Ok(resp)
326}
327
328pub fn build_job_attrs_response(
330 version: IppVersion,
331 request_id: u32,
332 job: &crate::job::JobRecord,
333 printer_uri_str: &str,
334) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
335 let mut resp =
336 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
337 append_job_attrs(resp.attributes_mut(), job, printer_uri_str);
338 Ok(resp)
339}
340
341pub fn build_get_jobs_response(
343 version: IppVersion,
344 request_id: u32,
345 jobs: &[crate::job::JobRecord],
346 printer_uri_str: &str,
347) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
348 let mut resp =
349 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
350 for job in jobs {
354 let mut group = ipp::attribute::IppAttributeGroup::new(DelimiterTag::JobAttributes);
355 for a in job_attrs_for_group(job, printer_uri_str) {
356 group
357 .attributes_mut()
358 .insert(a.name().to_owned(), a);
359 }
360 resp.attributes_mut().groups_mut().push(group);
361 }
362 Ok(resp)
363}
364
365fn append_job_attrs(
366 attrs: &mut IppAttributes,
367 job: &crate::job::JobRecord,
368 printer_uri_str: &str,
369) {
370 for a in job_attrs_for_group(job, printer_uri_str) {
371 attrs.add(DelimiterTag::JobAttributes, a);
372 }
373}
374
375fn job_attrs_for_group(
376 job: &crate::job::JobRecord,
377 printer_uri_str: &str,
378) -> Vec<IppAttribute> {
379 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
380 let mut out = vec![
381 attr("job-uri", uri(&job_uri_str)),
382 attr("job-id", IppValue::Integer(job.id as i32)),
383 attr("job-printer-uri", uri(printer_uri_str)),
384 attr(
385 "job-name",
386 IppValue::NameWithoutLanguage(
387 format!("job-{}", job.id).as_str().try_into().unwrap(),
388 ),
389 ),
390 attr("job-state", IppValue::Enum(job.state as i32)),
391 attr("time-at-creation", IppValue::Integer(job.created_secs())),
392 ];
393 let reason_kws = job_state_reason_keywords(job);
394 out.push(attr(
395 "job-state-reasons",
396 IppValue::Array(reason_kws.iter().map(|s| kw(s)).collect()),
397 ));
398 if !job.message.is_empty() {
399 out.push(attr(
400 "job-state-message",
401 IppValue::TextWithoutLanguage(job.message.as_str().try_into().unwrap()),
402 ));
403 }
404 if let Some(s) = job.completed_secs() {
405 out.push(attr("time-at-completed", IppValue::Integer(s)));
406 }
407 out
408}
409
410fn job_state_reason_keywords(job: &crate::job::JobRecord) -> Vec<&'static str> {
411 use crate::flags::PrinterReason;
412 use crate::job::JobState;
413 let mut out = Vec::new();
414 if job.reasons.contains(PrinterReason::MEDIA_EMPTY) {
415 out.push("job-completed-with-errors");
416 }
417 if job.reasons.contains(PrinterReason::MEDIA_JAM) {
418 out.push("aborted-by-system");
419 }
420 if job.reasons.contains(PrinterReason::OFFLINE) {
421 out.push("connection-error");
422 }
423 match job.state {
424 JobState::Canceled => out.push("job-canceled-by-user"),
425 JobState::Completed => out.push("job-completed-successfully"),
426 JobState::Aborted if out.is_empty() => out.push("aborted-by-system"),
427 _ => {}
428 }
429 if out.is_empty() {
430 out.push("none");
431 }
432 out
433}
434
435pub fn set_printer_processing(record: &mut PrinterRecord) {
437 record.state = IppPrinterState::Processing;
438}
439
440pub fn set_printer_idle(record: &mut PrinterRecord) {
442 record.state = IppPrinterState::Idle;
443}
444
445fn media_col(name: &str, size_hmm: [i32; 2]) -> IppValue {
448 use std::collections::BTreeMap;
449 let mut size = BTreeMap::new();
450 size.insert(
451 "x-dimension".try_into().unwrap(),
452 IppValue::Integer(size_hmm[0]),
453 );
454 size.insert(
455 "y-dimension".try_into().unwrap(),
456 IppValue::Integer(size_hmm[1]),
457 );
458 let mut col = BTreeMap::new();
459 col.insert(
460 "media-size".try_into().unwrap(),
461 IppValue::Collection(size),
462 );
463 col.insert(
464 "media-size-name".try_into().unwrap(),
465 kw(name),
466 );
467 IppValue::Collection(col)
468}
469
470fn uptime_secs() -> u64 {
471 use std::sync::OnceLock;
472 use std::time::Instant;
473 static START: OnceLock<Instant> = OnceLock::new();
474 START.get_or_init(Instant::now).elapsed().as_secs()
475}