44
55namespace DR \Ipp \Protocol ;
66
7- use DateTime ;
87use DR \Ipp \Entity \Response \CupsIppResponse ;
98use DR \Ipp \Entity \Response \IppResponseInterface ;
109use DR \Ipp \Enum \IppOperationTagEnum ;
1110use DR \Ipp \Enum \IppStatusCodeEnum ;
1211use DR \Ipp \Enum \IppTypeEnum ;
13- use RuntimeException ;
1412
1513/**
1614 * @internal
1715 */
1816class IppResponseParser implements IppResponseParserInterface
1917{
18+ private IppAttribute $ lastAttribute ;
19+
20+ /**
21+ * @see https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1
22+ */
2023 public function getResponse (string $ response ): IppResponseInterface
2124 {
22- [, $ response ] = $ this ->consume ($ response , 2 , IppTypeEnum::Int); // version 0x0101
25+ $ state = new IppResponseState ($ response );
26+
27+ $ state ->consume (2 , IppTypeEnum::Int); // version 0x0101
2328 /** @var int $status */
24- [ $ status, $ response ] = $ this ->consume ($ response , 2 , IppTypeEnum::Int); // status 0x0502
25- [, $ response ] = $ this ->consume ($ response , 4 , IppTypeEnum::Int); // requestId 0x00000001
26- [, $ response ] = $ this ->consume ($ response , 1 , IppTypeEnum::Int); // IPPOperationTag::OPERATION_ATTRIBUTE_START
29+ $ status = $ state ->consume (2 , IppTypeEnum::Int); // status 0x0502
30+ $ state ->consume (4 , IppTypeEnum::Int); // requestId 0x00000001
31+ $ state ->consume (1 , IppTypeEnum::Int); // IPPOperationTag::OPERATION_ATTRIBUTE_START
2732
2833 $ attributesTags = [
2934 IppOperationTagEnum::JobAttributeStart->value ,
3035 IppOperationTagEnum::PrinterAttributeStart->value ,
3136 IppOperationTagEnum::UnsupportedAttributes->value ,
3237 ];
3338 $ attributes = [];
34- while ($ this -> unpack ( ' c ' , $ response ) !== IppOperationTagEnum::AttributeEnd->value ) {
35- // look for attribute tag and remove it before parsing further attributes
36- if (in_array ($ this -> unpack ( ' c ' , $ response ), $ attributesTags , true )) {
37- [, $ response ] = $ this ->consume ($ response , 1 , null );
39+ while ($ state -> getNextByte ( ) !== IppOperationTagEnum::AttributeEnd->value ) {
40+ // look for an attribute tag and remove it before parsing further attributes
41+ if (in_array ($ state -> getNextByte ( ), $ attributesTags , true )) {
42+ $ state ->consume (1 , null );
3843 }
3944
40- [ $ attribute, $ response ] = $ this ->consumeAttribute ( $ response );
45+ $ attribute = $ this ->getAttribute ( $ state );
4146 $ attributes [$ attribute ->getName ()] = $ attribute ;
4247 }
4348 $ statusCode = IppStatusCodeEnum::tryFrom ($ status ) ?? IppStatusCodeEnum::Unknown;
@@ -46,78 +51,66 @@ public function getResponse(string $response): IppResponseInterface
4651 }
4752
4853 /**
49- * Decodes an attribute from the response, and returns the decoded values and the rest of the response
50- * @return array{IppAttribute, string}
54+ * @see https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1.6
5155 */
52- private function consumeAttribute ( string $ response ): array
56+ private function getCollection ( IppResponseState $ state ): IppCollection
5357 {
54- /** @var int $type */
55- [$ type , $ response ] = $ this ->consume ($ response , 1 , null );
56- [$ nameLength , $ response ] = $ this ->consume ($ response , 2 , IppTypeEnum::Int);
57- /** @var int $nameLength */
58- [$ attrName , $ response ] = $ this ->consume ($ response , $ nameLength , IppTypeEnum::NameWithoutLang);
59- /** @var string $attrName */
60- [$ valueLength , $ response ] = $ this ->consume ($ response , 2 , IppTypeEnum::Int);
61- /** @var int $valueLength */
62- [$ attrValue , $ response ] = $ this ->consume (
63- $ response ,
64- $ valueLength ,
65- IppTypeEnum::tryFrom ($ type ),
66- );
58+ $ collection = new IppCollection ();
59+
60+ $ state ->consume (2 , null ); // 0x0000
61+ while ($ state ->getNextByte () !== IppTypeEnum::EndCollection->value ) {
62+ $ state ->consume (3 , null ); // 0x4a 0x00 0x00
63+
64+ /** @var string $name */
65+ $ name = $ this ->getAttributeValue (IppTypeEnum::MemberAttributeName, $ state );
66+ /** @var int $valueType */
67+ $ valueType = $ state ->consume (1 , null );
68+ $ state ->consume (2 , null ); // 0x00 0x00
69+
70+ /** @var int $valueLength */
71+ $ valueLength = $ state ->consume (2 , IppTypeEnum::Int);
72+ $ value = $ state ->consume ($ valueLength , IppTypeEnum::tryFrom ($ valueType ));
73+ $ collection ->add ($ name , $ value );
74+ }
75+ $ state ->consume (5 , null ); // 0x37 0x00 0x00 0x00 0x00
6776
68- return [ new IppAttribute (IppTypeEnum:: tryFrom ( $ type ) ?? IppTypeEnum::Int, $ attrName , $ attrValue ), $ response ] ;
77+ return $ collection ;
6978 }
7079
7180 /**
72- * Decodes part of a binary string, and returns the decoded value and the rest of the binary string
73- * @return array{int|string|DateTime, string}
81+ * Decodes an attribute from the response, and returns the decoded value(s)
7482 */
75- private function consume ( string $ response , int $ length , ? IppTypeEnum $ type ): array
83+ private function getAttribute ( IppResponseState $ state ): IppAttribute
7684 {
77- switch ($ type ) {
78- case IppTypeEnum::Int:
79- case IppTypeEnum::Enum:
80- $ unpack = $ length === 2 ? 'n ' : 'N ' ;
81- break ;
82- case IppTypeEnum::DateTime:
83- return [$ this ->unpackDateTime ($ response ), substr ($ response , $ length )];
84- case null :
85- $ unpack = 'c ' . $ length ;
86- break ;
87- default :
88- $ unpack = 'a ' . $ length ;
89- }
90-
91- return [$ this ->unpack ($ unpack , $ response ), substr ($ response , $ length )];
92- }
85+ /** @var int $type */
86+ $ type = $ state ->consume (1 , null );
87+ $ attrType = IppTypeEnum::tryFrom ($ type );
9388
94- private function unpackDateTime (string $ response ): DateTime
95- {
96- // Datetime in rfc2579 format: https://datatracker.ietf.org/doc/html/rfc2579
97- /** @var array{year: int, month: int, day: int, hour: int, min: int, sec: int, int, tz: string, tzhour:int, tzmin: int}|false $dateTime */
98- $ dateTime = @unpack ('nyear/cmonth/cday/chour/cmin/csec/c/atz/ctzhour/ctzmin ' , $ response );
99- if ($ dateTime === false ) {
100- throw new RuntimeException ('Failed to unpack IPP datetime ' );
89+ /** @var int $nameLength */
90+ $ nameLength = $ state ->consume (2 , IppTypeEnum::Int);
91+ // Additional value https://datatracker.ietf.org/doc/html/rfc8010/#section-3.1.5
92+ if ($ nameLength === 0x0000 ) {
93+ return $ this ->lastAttribute ->appendValue ($ this ->getAttributeValue ($ attrType , $ state ));
10194 }
102- $ date = $ dateTime ['year ' ] . '- ' . $ dateTime ['month ' ] . '- ' . $ dateTime ['day ' ];
103- $ time = $ dateTime ['hour ' ] . ': ' . sprintf ('%02d ' , $ dateTime ['min ' ]) . ': ' . sprintf ('%02d ' , $ dateTime ['sec ' ]);
104- $ timeZone = $ dateTime ['tz ' ] . sprintf ('%02d ' , $ dateTime ['tzhour ' ]) . sprintf ('%02d ' , $ dateTime ['tzmin ' ]);
10595
106- $ converted = DateTime::createFromFormat ('Y-n-j G:i:sO ' , $ date . ' ' . $ time . $ timeZone );
107- if ($ converted === false ) {
108- throw new RuntimeException ('Invalid DateTime in IPP attribute ' );
109- }
96+ /** @var string $attrName */
97+ $ attrName = $ state ->consume ($ nameLength , IppTypeEnum::NameWithoutLang);
98+ $ attrValue = $ this ->getAttributeValue ($ attrType , $ state );
99+
100+ $ this ->lastAttribute = new IppAttribute ($ attrType ?? IppTypeEnum::Int, $ attrName , $ attrValue );
110101
111- return $ converted ;
102+ return $ this -> lastAttribute ;
112103 }
113104
114- private function unpack ( string $ unpack , string $ string ): string | int
105+ private function getAttributeValue (? IppTypeEnum $ type , IppResponseState $ state ): mixed
115106 {
116- $ data = @unpack ($ unpack , $ string );
117- if ($ data === false || isset ($ data [1 ]) === false || (is_string ($ data [1 ]) === false && is_int ($ data [1 ]) === false )) {
118- throw new RuntimeException ();
107+ if ($ type === IppTypeEnum::Collection) {
108+ return $ this ->getCollection ($ state );
119109 }
120110
121- return $ data [1 ];
111+ /** @var int $valueLength */
112+ $ valueLength = $ state ->consume (2 , IppTypeEnum::Int);
113+
114+ return $ state ->consume ($ valueLength , $ type );
122115 }
123116}
0 commit comments