1use std::collections::BTreeSet;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use ipp::attribute::{IppAttribute, IppAttributes};
7use ipp::model::DelimiterTag;
8use ipp::prelude::*;
9use ipp::request::IppRequestResponse;
10use ipp::value::IppValue;
11
12use crate::printer::{IppPrinterState, PrinterRecord};
13
14fn kw(s: &str) -> IppValue {
15 IppValue::Keyword(s.try_into().expect("keyword"))
16}
17
18fn mime(s: &str) -> IppValue {
19 IppValue::MimeMediaType(s.try_into().expect("mime"))
20}
21
22fn uri(s: &str) -> IppValue {
23 IppValue::Uri(s.try_into().expect("uri"))
24}
25
26fn charset(s: &str) -> IppValue {
27 IppValue::Charset(s.try_into().expect("charset"))
28}
29
30fn lang(s: &str) -> IppValue {
31 IppValue::NaturalLanguage(s.try_into().expect("language"))
32}
33
34fn attr(name: &str, value: IppValue) -> IppAttribute {
35 IppAttribute::new(name.try_into().expect("attr name"), value)
36}
37
38fn add(attrs: &mut IppAttributes, tag: DelimiterTag, name: &str, value: IppValue) {
39 attrs.add(tag, attr(name, value));
40}
41
42fn add_array_keyword(attrs: &mut IppAttributes, tag: DelimiterTag, name: &str, items: &[&str]) {
43 let values: Vec<IppValue> = items.iter().map(|s| kw(s)).collect();
44 add(attrs, tag, name, IppValue::Array(values));
45}
46
47fn text(s: &str) -> IppValue {
48 IppValue::TextWithoutLanguage(s.try_into().expect("text"))
49}
50
51fn add_array_enum(attrs: &mut IppAttributes, tag: DelimiterTag, name: &str, codes: &[i32]) {
52 let values: Vec<IppValue> = codes.iter().map(|c| IppValue::Enum(*c)).collect();
53 add(attrs, tag, name, IppValue::Array(values));
54}
55
56fn datetime_utc(unix_secs: i64) -> IppValue {
59 let days = unix_secs.div_euclid(86_400);
60 let rem = unix_secs.rem_euclid(86_400);
61 let (hour, minutes, seconds) = (rem / 3600, (rem % 3600) / 60, rem % 60);
62
63 let z = days + 719_468;
64 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
65 let doe = z - era * 146_097;
66 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
67 let year = yoe + era * 400;
68 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
69 let mp = (5 * doy + 2) / 153;
70 let day = doy - (153 * mp + 2) / 5 + 1;
71 let month = if mp < 10 { mp + 3 } else { mp - 9 };
72 let year = if month <= 2 { year + 1 } else { year };
73
74 IppValue::DateTime {
75 year: year as u16,
76 month: month as u8,
77 day: day as u8,
78 hour: hour as u8,
79 minutes: minutes as u8,
80 seconds: seconds as u8,
81 deci_seconds: 0,
82 utc_dir: '+',
83 utc_hours: 0,
84 utc_mins: 0,
85 }
86}
87
88fn now_unix() -> i64 {
89 SystemTime::now()
90 .duration_since(UNIX_EPOCH)
91 .map(|d| d.as_secs() as i64)
92 .unwrap_or(0)
93}
94
95fn advertise_host(host: &str) -> &str {
97 if host == "0.0.0.0" || host == "::" || host.is_empty() {
98 "localhost"
99 } else {
100 host
101 }
102}
103
104fn printer_uri(record: &PrinterRecord, host: &str, port: u16) -> String {
106 format!(
107 "ipp://{}:{}/ipp/print/{}",
108 advertise_host(host),
109 port,
110 record.config.name
111 )
112}
113
114pub fn get_printer_attributes(
121 version: IppVersion,
122 request_id: u32,
123 record: &PrinterRecord,
124 host: &str,
125 port: u16,
126 requested: Option<&BTreeSet<String>>,
127) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
128 let mut resp =
129 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
130 let attrs = resp.attributes_mut();
131 let cfg = &record.config;
132 let printer_uri_str = printer_uri(record, host, port);
133 let more_info = format!(
134 "http://{}:{}/",
135 advertise_host(host),
136 port
137 );
138
139 let p = DelimiterTag::PrinterAttributes;
140 add(attrs, p, "printer-uri-supported", uri(&printer_uri_str));
141 add(attrs, p, "uri-authentication-supported", kw("none"));
142 add(attrs, p, "uri-security-supported", kw("none"));
143 add(
144 attrs,
145 p,
146 "printer-name",
147 IppValue::NameWithoutLanguage(cfg.name.as_str().try_into().unwrap()),
148 );
149 add(
150 attrs,
151 p,
152 "printer-location",
153 IppValue::TextWithoutLanguage("".try_into().unwrap()),
154 );
155 add(
156 attrs,
157 p,
158 "printer-info",
159 IppValue::TextWithoutLanguage(cfg.make_and_model.as_str().try_into().unwrap()),
160 );
161 add(
162 attrs,
163 p,
164 "printer-make-and-model",
165 IppValue::TextWithoutLanguage(cfg.make_and_model.as_str().try_into().unwrap()),
166 );
167 add(attrs, p, "printer-more-info", uri(&more_info));
168 add(
169 attrs,
170 p,
171 "printer-uuid",
172 uri(&format!("urn:uuid:{}", record.uuid)),
173 );
174 add(
178 attrs,
179 p,
180 "printer-up-time",
181 IppValue::Integer(uptime_secs().max(1) as i32),
182 );
183
184 add(attrs, p, "printer-state", IppValue::Enum(record.state as i32));
185 let reason_kws: Vec<&str> = record.reasons.ipp_keywords();
186 add_array_keyword(attrs, p, "printer-state-reasons", &reason_kws);
187 add(attrs, p, "printer-is-accepting-jobs", IppValue::Boolean(true));
188 add(attrs, p, "queued-job-count", IppValue::Integer(0));
189
190 add_array_keyword(attrs, p, "ipp-versions-supported", &["1.1", "2.0", "2.1"]);
191 add_array_keyword(attrs, p, "ipp-features-supported", &["ipp-everywhere"]);
192 add(attrs, p, "pdl-override-supported", kw("attempted"));
193 if !cfg.device_id.is_empty() {
194 add(
195 attrs,
196 p,
197 "printer-device-id",
198 IppValue::TextWithoutLanguage(cfg.device_id.as_str().try_into().unwrap()),
199 );
200 }
201 add_array_enum(
204 attrs,
205 p,
206 "operations-supported",
207 &[
208 0x0002, 0x0004, 0x0005, 0x0006, 0x0008, 0x0009, 0x000a, 0x000b, 0x0039, 0x003b, 0x003c, ],
220 );
221 add(
222 attrs,
223 p,
224 "charset-configured",
225 charset("utf-8"),
226 );
227 add(
228 attrs,
229 p,
230 "charset-supported",
231 IppValue::Array(vec![charset("utf-8")]),
232 );
233 add(attrs, p, "natural-language-configured", lang("en"));
234 add(
235 attrs,
236 p,
237 "natural-language-supported",
238 IppValue::Array(vec![lang("en")]),
239 );
240 add(
241 attrs,
242 p,
243 "generated-natural-language-supported",
244 IppValue::Array(vec![lang("en")]),
245 );
246 add_array_keyword(attrs, p, "compression-supported", &["none"]);
247
248 let format_values: Vec<IppValue> = if cfg.document_formats.is_empty() {
253 vec![
254 mime("image/pwg-raster"),
255 mime("application/vnd.cups-raster"),
256 mime("application/octet-stream"),
257 ]
258 } else {
259 cfg.document_formats.iter().map(|f| mime(f)).collect()
260 };
261 add(
262 attrs,
263 p,
264 "document-format-supported",
265 IppValue::Array(format_values),
266 );
267 add(
268 attrs,
269 p,
270 "document-format-default",
271 mime("image/pwg-raster"),
272 );
273 add_array_keyword(
275 attrs,
276 p,
277 "pwg-raster-document-type-supported",
278 &["black_1"],
279 );
280 add(
284 attrs,
285 p,
286 "pwg-raster-document-resolution-supported",
287 IppValue::Array(vec![IppValue::Resolution {
288 cross_feed: cfg.dpi,
289 feed: cfg.dpi,
290 units: 3,
291 }]),
292 );
293 add_array_keyword(
294 attrs,
295 p,
296 "urf-supported",
297 &["W8", "SRGB24", "CP1", "RS203"],
298 );
299
300 add(attrs, p, "color-supported", IppValue::Boolean(false));
301 add_array_keyword(attrs, p, "print-color-mode-supported", &["monochrome"]);
302 add(attrs, p, "print-color-mode-default", kw("monochrome"));
303 add_array_keyword(attrs, p, "sides-supported", &["one-sided"]);
304 add(attrs, p, "sides-default", kw("one-sided"));
305 add(attrs, p, "orientation-requested-default", IppValue::Enum(3));
306 add_array_enum(attrs, p, "orientation-requested-supported", &[3, 4, 5, 6]);
308
309 add_array_keyword(attrs, p, "identify-actions-supported", &["display", "sound"]);
313 add_array_keyword(attrs, p, "identify-actions-default", &["display"]);
314
315 add_array_keyword(attrs, p, "media-source-supported", &["main"]);
319 add_array_keyword(attrs, p, "media-type-supported", &["labels", "stationery"]);
320 add_array_keyword(attrs, p, "output-bin-supported", &["face-up"]);
321 add(attrs, p, "output-bin-default", kw("face-up"));
322 add_array_keyword(
323 attrs,
324 p,
325 "print-content-optimize-supported",
326 &["auto", "graphic", "photo", "text", "text-and-graphic"],
327 );
328 add(attrs, p, "print-content-optimize-default", kw("auto"));
329 add_array_enum(attrs, p, "finishings-supported", &[3]);
331 add(attrs, p, "finishings-default", IppValue::Enum(3));
332 add(attrs, p, "job-creation-attributes-supported", IppValue::Array(vec![
333 kw("copies"),
334 kw("media"),
335 kw("media-col"),
336 kw("orientation-requested"),
337 kw("print-color-mode"),
338 kw("print-content-optimize"),
339 kw("print-quality"),
340 kw("printer-resolution"),
341 kw("sides"),
342 ]));
343
344 add(
345 attrs,
346 p,
347 "printer-resolution-default",
348 IppValue::Resolution {
349 cross_feed: cfg.dpi,
350 feed: cfg.dpi,
351 units: 3,
352 },
353 );
354 add(
355 attrs,
356 p,
357 "printer-resolution-supported",
358 IppValue::Array(vec![IppValue::Resolution {
359 cross_feed: cfg.dpi,
360 feed: cfg.dpi,
361 units: 3,
362 }]),
363 );
364
365 let media_kws: Vec<&str> = cfg.media_names.iter().map(|s| s.as_str()).collect();
366 if !media_kws.is_empty() {
367 add(attrs, p, "media-default", kw(media_kws[0]));
368 add_array_keyword(attrs, p, "media-supported", &media_kws);
369
370 let default_size = cfg.media_sizes.first().copied().unwrap_or([4000, 3000]);
372 add(
373 attrs,
374 p,
375 "media-col-default",
376 media_col(media_kws[0], default_size),
377 );
378 let media_cols: Vec<IppValue> = media_kws
379 .iter()
380 .zip(cfg.media_sizes.iter().copied().chain(std::iter::repeat(default_size)))
381 .map(|(name, size)| media_col(name, size))
382 .collect();
383 add_array_keyword(
388 attrs,
389 p,
390 "media-col-supported",
391 &[
392 "media-size",
393 "media-size-name",
394 "media-top-margin",
395 "media-bottom-margin",
396 "media-left-margin",
397 "media-right-margin",
398 "media-source",
399 "media-type",
400 ],
401 );
402 add(attrs, p, "media-col-database", IppValue::Array(media_cols.clone()));
403
404 let media_sizes: Vec<IppValue> = media_kws
407 .iter()
408 .zip(cfg.media_sizes.iter().copied().chain(std::iter::repeat(default_size)))
409 .map(|(_, size)| media_size_col(size))
410 .collect();
411 add(attrs, p, "media-size-supported", IppValue::Array(media_sizes));
412
413 let (ready_name, ready_size) = match &record.ready_media {
417 Some(rm) => (rm.name.as_str(), rm.size_hmm),
418 None => (media_kws[0], default_size),
419 };
420 add(attrs, p, "media-ready", kw(ready_name));
421 add(
422 attrs,
423 p,
424 "media-col-ready",
425 IppValue::Array(vec![media_col(ready_name, ready_size)]),
426 );
427 }
428
429 for margin in [
432 "media-top-margin-supported",
433 "media-bottom-margin-supported",
434 "media-left-margin-supported",
435 "media-right-margin-supported",
436 ] {
437 add(attrs, p, margin, IppValue::Integer(0));
438 }
439
440 add(
441 attrs,
442 p,
443 "copies-supported",
444 IppValue::RangeOfInteger { min: 1, max: 999 },
445 );
446 add(attrs, p, "copies-default", IppValue::Integer(1));
447 add(
448 attrs,
449 p,
450 "print-quality-supported",
451 IppValue::Array(vec![
452 IppValue::Enum(3),
453 IppValue::Enum(4),
454 IppValue::Enum(5),
455 ]),
456 );
457 add(attrs, p, "print-quality-default", IppValue::Enum(4));
458
459 add(attrs, p, "multiple-document-jobs-supported", IppValue::Boolean(false));
461 add(attrs, p, "multiple-operation-time-out", IppValue::Integer(60));
462 add(attrs, p, "multiple-operation-time-out-action", kw("process-job"));
463 add(attrs, p, "job-ids-supported", IppValue::Boolean(true));
464 add(attrs, p, "preferred-attributes-supported", IppValue::Boolean(false));
465 add_array_keyword(attrs, p, "overrides-supported", &["document-number", "pages"]);
466 add_array_keyword(attrs, p, "printer-get-attributes-supported", &["document-format"]);
467 add_array_keyword(
468 attrs,
469 p,
470 "which-jobs-supported",
471 &[
472 "completed",
473 "not-completed",
474 "aborted",
475 "canceled",
476 "pending",
477 "processing",
478 ],
479 );
480
481 add(attrs, p, "print-rendering-intent-default", kw("auto"));
483 add_array_keyword(attrs, p, "print-rendering-intent-supported", &["auto"]);
484 add(attrs, p, "pwg-raster-document-sheet-back", kw("normal"));
486
487 add(attrs, p, "printer-geo-location", IppValue::Other { tag: 0x12, data: Vec::<u8>::new().into() });
491 add(attrs, p, "printer-organization", text(""));
492 add(attrs, p, "printer-organizational-unit", text(""));
493 add(
494 attrs,
495 p,
496 "printer-icons",
497 IppValue::Array(vec![uri(&format!(
498 "http://{}:{}/icon.png",
499 advertise_host(host),
500 port
501 ))]),
502 );
503 add(attrs, p, "pages-per-minute", IppValue::Integer(20));
504
505 let supply_level = record.supply_percent.unwrap_or(100);
510 add(
511 attrs,
512 p,
513 "printer-supply",
514 IppValue::Array(vec![IppValue::OctetString(
515 format!(
516 "index=1;class=supplyThatIsConsumed;type=stoppingMaterial;\
517 unit=percent;maxcapacity=100;level={supply_level};colorantname=unknown;"
518 )
519 .try_into()
520 .expect("supply"),
521 )]),
522 );
523 add(
524 attrs,
525 p,
526 "printer-supply-description",
527 IppValue::Array(vec![text("Label Stock")]),
528 );
529 add(
530 attrs,
531 p,
532 "printer-supply-info-uri",
533 uri(&format!("http://{}:{}/", advertise_host(host), port)),
534 );
535
536 let now = now_unix();
538 add(attrs, p, "printer-config-change-time", IppValue::Integer(uptime_secs() as i32));
539 add(attrs, p, "printer-config-change-date-time", datetime_utc(now));
540 add(attrs, p, "printer-state-change-time", IppValue::Integer(uptime_secs() as i32));
541 add(attrs, p, "printer-state-change-date-time", datetime_utc(now));
542
543 filter_requested(&mut resp, requested);
544 Ok(resp)
545}
546
547fn filter_requested(resp: &mut IppRequestResponse, requested: Option<&BTreeSet<String>>) {
553 let Some(set) = requested else { return };
554 if set.is_empty() || set.contains("all") {
555 return;
556 }
557 for group in resp.attributes_mut().groups_mut() {
558 if group.tag() != DelimiterTag::PrinterAttributes {
559 continue;
560 }
561 group
562 .attributes_mut()
563 .retain(|name, _| set.contains(name.as_str()));
564 }
565}
566
567pub fn validate_job(
569 version: IppVersion,
570 request_id: u32,
571 record: &PrinterRecord,
572 host: &str,
573 port: u16,
574) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
575 get_printer_attributes(version, request_id, record, host, port, None)
576}
577
578pub fn print_job_accepted(
580 version: IppVersion,
581 request_id: u32,
582 job: &crate::job::JobRecord,
583 printer_uri_str: &str,
584) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
585 let mut resp =
586 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
587 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
588 let j = DelimiterTag::JobAttributes;
589 add(resp.attributes_mut(), j, "job-uri", uri(&job_uri_str));
590 add(
591 resp.attributes_mut(),
592 j,
593 "job-id",
594 IppValue::Integer(job.id as i32),
595 );
596 add(
597 resp.attributes_mut(),
598 j,
599 "job-state",
600 IppValue::Enum(job.state as i32),
601 );
602 add_array_keyword(
603 resp.attributes_mut(),
604 j,
605 "job-state-reasons",
606 &job_state_reason_keywords(job),
607 );
608 Ok(resp)
609}
610
611pub fn build_job_attrs_response(
614 version: IppVersion,
615 request_id: u32,
616 job: &crate::job::JobRecord,
617 printer_uri_str: &str,
618 requested: Option<&BTreeSet<String>>,
619) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
620 let mut resp =
621 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
622 for a in job_attrs_for_group(job, printer_uri_str, requested) {
623 resp.attributes_mut().add(DelimiterTag::JobAttributes, a);
624 }
625 Ok(resp)
626}
627
628pub fn build_get_jobs_response(
632 version: IppVersion,
633 request_id: u32,
634 jobs: &[crate::job::JobRecord],
635 printer_uri_str: &str,
636 requested: Option<&BTreeSet<String>>,
637) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
638 let mut resp =
639 IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
640 for job in jobs {
644 let mut group = ipp::attribute::IppAttributeGroup::new(DelimiterTag::JobAttributes);
645 for a in job_attrs_for_group(job, printer_uri_str, requested) {
646 group
647 .attributes_mut()
648 .insert(a.name().to_owned(), a);
649 }
650 resp.attributes_mut().groups_mut().push(group);
651 }
652 Ok(resp)
653}
654
655fn job_attrs_for_group(
656 job: &crate::job::JobRecord,
657 printer_uri_str: &str,
658 requested: Option<&BTreeSet<String>>,
659) -> Vec<IppAttribute> {
660 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
661 let mut out = vec![
662 attr("job-uri", uri(&job_uri_str)),
663 attr("job-id", IppValue::Integer(job.id as i32)),
664 attr("job-printer-uri", uri(printer_uri_str)),
665 attr(
666 "job-name",
667 IppValue::NameWithoutLanguage(
668 format!("job-{}", job.id).as_str().try_into().unwrap(),
669 ),
670 ),
671 attr("job-state", IppValue::Enum(job.state as i32)),
672 attr(
673 "job-originating-user-name",
674 IppValue::NameWithoutLanguage(job.owner.as_str().try_into().unwrap_or_else(|_| {
675 "anonymous".try_into().expect("anonymous")
676 })),
677 ),
678 attr("time-at-creation", IppValue::Integer(job.created_secs())),
679 ];
680 let reason_kws = job_state_reason_keywords(job);
681 out.push(attr(
682 "job-state-reasons",
683 IppValue::Array(reason_kws.iter().map(|s| kw(s)).collect()),
684 ));
685 if !job.message.is_empty() {
686 out.push(attr(
687 "job-state-message",
688 IppValue::TextWithoutLanguage(job.message.as_str().try_into().unwrap()),
689 ));
690 }
691 out.push(attr("time-at-processing", IppValue::Integer(job.created_secs())));
695 out.push(attr("job-printer-up-time", IppValue::Integer(uptime_secs() as i32)));
696 if let Some(s) = job.completed_secs() {
697 out.push(attr("time-at-completed", IppValue::Integer(s)));
698 }
699 if let Some(set) = requested {
700 out.retain(|a| set.contains(a.name().as_str()));
701 }
702 out
703}
704
705fn job_state_reason_keywords(job: &crate::job::JobRecord) -> Vec<&'static str> {
706 use crate::flags::PrinterReason;
707 use crate::job::JobState;
708 let mut out = Vec::new();
709 if job.reasons.contains(PrinterReason::MEDIA_EMPTY) {
710 out.push("job-completed-with-errors");
711 }
712 if job.reasons.contains(PrinterReason::MEDIA_JAM) {
713 out.push("aborted-by-system");
714 }
715 if job.reasons.contains(PrinterReason::OFFLINE) {
716 out.push("connection-error");
717 }
718 match job.state {
719 JobState::Canceled => out.push("job-canceled-by-user"),
720 JobState::Completed => out.push("job-completed-successfully"),
721 JobState::Aborted if out.is_empty() => out.push("aborted-by-system"),
722 _ => {}
723 }
724 if out.is_empty() {
725 out.push("none");
726 }
727 out
728}
729
730pub fn set_printer_processing(record: &mut PrinterRecord) {
732 record.state = IppPrinterState::Processing;
733}
734
735pub fn set_printer_idle(record: &mut PrinterRecord) {
737 record.state = IppPrinterState::Idle;
738}
739
740fn media_col(name: &str, size_hmm: [i32; 2]) -> IppValue {
743 use std::collections::BTreeMap;
744 let mut size = BTreeMap::new();
745 size.insert(
746 "x-dimension".try_into().unwrap(),
747 IppValue::Integer(size_hmm[0]),
748 );
749 size.insert(
750 "y-dimension".try_into().unwrap(),
751 IppValue::Integer(size_hmm[1]),
752 );
753 let mut col = BTreeMap::new();
754 col.insert(
755 "media-size".try_into().unwrap(),
756 IppValue::Collection(size),
757 );
758 col.insert(
759 "media-size-name".try_into().unwrap(),
760 kw(name),
761 );
762 IppValue::Collection(col)
763}
764
765fn media_size_col(size_hmm: [i32; 2]) -> IppValue {
768 use std::collections::BTreeMap;
769 let mut size = BTreeMap::new();
770 size.insert(
771 "x-dimension".try_into().unwrap(),
772 IppValue::Integer(size_hmm[0]),
773 );
774 size.insert(
775 "y-dimension".try_into().unwrap(),
776 IppValue::Integer(size_hmm[1]),
777 );
778 IppValue::Collection(size)
779}
780
781fn uptime_secs() -> u64 {
782 use std::sync::OnceLock;
783 use std::time::Instant;
784 static START: OnceLock<Instant> = OnceLock::new();
785 START.get_or_init(Instant::now).elapsed().as_secs()
786}