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 = IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
129 let attrs = resp.attributes_mut();
130 let cfg = &record.config;
131 let printer_uri_str = printer_uri(record, host, port);
132 let more_info = format!("http://{}:{}/", advertise_host(host), port);
133
134 let p = DelimiterTag::PrinterAttributes;
135 add(attrs, p, "printer-uri-supported", uri(&printer_uri_str));
136 add(attrs, p, "uri-authentication-supported", kw("none"));
137 add(attrs, p, "uri-security-supported", kw("none"));
138 add(
139 attrs,
140 p,
141 "printer-name",
142 IppValue::NameWithoutLanguage(cfg.name.as_str().try_into().unwrap()),
143 );
144 add(
145 attrs,
146 p,
147 "printer-location",
148 IppValue::TextWithoutLanguage("".try_into().unwrap()),
149 );
150 add(
151 attrs,
152 p,
153 "printer-info",
154 IppValue::TextWithoutLanguage(cfg.display_label().try_into().unwrap()),
155 );
156 add(
157 attrs,
158 p,
159 "printer-make-and-model",
160 IppValue::TextWithoutLanguage(cfg.make_and_model.as_str().try_into().unwrap()),
161 );
162 add(attrs, p, "printer-more-info", uri(&more_info));
163 add(
164 attrs,
165 p,
166 "printer-uuid",
167 uri(&format!("urn:uuid:{}", record.uuid)),
168 );
169 add(
173 attrs,
174 p,
175 "printer-up-time",
176 IppValue::Integer(uptime_secs().max(1) as i32),
177 );
178
179 add(
180 attrs,
181 p,
182 "printer-state",
183 IppValue::Enum(record.state as i32),
184 );
185 let reason_kws: Vec<&str> = record.reasons.ipp_keywords();
186 add_array_keyword(attrs, p, "printer-state-reasons", &reason_kws);
187 add(
188 attrs,
189 p,
190 "printer-is-accepting-jobs",
191 IppValue::Boolean(true),
192 );
193 add(attrs, p, "queued-job-count", IppValue::Integer(0));
194
195 add_array_keyword(attrs, p, "ipp-versions-supported", &["1.1", "2.0", "2.1"]);
196 add_array_keyword(attrs, p, "ipp-features-supported", &["ipp-everywhere"]);
197 add(attrs, p, "pdl-override-supported", kw("attempted"));
198 if !cfg.device_id.is_empty() {
199 add(
200 attrs,
201 p,
202 "printer-device-id",
203 IppValue::TextWithoutLanguage(cfg.device_id.as_str().try_into().unwrap()),
204 );
205 }
206 add_array_enum(
209 attrs,
210 p,
211 "operations-supported",
212 &[
213 0x0002, 0x0004, 0x0005, 0x0006, 0x0008, 0x0009, 0x000a, 0x000b, 0x0039, 0x003b, 0x003c, ],
225 );
226 add(attrs, p, "charset-configured", charset("utf-8"));
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(attrs, p, "pwg-raster-document-type-supported", &["black_1"]);
275 add(
279 attrs,
280 p,
281 "pwg-raster-document-resolution-supported",
282 IppValue::Array(vec![IppValue::Resolution {
283 cross_feed: cfg.dpi,
284 feed: cfg.dpi,
285 units: 3,
286 }]),
287 );
288 add_array_keyword(attrs, p, "urf-supported", &["W8", "SRGB24", "CP1", "RS203"]);
289
290 add(attrs, p, "color-supported", IppValue::Boolean(false));
291 add_array_keyword(attrs, p, "print-color-mode-supported", &["monochrome"]);
292 add(attrs, p, "print-color-mode-default", kw("monochrome"));
293 add_array_keyword(attrs, p, "sides-supported", &["one-sided"]);
294 add(attrs, p, "sides-default", kw("one-sided"));
295 add(attrs, p, "orientation-requested-default", IppValue::Enum(3));
296 add_array_enum(attrs, p, "orientation-requested-supported", &[3, 4, 5, 6]);
298
299 add_array_keyword(
303 attrs,
304 p,
305 "identify-actions-supported",
306 &["display", "sound"],
307 );
308 add_array_keyword(attrs, p, "identify-actions-default", &["display"]);
309
310 add_array_keyword(attrs, p, "media-source-supported", &["main"]);
314 add_array_keyword(attrs, p, "media-type-supported", &["labels", "stationery"]);
315 add_array_keyword(attrs, p, "output-bin-supported", &["face-up"]);
316 add(attrs, p, "output-bin-default", kw("face-up"));
317 add_array_keyword(
318 attrs,
319 p,
320 "print-content-optimize-supported",
321 &["auto", "graphic", "photo", "text", "text-and-graphic"],
322 );
323 add(attrs, p, "print-content-optimize-default", kw("auto"));
324 add_array_enum(attrs, p, "finishings-supported", &[3]);
326 add(attrs, p, "finishings-default", IppValue::Enum(3));
327 add(
328 attrs,
329 p,
330 "job-creation-attributes-supported",
331 IppValue::Array(vec![
332 kw("copies"),
333 kw("media"),
334 kw("media-col"),
335 kw("orientation-requested"),
336 kw("print-color-mode"),
337 kw("print-content-optimize"),
338 kw("print-quality"),
339 kw("printer-resolution"),
340 kw("sides"),
341 ]),
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 custom_range = custom_media_range(cfg);
369 let custom_kws: Vec<String> = custom_range
370 .iter()
371 .flat_map(|(min, max)| {
372 [
373 custom_media_name("min", *min),
374 custom_media_name("max", *max),
375 ]
376 })
377 .collect();
378
379 let media_kws: Vec<&str> = cfg
380 .media_names
381 .iter()
382 .map(|s| s.as_str())
383 .chain(custom_kws.iter().map(|s| s.as_str()))
384 .collect();
385
386 if !media_kws.is_empty() {
387 add(attrs, p, "media-default", kw(media_kws[0]));
388 add_array_keyword(attrs, p, "media-supported", &media_kws);
389
390 let default_size = cfg.media_sizes.first().copied().unwrap_or([4000, 3000]);
392 add(
393 attrs,
394 p,
395 "media-col-default",
396 media_col(
397 media_kws[0],
398 default_size,
399 side_margin_hmm(cfg, default_size[0]),
400 ),
401 );
402 let mut media_cols: Vec<IppValue> = cfg
407 .media_names
408 .iter()
409 .zip(
410 cfg.media_sizes
411 .iter()
412 .copied()
413 .chain(std::iter::repeat(default_size)),
414 )
415 .map(|(name, size)| media_col(name, size, side_margin_hmm(cfg, size[0])))
416 .collect();
417 if let Some((min, max)) = custom_range {
418 media_cols.push(media_col(
419 &custom_media_name("min", min),
420 min,
421 side_margin_hmm(cfg, min[0]),
422 ));
423 media_cols.push(media_col(
424 &custom_media_name("max", max),
425 max,
426 side_margin_hmm(cfg, max[0]),
427 ));
428 }
429 add_array_keyword(
434 attrs,
435 p,
436 "media-col-supported",
437 &[
438 "media-size",
439 "media-size-name",
440 "media-top-margin",
441 "media-bottom-margin",
442 "media-left-margin",
443 "media-right-margin",
444 "media-source",
445 "media-type",
446 ],
447 );
448 add(
449 attrs,
450 p,
451 "media-col-database",
452 IppValue::Array(media_cols.clone()),
453 );
454
455 let mut media_sizes: Vec<IppValue> = cfg
458 .media_sizes
459 .iter()
460 .copied()
461 .map(media_size_col)
462 .collect();
463 if let Some((min, max)) = custom_range {
468 media_sizes.push(media_size_range_col(min, max));
469 }
470 add(
471 attrs,
472 p,
473 "media-size-supported",
474 IppValue::Array(media_sizes),
475 );
476
477 let (ready_name, ready_size) = match &record.ready_media {
481 Some(rm) => (rm.name.as_str(), rm.size_hmm),
482 None => (media_kws[0], default_size),
483 };
484 add(attrs, p, "media-ready", kw(ready_name));
485 add(
486 attrs,
487 p,
488 "media-col-ready",
489 IppValue::Array(vec![media_col(
490 ready_name,
491 ready_size,
492 side_margin_hmm(cfg, ready_size[0]),
493 )]),
494 );
495 }
496
497 for margin in [
503 "media-top-margin-supported",
504 "media-bottom-margin-supported",
505 ] {
506 add(attrs, p, margin, IppValue::Integer(0));
507 }
508 let mut side_margins: Vec<i32> = cfg
509 .media_sizes
510 .iter()
511 .map(|s| side_margin_hmm(cfg, s[0]))
512 .collect();
513 if let Some((min, max)) = custom_media_range(cfg) {
514 side_margins.push(side_margin_hmm(cfg, min[0]));
515 side_margins.push(side_margin_hmm(cfg, max[0]));
516 }
517 if side_margins.is_empty() {
522 side_margins.push(0);
523 }
524 side_margins.sort_unstable();
525 side_margins.dedup();
526 let side_margins: Vec<IppValue> = side_margins.into_iter().map(IppValue::Integer).collect();
527 for margin in [
528 "media-left-margin-supported",
529 "media-right-margin-supported",
530 ] {
531 add(attrs, p, margin, IppValue::Array(side_margins.clone()));
532 }
533
534 add(
535 attrs,
536 p,
537 "copies-supported",
538 IppValue::RangeOfInteger { min: 1, max: 999 },
539 );
540 add(attrs, p, "copies-default", IppValue::Integer(1));
541 add(
542 attrs,
543 p,
544 "print-quality-supported",
545 IppValue::Array(vec![
546 IppValue::Enum(3),
547 IppValue::Enum(4),
548 IppValue::Enum(5),
549 ]),
550 );
551 add(attrs, p, "print-quality-default", IppValue::Enum(4));
552
553 add(
555 attrs,
556 p,
557 "multiple-document-jobs-supported",
558 IppValue::Boolean(false),
559 );
560 add(
561 attrs,
562 p,
563 "multiple-operation-time-out",
564 IppValue::Integer(60),
565 );
566 add(
567 attrs,
568 p,
569 "multiple-operation-time-out-action",
570 kw("process-job"),
571 );
572 add(attrs, p, "job-ids-supported", IppValue::Boolean(true));
573 add(
574 attrs,
575 p,
576 "preferred-attributes-supported",
577 IppValue::Boolean(false),
578 );
579 add_array_keyword(
580 attrs,
581 p,
582 "overrides-supported",
583 &["document-number", "pages"],
584 );
585 add_array_keyword(
586 attrs,
587 p,
588 "printer-get-attributes-supported",
589 &["document-format"],
590 );
591 add_array_keyword(
592 attrs,
593 p,
594 "which-jobs-supported",
595 &[
596 "completed",
597 "not-completed",
598 "aborted",
599 "canceled",
600 "pending",
601 "processing",
602 ],
603 );
604
605 add(attrs, p, "print-rendering-intent-default", kw("auto"));
607 add_array_keyword(attrs, p, "print-rendering-intent-supported", &["auto"]);
608 add(attrs, p, "pwg-raster-document-sheet-back", kw("normal"));
610
611 add(
615 attrs,
616 p,
617 "printer-geo-location",
618 IppValue::Other {
619 tag: 0x12,
620 data: Vec::<u8>::new().into(),
621 },
622 );
623 add(attrs, p, "printer-organization", text(""));
624 add(attrs, p, "printer-organizational-unit", text(""));
625 add(
626 attrs,
627 p,
628 "printer-icons",
629 IppValue::Array(vec![uri(&format!(
630 "http://{}:{}/icon.png",
631 advertise_host(host),
632 port
633 ))]),
634 );
635 add(attrs, p, "pages-per-minute", IppValue::Integer(20));
636
637 let supply_level = record.supply_percent.unwrap_or(100);
642 add(
643 attrs,
644 p,
645 "printer-supply",
646 IppValue::Array(vec![IppValue::OctetString(
647 format!(
648 "index=1;class=supplyThatIsConsumed;type=stoppingMaterial;\
649 unit=percent;maxcapacity=100;level={supply_level};colorantname=unknown;"
650 )
651 .try_into()
652 .expect("supply"),
653 )]),
654 );
655 add(
656 attrs,
657 p,
658 "printer-supply-description",
659 IppValue::Array(vec![text("Label Stock")]),
660 );
661 add(
662 attrs,
663 p,
664 "printer-supply-info-uri",
665 uri(&format!("http://{}:{}/", advertise_host(host), port)),
666 );
667
668 let now = now_unix();
670 add(
671 attrs,
672 p,
673 "printer-config-change-time",
674 IppValue::Integer(uptime_secs() as i32),
675 );
676 add(
677 attrs,
678 p,
679 "printer-config-change-date-time",
680 datetime_utc(now),
681 );
682 add(
683 attrs,
684 p,
685 "printer-state-change-time",
686 IppValue::Integer(uptime_secs() as i32),
687 );
688 add(
689 attrs,
690 p,
691 "printer-state-change-date-time",
692 datetime_utc(now),
693 );
694
695 filter_requested(&mut resp, requested);
696 Ok(resp)
697}
698
699fn filter_requested(resp: &mut IppRequestResponse, requested: Option<&BTreeSet<String>>) {
705 let Some(set) = requested else { return };
706 if set.is_empty() || set.contains("all") {
707 return;
708 }
709 for group in resp.attributes_mut().groups_mut() {
710 if group.tag() != DelimiterTag::PrinterAttributes {
711 continue;
712 }
713 group
714 .attributes_mut()
715 .retain(|name, _| set.contains(name.as_str()));
716 }
717}
718
719pub fn validate_job(
721 version: IppVersion,
722 request_id: u32,
723 record: &PrinterRecord,
724 host: &str,
725 port: u16,
726) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
727 get_printer_attributes(version, request_id, record, host, port, None)
728}
729
730pub fn print_job_accepted(
732 version: IppVersion,
733 request_id: u32,
734 job: &crate::job::JobRecord,
735 printer_uri_str: &str,
736) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
737 let mut resp = IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
738 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
739 let j = DelimiterTag::JobAttributes;
740 add(resp.attributes_mut(), j, "job-uri", uri(&job_uri_str));
741 add(
742 resp.attributes_mut(),
743 j,
744 "job-id",
745 IppValue::Integer(job.id as i32),
746 );
747 add(
748 resp.attributes_mut(),
749 j,
750 "job-state",
751 IppValue::Enum(job.state as i32),
752 );
753 add_array_keyword(
754 resp.attributes_mut(),
755 j,
756 "job-state-reasons",
757 &job_state_reason_keywords(job),
758 );
759 Ok(resp)
760}
761
762pub fn build_job_attrs_response(
765 version: IppVersion,
766 request_id: u32,
767 job: &crate::job::JobRecord,
768 printer_uri_str: &str,
769 requested: Option<&BTreeSet<String>>,
770) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
771 let mut resp = IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
772 for a in job_attrs_for_group(job, printer_uri_str, requested) {
773 resp.attributes_mut().add(DelimiterTag::JobAttributes, a);
774 }
775 Ok(resp)
776}
777
778pub fn build_get_jobs_response(
782 version: IppVersion,
783 request_id: u32,
784 jobs: &[crate::job::JobRecord],
785 printer_uri_str: &str,
786 requested: Option<&BTreeSet<String>>,
787) -> Result<IppRequestResponse, ipp::parser::IppParseError> {
788 let mut resp = IppRequestResponse::new_response(version, StatusCode::SuccessfulOk, request_id)?;
789 for job in jobs {
793 let mut group = ipp::attribute::IppAttributeGroup::new(DelimiterTag::JobAttributes);
794 for a in job_attrs_for_group(job, printer_uri_str, requested) {
795 group.attributes_mut().insert(a.name().to_owned(), a);
796 }
797 resp.attributes_mut().groups_mut().push(group);
798 }
799 Ok(resp)
800}
801
802fn job_attrs_for_group(
803 job: &crate::job::JobRecord,
804 printer_uri_str: &str,
805 requested: Option<&BTreeSet<String>>,
806) -> Vec<IppAttribute> {
807 let job_uri_str = format!("{printer_uri_str}/job/{}", job.id);
808 let mut out = vec![
809 attr("job-uri", uri(&job_uri_str)),
810 attr("job-id", IppValue::Integer(job.id as i32)),
811 attr("job-printer-uri", uri(printer_uri_str)),
812 attr(
813 "job-name",
814 IppValue::NameWithoutLanguage(format!("job-{}", job.id).as_str().try_into().unwrap()),
815 ),
816 attr("job-state", IppValue::Enum(job.state as i32)),
817 attr(
818 "job-originating-user-name",
819 IppValue::NameWithoutLanguage(
820 job.owner
821 .as_str()
822 .try_into()
823 .unwrap_or_else(|_| "anonymous".try_into().expect("anonymous")),
824 ),
825 ),
826 attr("time-at-creation", IppValue::Integer(job.created_secs())),
827 ];
828 let reason_kws = job_state_reason_keywords(job);
829 out.push(attr(
830 "job-state-reasons",
831 IppValue::Array(reason_kws.iter().map(|s| kw(s)).collect()),
832 ));
833 if !job.message.is_empty() {
834 out.push(attr(
835 "job-state-message",
836 IppValue::TextWithoutLanguage(job.message.as_str().try_into().unwrap()),
837 ));
838 }
839 out.push(attr(
843 "time-at-processing",
844 IppValue::Integer(job.created_secs()),
845 ));
846 out.push(attr(
847 "job-printer-up-time",
848 IppValue::Integer(uptime_secs() as i32),
849 ));
850 if let Some(s) = job.completed_secs() {
851 out.push(attr("time-at-completed", IppValue::Integer(s)));
852 }
853 if let Some(set) = requested {
854 out.retain(|a| set.contains(a.name().as_str()));
855 }
856 out
857}
858
859fn job_state_reason_keywords(job: &crate::job::JobRecord) -> Vec<&'static str> {
860 use crate::flags::PrinterReason;
861 use crate::job::JobState;
862 let mut out = Vec::new();
863 if job.reasons.contains(PrinterReason::MEDIA_EMPTY) {
864 out.push("job-completed-with-errors");
865 }
866 if job.reasons.contains(PrinterReason::MEDIA_JAM) {
867 out.push("aborted-by-system");
868 }
869 if job.reasons.contains(PrinterReason::OFFLINE) {
870 out.push("connection-error");
871 }
872 match job.state {
873 JobState::Canceled => out.push("job-canceled-by-user"),
874 JobState::Completed => out.push("job-completed-successfully"),
875 JobState::Aborted if out.is_empty() => out.push("aborted-by-system"),
876 _ => {}
877 }
878 if out.is_empty() {
879 out.push("none");
880 }
881 out
882}
883
884pub fn set_printer_processing(record: &mut PrinterRecord) {
886 record.state = IppPrinterState::Processing;
887}
888
889pub fn set_printer_idle(record: &mut PrinterRecord) {
891 record.state = IppPrinterState::Idle;
892}
893
894fn media_col(name: &str, size_hmm: [i32; 2], side_margin_hmm: i32) -> IppValue {
897 use std::collections::BTreeMap;
898 let mut size = BTreeMap::new();
899 size.insert(
900 "x-dimension".try_into().unwrap(),
901 IppValue::Integer(size_hmm[0]),
902 );
903 size.insert(
904 "y-dimension".try_into().unwrap(),
905 IppValue::Integer(size_hmm[1]),
906 );
907 let mut col = BTreeMap::new();
908 col.insert("media-size".try_into().unwrap(), IppValue::Collection(size));
909 col.insert("media-size-name".try_into().unwrap(), kw(name));
910 col.insert("media-top-margin".try_into().unwrap(), IppValue::Integer(0));
913 col.insert(
914 "media-bottom-margin".try_into().unwrap(),
915 IppValue::Integer(0),
916 );
917 col.insert(
918 "media-left-margin".try_into().unwrap(),
919 IppValue::Integer(side_margin_hmm),
920 );
921 col.insert(
922 "media-right-margin".try_into().unwrap(),
923 IppValue::Integer(side_margin_hmm),
924 );
925 IppValue::Collection(col)
926}
927
928pub(crate) fn side_margin_hmm(cfg: &crate::printer::PrinterConfig, width_hmm: i32) -> i32 {
939 if cfg.dpi <= 0 || cfg.printhead_width_dots == 0 {
940 return 0;
941 }
942 let head_hmm = (cfg.printhead_width_dots as i64 * 2540 / cfg.dpi as i64) as i32;
944 ((width_hmm - head_hmm).max(0)) / 2
945}
946
947fn custom_media_range(cfg: &crate::printer::PrinterConfig) -> Option<([i32; 2], [i32; 2])> {
952 let (min, max) = (cfg.media_size_min, cfg.media_size_max);
953 let sane = min.iter().all(|&v| v > 0)
954 && max.iter().all(|&v| v > 0)
955 && max[0] >= min[0]
956 && max[1] >= min[1];
957 sane.then_some((min, max))
958}
959
960fn custom_media_name(bound: &str, size_hmm: [i32; 2]) -> String {
964 let (w, h) = (size_hmm[0] / 100, size_hmm[1] / 100);
965 format!("custom_{bound}_{w}x{h}mm_{w}x{h}mm")
966}
967
968fn media_size_range_col(min_hmm: [i32; 2], max_hmm: [i32; 2]) -> IppValue {
971 use std::collections::BTreeMap;
972 let mut size = BTreeMap::new();
973 size.insert(
974 "x-dimension".try_into().unwrap(),
975 IppValue::RangeOfInteger {
976 min: min_hmm[0],
977 max: max_hmm[0],
978 },
979 );
980 size.insert(
981 "y-dimension".try_into().unwrap(),
982 IppValue::RangeOfInteger {
983 min: min_hmm[1],
984 max: max_hmm[1],
985 },
986 );
987 IppValue::Collection(size)
988}
989
990fn media_size_col(size_hmm: [i32; 2]) -> IppValue {
993 use std::collections::BTreeMap;
994 let mut size = BTreeMap::new();
995 size.insert(
996 "x-dimension".try_into().unwrap(),
997 IppValue::Integer(size_hmm[0]),
998 );
999 size.insert(
1000 "y-dimension".try_into().unwrap(),
1001 IppValue::Integer(size_hmm[1]),
1002 );
1003 IppValue::Collection(size)
1004}
1005
1006fn uptime_secs() -> u64 {
1007 use std::sync::OnceLock;
1008 use std::time::Instant;
1009 static START: OnceLock<Instant> = OnceLock::new();
1010 START.get_or_init(Instant::now).elapsed().as_secs()
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015
1016 #[test]
1021 fn side_margins_come_from_the_head_width() {
1022 let mut cfg = config_with_range([1000, 1000], [5000, 12000]);
1023 cfg.dpi = 203;
1024 cfg.printhead_width_dots = 384; assert_eq!(side_margin_hmm(&cfg, 5000), 98);
1027 assert_eq!(side_margin_hmm(&cfg, 4000), 0);
1029 assert_eq!(side_margin_hmm(&cfg, 4804), 0);
1030 }
1031
1032 #[test]
1034 fn side_margins_are_zero_without_geometry() {
1035 let mut cfg = config_with_range([1000, 1000], [5000, 12000]);
1036 cfg.dpi = 0;
1037 assert_eq!(side_margin_hmm(&cfg, 5000), 0);
1038 cfg.dpi = 203;
1039 cfg.printhead_width_dots = 0;
1040 assert_eq!(side_margin_hmm(&cfg, 5000), 0);
1041 }
1042 use super::*;
1043 use crate::printer::{PrinterConfig, PrinterRecord};
1044
1045 fn config_with_range(min: [i32; 2], max: [i32; 2]) -> PrinterConfig {
1046 PrinterConfig {
1047 name: "p".into(),
1048 display_name: String::new(),
1049 driver_name: "t".into(),
1050 make_and_model: "Test".into(),
1051 device_id: String::new(),
1052 device_uri: "mock://x".into(),
1053 dpi: 203,
1054 printhead_width_dots: 384,
1055 media_names: vec!["om_40x30mm_40x30mm".into()],
1056 media_sizes: vec![[4000, 3000]],
1057 media_size_min: min,
1058 media_size_max: max,
1059 darkness: 50,
1060 document_formats: vec![],
1061 }
1062 }
1063
1064 fn attrs_text(cfg: PrinterConfig) -> String {
1065 let record = PrinterRecord::new(cfg);
1066 let resp = get_printer_attributes(IppVersion::v2_0(), 1, &record, "localhost", 8631, None)
1067 .unwrap();
1068 format!("{:?}", resp.attributes())
1069 }
1070
1071 #[test]
1074 fn custom_range_is_advertised_when_bounds_set() {
1075 let text = attrs_text(config_with_range([1000, 1000], [5000, 12000]));
1076 assert!(text.contains("custom_min_10x10mm"), "min bookend missing");
1077 assert!(text.contains("custom_max_50x120mm"), "max bookend missing");
1078 assert!(
1079 text.contains("RangeOfInteger"),
1080 "media-size-supported has no range entry"
1081 );
1082 }
1083
1084 #[test]
1087 fn no_custom_range_when_bounds_unset() {
1088 let text = attrs_text(config_with_range([0, 0], [0, 0]));
1089 assert!(!text.contains("custom_min"));
1090 assert!(!text.contains("custom_max"));
1091 }
1092
1093 #[test]
1096 fn malformed_bounds_are_ignored() {
1097 for (min, max) in [
1098 ([1000, 1000], [0, 0]),
1099 ([0, 0], [5000, 5000]),
1100 ([5000, 5000], [1000, 1000]),
1101 ([1000, 6000], [5000, 5000]),
1102 ] {
1103 assert!(
1104 custom_media_range(&config_with_range(min, max)).is_none(),
1105 "{min:?}..{max:?} should be rejected"
1106 );
1107 }
1108 }
1109
1110 fn printer_attr(cfg: PrinterConfig, name: &str) -> IppValue {
1112 let record = PrinterRecord::new(cfg);
1113 let resp = get_printer_attributes(IppVersion::v2_0(), 1, &record, "localhost", 8631, None)
1114 .unwrap();
1115 let value = resp
1116 .attributes()
1117 .groups_of(DelimiterTag::PrinterAttributes)
1118 .flat_map(|g| g.attributes().values())
1119 .find(|a| a.name().as_ref() == name)
1120 .unwrap_or_else(|| panic!("{name} missing"))
1121 .value()
1122 .clone();
1123 value
1124 }
1125
1126 fn x_dimension_of(db: &IppValue, size_name: &str) -> Option<i32> {
1128 db.into_iter().find_map(|entry| {
1129 let IppValue::Collection(col) = entry else {
1130 return None;
1131 };
1132 let named = col
1133 .iter()
1134 .any(|(k, v)| k.as_ref() == "media-size-name" && v.to_string() == size_name);
1135 if !named {
1136 return None;
1137 }
1138 let IppValue::Collection(size) =
1139 col.iter().find(|(k, _)| k.as_ref() == "media-size")?.1
1140 else {
1141 return None;
1142 };
1143 match size.iter().find(|(k, _)| k.as_ref() == "x-dimension")?.1 {
1144 IppValue::Integer(v) => Some(*v),
1145 _ => None,
1146 }
1147 })
1148 }
1149
1150 #[test]
1155 fn custom_bookends_carry_their_own_size() {
1156 let db = printer_attr(
1157 config_with_range([1000, 1000], [5000, 12000]),
1158 "media-col-database",
1159 );
1160 assert_eq!(
1161 x_dimension_of(&db, "custom_min_10x10mm_10x10mm"),
1162 Some(1000),
1163 "min bookend not paired with its own size"
1164 );
1165 assert_eq!(
1166 x_dimension_of(&db, "custom_max_50x120mm_50x120mm"),
1167 Some(5000),
1168 "max bookend not paired with its own size"
1169 );
1170 assert_eq!(x_dimension_of(&db, "om_40x30mm_40x30mm"), Some(4000));
1172 }
1173
1174 #[test]
1175 fn custom_name_uses_pwg_metric_form() {
1176 assert_eq!(
1177 custom_media_name("min", [1000, 1500]),
1178 "custom_min_10x15mm_10x15mm"
1179 );
1180 }
1181}