├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── attr.go ├── attr_test.go ├── const.go ├── decoder.go ├── doc.go ├── encdec_test.go ├── encoder.go ├── formatter.go ├── formatter_test.go ├── go.mod ├── group.go ├── group_test.go ├── index.md ├── message.go ├── message_test.go ├── op.go ├── op_test.go ├── status.go ├── status_test.go ├── tag.go ├── tag_test.go ├── type.go ├── type_test.go ├── value.go └── value_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | ipp-usb 2 | tags 3 | *.swp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Alexander Pevzner 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | -gotags -R . > tags 3 | go build 4 | 5 | test: 6 | go test 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goipp 2 | 3 | [![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp) 4 | ![GitHub](https://img.shields.io/github/license/OpenPrinting/goipp) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/OpenPrinting/goipp)](https://goreportcard.com/report/github.com/OpenPrinting/goipp) 6 | 7 | The goipp library is fairly complete implementation of IPP core protocol in 8 | pure Go. Essentially, it is IPP messages parser/composer. Transport is 9 | not implemented here, because Go standard library has an excellent built-in 10 | HTTP client, and it doesn't make a lot of sense to wrap it here. 11 | 12 | High-level requests, like "print a file" are also not implemented, only the 13 | low-level stuff. 14 | 15 | All documentation is on godoc.org -- follow the link above. Pull requests 16 | are welcomed, assuming they don't break existing API. 17 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /attr.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Message attributes 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "fmt" 13 | "sort" 14 | ) 15 | 16 | // Attributes represents a slice of attributes 17 | type Attributes []Attribute 18 | 19 | // Add Attribute to Attributes 20 | func (attrs *Attributes) Add(attr Attribute) { 21 | *attrs = append(*attrs, attr) 22 | } 23 | 24 | // Clone creates a shallow copy of Attributes. 25 | // For nil input it returns nil output. 26 | func (attrs Attributes) Clone() Attributes { 27 | var attrs2 Attributes 28 | if attrs != nil { 29 | attrs2 = make(Attributes, len(attrs)) 30 | copy(attrs2, attrs) 31 | } 32 | return attrs2 33 | } 34 | 35 | // DeepCopy creates a deep copy of Attributes. 36 | // For nil input it returns nil output. 37 | func (attrs Attributes) DeepCopy() Attributes { 38 | var attrs2 Attributes 39 | if attrs != nil { 40 | attrs2 = make(Attributes, len(attrs)) 41 | for i := range attrs { 42 | attrs2[i] = attrs[i].DeepCopy() 43 | } 44 | } 45 | return attrs2 46 | } 47 | 48 | // Equal checks that attrs and attrs2 are equal 49 | // 50 | // Note, Attributes(nil) and Attributes{} are not Equal but Similar. 51 | func (attrs Attributes) Equal(attrs2 Attributes) bool { 52 | if len(attrs) != len(attrs2) { 53 | return false 54 | } 55 | 56 | if (attrs == nil) != (attrs2 == nil) { 57 | return false 58 | } 59 | 60 | for i, attr := range attrs { 61 | attr2 := attrs2[i] 62 | if !attr.Equal(attr2) { 63 | return false 64 | } 65 | } 66 | 67 | return true 68 | } 69 | 70 | // Similar checks that attrs and attrs2 are **logically** equal, 71 | // which means the following: 72 | // - attrs and addrs2 contain the same set of attributes, 73 | // but may be differently ordered 74 | // - Values of attributes of the same name within attrs and 75 | // attrs2 are similar 76 | // 77 | // Note, Attributes(nil) and Attributes{} are not Equal but Similar. 78 | func (attrs Attributes) Similar(attrs2 Attributes) bool { 79 | // Fast check: if lengths are not the same, attributes 80 | // are definitely not equal 81 | if len(attrs) != len(attrs2) { 82 | return false 83 | } 84 | 85 | // Sort attrs and attrs2 by name 86 | sorted1 := attrs.Clone() 87 | sort.SliceStable(sorted1, func(i, j int) bool { 88 | return sorted1[i].Name < sorted1[j].Name 89 | }) 90 | 91 | sorted2 := attrs2.Clone() 92 | sort.SliceStable(sorted2, func(i, j int) bool { 93 | return sorted2[i].Name < sorted2[j].Name 94 | }) 95 | 96 | // And now compare sorted slices 97 | for i, attr1 := range sorted1 { 98 | attr2 := sorted2[i] 99 | if !attr1.Similar(attr2) { 100 | return false 101 | } 102 | } 103 | 104 | return true 105 | } 106 | 107 | // Attribute represents a single attribute, which consist of 108 | // the Name and one or more Values 109 | type Attribute struct { 110 | Name string // Attribute name 111 | Values Values // Slice of values 112 | } 113 | 114 | // MakeAttribute makes Attribute with single value. 115 | // 116 | // Deprecated. Use [MakeAttr] instead. 117 | func MakeAttribute(name string, tag Tag, value Value) Attribute { 118 | attr := Attribute{Name: name} 119 | attr.Values.Add(tag, value) 120 | return attr 121 | } 122 | 123 | // MakeAttr makes Attribute with one or more values. 124 | func MakeAttr(name string, tag Tag, val1 Value, values ...Value) Attribute { 125 | attr := Attribute{Name: name} 126 | attr.Values.Add(tag, val1) 127 | for _, val := range values { 128 | attr.Values.Add(tag, val) 129 | } 130 | return attr 131 | } 132 | 133 | // MakeAttrCollection makes [Attribute] with [Collection] value. 134 | func MakeAttrCollection(name string, 135 | member1 Attribute, members ...Attribute) Attribute { 136 | 137 | col := make(Collection, len(members)+1) 138 | col[0] = member1 139 | copy(col[1:], members) 140 | 141 | return MakeAttribute(name, TagBeginCollection, col) 142 | } 143 | 144 | // Equal checks that Attribute is equal to another Attribute 145 | // (i.e., names are the same and values are equal) 146 | func (a Attribute) Equal(a2 Attribute) bool { 147 | return a.Name == a2.Name && a.Values.Equal(a2.Values) 148 | } 149 | 150 | // Similar checks that Attribute is **logically** equal to another 151 | // Attribute (i.e., names are the same and values are similar) 152 | func (a Attribute) Similar(a2 Attribute) bool { 153 | return a.Name == a2.Name && a.Values.Similar(a2.Values) 154 | } 155 | 156 | // DeepCopy creates a deep copy of the Attribute 157 | func (a Attribute) DeepCopy() Attribute { 158 | a2 := a 159 | a2.Values = a2.Values.DeepCopy() 160 | return a2 161 | } 162 | 163 | // Unpack attribute value from its wire representation 164 | func (a *Attribute) unpack(tag Tag, value []byte) error { 165 | var err error 166 | var val Value 167 | 168 | switch tag.Type() { 169 | case TypeVoid, TypeCollection: 170 | val = Void{} 171 | 172 | case TypeInteger: 173 | val = Integer(0) 174 | 175 | case TypeBoolean: 176 | val = Boolean(false) 177 | 178 | case TypeString: 179 | val = String("") 180 | 181 | case TypeDateTime: 182 | val = Time{} 183 | 184 | case TypeResolution: 185 | val = Resolution{} 186 | 187 | case TypeRange: 188 | val = Range{} 189 | 190 | case TypeTextWithLang: 191 | val = TextWithLang{} 192 | 193 | case TypeBinary: 194 | val = Binary(nil) 195 | 196 | default: 197 | panic(fmt.Sprintf("(Attribute) uppack(): tag=%s type=%s", tag, tag.Type())) 198 | } 199 | 200 | val, err = val.decode(value) 201 | 202 | if err == nil { 203 | a.Values.Add(tag, val) 204 | } else { 205 | err = fmt.Errorf("%s: %s", tag, err) 206 | } 207 | 208 | return err 209 | } 210 | -------------------------------------------------------------------------------- /attr_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Message attributes tests 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "errors" 13 | "testing" 14 | "time" 15 | "unsafe" 16 | ) 17 | 18 | // TestAttributesEqualSimilar tests Attributes.Equal and 19 | // Attributes.Similar methods 20 | func TestAttributesEqualSimilar(t *testing.T) { 21 | type testData struct { 22 | a1, a2 Attributes // A pair of Attributes slice 23 | equal bool // Expected a1.Equal(a2) output 24 | similar bool // Expected a2.Similar(a2) output 25 | 26 | } 27 | 28 | tests := []testData{ 29 | { 30 | // nil Attributes are equal and similar 31 | a1: nil, 32 | a2: nil, 33 | equal: true, 34 | similar: true, 35 | }, 36 | 37 | { 38 | // Empty Attributes are equal and similar 39 | a1: Attributes{}, 40 | a2: Attributes{}, 41 | equal: true, 42 | similar: true, 43 | }, 44 | 45 | { 46 | // Attributes(nil) vs Attributes{} are similar but not equal 47 | a1: Attributes{}, 48 | a2: nil, 49 | equal: false, 50 | similar: true, 51 | }, 52 | 53 | { 54 | // Attributes of different length are neither equal nor similar 55 | a1: Attributes{ 56 | MakeAttr("attr1", TagInteger, Integer(0)), 57 | }, 58 | a2: Attributes{}, 59 | equal: false, 60 | similar: false, 61 | }, 62 | { 63 | // Same Attributes are equal and similar 64 | a1: Attributes{ 65 | MakeAttr("attr1", TagInteger, Integer(0)), 66 | }, 67 | a2: Attributes{ 68 | MakeAttr("attr1", TagInteger, Integer(0)), 69 | }, 70 | equal: true, 71 | similar: true, 72 | }, 73 | { 74 | // Same tag, different value: not equal or similar 75 | a1: Attributes{ 76 | MakeAttr("attr1", TagInteger, Integer(0)), 77 | }, 78 | a2: Attributes{ 79 | MakeAttr("attr1", TagInteger, Integer(1)), 80 | }, 81 | equal: false, 82 | similar: false, 83 | }, 84 | { 85 | // Same value, tag value: not equal or similar 86 | a1: Attributes{ 87 | MakeAttr("attr1", TagInteger, Integer(0)), 88 | }, 89 | a2: Attributes{ 90 | MakeAttr("attr1", TagEnum, Integer(0)), 91 | }, 92 | equal: false, 93 | similar: false, 94 | }, 95 | 96 | { 97 | // Different but similar Value types: 98 | // Attributes are not equal but similar 99 | a1: Attributes{ 100 | MakeAttr("attr1", TagString, Binary("hello")), 101 | MakeAttr("attr2", TagString, String("world")), 102 | }, 103 | a2: Attributes{ 104 | MakeAttr("attr1", TagString, String("hello")), 105 | MakeAttr("attr2", TagString, Binary("world")), 106 | }, 107 | equal: false, 108 | similar: true, 109 | }, 110 | 111 | { 112 | // Different order: not equal but similar 113 | a1: Attributes{ 114 | MakeAttr("attr1", TagString, String("hello")), 115 | MakeAttr("attr2", TagString, String("world")), 116 | }, 117 | a2: Attributes{ 118 | MakeAttr("attr2", TagString, String("world")), 119 | MakeAttr("attr1", TagString, String("hello")), 120 | }, 121 | equal: false, 122 | similar: true, 123 | }, 124 | } 125 | 126 | for _, test := range tests { 127 | equal := test.a1.Equal(test.a2) 128 | similar := test.a1.Similar(test.a2) 129 | 130 | if equal != test.equal { 131 | t.Errorf("testing Attributes.Equal:\n"+ 132 | "attrs 1: %s\n"+ 133 | "attrs 2: %s\n"+ 134 | "expected: %v\n"+ 135 | "present: %v\n", 136 | test.a1, test.a2, 137 | test.equal, equal, 138 | ) 139 | } 140 | 141 | if similar != test.similar { 142 | t.Errorf("testing Attributes.Similar:\n"+ 143 | "attrs 1: %s\n"+ 144 | "attrs 2: %s\n"+ 145 | "expected: %v\n"+ 146 | "present: %v\n", 147 | test.a1, test.a2, 148 | test.similar, similar, 149 | ) 150 | } 151 | } 152 | } 153 | 154 | // TestAttributesConstructors tests Attributes.Add and MakeAttr 155 | func TestAttributesConstructors(t *testing.T) { 156 | attrs1 := Attributes{ 157 | Attribute{ 158 | Name: "attr1", 159 | Values: Values{ 160 | {TagString, String("hello")}, 161 | {TagString, String("world")}, 162 | }, 163 | }, 164 | Attribute{ 165 | Name: "attr2", 166 | Values: Values{ 167 | {TagInteger, Integer(1)}, 168 | {TagInteger, Integer(2)}, 169 | {TagInteger, Integer(3)}, 170 | }, 171 | }, 172 | } 173 | 174 | attrs2 := Attributes{} 175 | attrs2.Add(MakeAttr("attr1", TagString, String("hello"), String("world"))) 176 | attrs2.Add(MakeAttr("attr2", TagInteger, Integer(1), Integer(2), Integer(3))) 177 | 178 | if !attrs1.Equal(attrs2) { 179 | t.Errorf("Attributes constructors test failed") 180 | } 181 | } 182 | 183 | // TestMakeAttribute tests MakeAttribute function 184 | func TestMakeAttribute(t *testing.T) { 185 | a1 := Attribute{ 186 | Name: "attr", 187 | Values: Values{{TagInteger, Integer(1)}}, 188 | } 189 | 190 | a2 := MakeAttribute("attr", TagInteger, Integer(1)) 191 | 192 | if !a1.Equal(a2) { 193 | t.Errorf("MakeAttribute test failed") 194 | } 195 | } 196 | 197 | // TestAttributesCopy tests Attributes.Clone and Attributes.DeepCopy 198 | func TestAttributesCopy(t *testing.T) { 199 | type testData struct { 200 | attrs Attributes 201 | } 202 | 203 | tests := []testData{ 204 | {nil}, 205 | {Attributes{}}, 206 | { 207 | Attributes{ 208 | MakeAttr("attr1", TagString, String("hello"), String("world")), 209 | MakeAttr("attr2", TagInteger, Integer(1), Integer(2), Integer(3)), 210 | MakeAttr("attr2", TagBoolean, Boolean(true), Boolean(false)), 211 | }, 212 | }, 213 | } 214 | 215 | for _, test := range tests { 216 | clone := test.attrs.Clone() 217 | 218 | if !test.attrs.Equal(clone) { 219 | t.Errorf("testing Attributes.Clone\n"+ 220 | "expected: %#v\n"+ 221 | "present: %#v\n", 222 | test.attrs, 223 | clone, 224 | ) 225 | } 226 | 227 | copy := test.attrs.DeepCopy() 228 | if !test.attrs.Equal(copy) { 229 | t.Errorf("testing Attributes.DeepCopy\n"+ 230 | "expected: %#v\n"+ 231 | "present: %#v\n", 232 | test.attrs, 233 | copy, 234 | ) 235 | } 236 | } 237 | } 238 | 239 | // TestAttributeUnpack tests Attribute.unpack method for all kinds 240 | // of Value types 241 | func TestAttributeUnpack(t *testing.T) { 242 | loc := time.FixedZone("UTC+3:30", 3*3600+1800) 243 | tm, _ := time.ParseInLocation( 244 | time.RFC3339, "2025-03-29T16:48:53+03:30", loc) 245 | 246 | values := Values{ 247 | {TagBoolean, Boolean(true)}, 248 | {TagExtension, Binary{}}, 249 | {TagString, Binary{1, 2, 3}}, 250 | {TagInteger, Integer(123)}, 251 | {TagEnum, Integer(-321)}, 252 | {TagRange, Range{-100, 200}}, 253 | {TagRange, Range{-100, -50}}, 254 | {TagResolution, Resolution{150, 300, UnitsDpi}}, 255 | {TagResolution, Resolution{100, 200, UnitsDpcm}}, 256 | {TagResolution, Resolution{75, 150, 10}}, 257 | {TagName, String("hello")}, 258 | {TagTextLang, TextWithLang{"en-US", "hello"}}, 259 | {TagDateTime, Time{tm}}, 260 | {TagNoValue, Void{}}, 261 | } 262 | 263 | for _, v := range values { 264 | expected := Attribute{Name: "attr", Values: Values{v}} 265 | present := Attribute{Name: "attr"} 266 | data, _ := v.V.encode() 267 | present.unpack(v.T, data) 268 | 269 | if !expected.Equal(present) { 270 | t.Errorf("%x %d", data, unsafe.Sizeof(int(5))) 271 | t.Errorf("testing Attribute.unpack:\n"+ 272 | "expected: %#v\n"+ 273 | "present: %#v\n", 274 | expected, present, 275 | ) 276 | } 277 | } 278 | } 279 | 280 | // TestAttributeUnpackError tests that Attribute.unpack properly 281 | // handles errors from the Value.decode 282 | func TestAttributeUnpackError(t *testing.T) { 283 | noError := errors.New("") 284 | 285 | type testData struct { 286 | t Tag // Input value tag 287 | data []byte // Input data 288 | err string // Expected error 289 | } 290 | 291 | tests := []testData{ 292 | { 293 | t: TagInteger, 294 | data: []byte{}, 295 | err: "integer: value must be 4 bytes", 296 | }, 297 | 298 | { 299 | t: TagBoolean, 300 | data: []byte{}, 301 | err: "boolean: value must be 1 byte", 302 | }, 303 | } 304 | 305 | for _, test := range tests { 306 | attr := Attribute{Name: "attr"} 307 | err := attr.unpack(test.t, test.data) 308 | if err == nil { 309 | err = noError 310 | } 311 | 312 | if err.Error() != test.err { 313 | t.Errorf("testing Attribute.unpack:\n"+ 314 | "input tag: %s\n"+ 315 | "input data: %x\n"+ 316 | "error expected: %s\n"+ 317 | "error present: %s\n", 318 | test.t, test.data, 319 | test.err, err, 320 | ) 321 | 322 | } 323 | } 324 | } 325 | 326 | // TestAttributeUnpackPanic tests that Attribute.unpack panics 327 | // on invalid Tag 328 | func TestAttributeUnpackPanic(t *testing.T) { 329 | defer func() { 330 | recover() 331 | }() 332 | 333 | attr := Attribute{Name: "attr"} 334 | attr.unpack(TagOperationGroup, []byte{}) 335 | 336 | t.Errorf("Attribute.unpack must panic on the invalid Tag") 337 | } 338 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Various constants 7 | */ 8 | 9 | package goipp 10 | 11 | const ( 12 | // ContentType is the HTTP content type for IPP messages 13 | ContentType = "application/ipp" 14 | 15 | // msgPrintIndent used for indentation by message pretty-printer 16 | msgPrintIndent = " " 17 | ) 18 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Message decoder 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "encoding/binary" 13 | "errors" 14 | "fmt" 15 | "io" 16 | ) 17 | 18 | // DecoderOptions represents message decoder options 19 | type DecoderOptions struct { 20 | // EnableWorkarounds, if set to true, enables various workarounds 21 | // for decoding IPP messages that violate IPP protocol specification 22 | // 23 | // Currently it includes the following workarounds: 24 | // * Pantum M7300FDW violates collection encoding rules. 25 | // Instead of using TagMemberName, it uses named attributes 26 | // within the collection 27 | // 28 | // The list of implemented workarounds may grow in the 29 | // future 30 | EnableWorkarounds bool 31 | } 32 | 33 | // messageDecoder represents Message decoder 34 | type messageDecoder struct { 35 | in io.Reader // Input stream 36 | off int // Offset of last read 37 | cnt int // Count of read bytes 38 | opt DecoderOptions // Options 39 | } 40 | 41 | // Decode the message 42 | func (md *messageDecoder) decode(m *Message) error { 43 | // Wire format: 44 | // 45 | // 2 bytes: Version 46 | // 2 bytes: Code (Operation or Status) 47 | // 4 bytes: RequestID 48 | // variable: attributes 49 | // 1 byte: TagEnd 50 | 51 | // Parse message header 52 | var err error 53 | m.Version, err = md.decodeVersion() 54 | if err == nil { 55 | m.Code, err = md.decodeCode() 56 | } 57 | if err == nil { 58 | m.RequestID, err = md.decodeU32() 59 | } 60 | 61 | // Now parse attributes 62 | done := false 63 | var group *Attributes 64 | var attr Attribute 65 | var prev *Attribute 66 | 67 | for err == nil && !done { 68 | var tag Tag 69 | tag, err = md.decodeTag() 70 | 71 | if err != nil { 72 | break 73 | } 74 | 75 | if tag.IsDelimiter() { 76 | prev = nil 77 | } 78 | 79 | if tag.IsGroup() { 80 | m.Groups.Add(Group{tag, nil}) 81 | } 82 | 83 | switch tag { 84 | case TagZero: 85 | err = errors.New("Invalid tag 0") 86 | case TagEnd: 87 | done = true 88 | 89 | case TagOperationGroup: 90 | group = &m.Operation 91 | case TagJobGroup: 92 | group = &m.Job 93 | case TagPrinterGroup: 94 | group = &m.Printer 95 | case TagUnsupportedGroup: 96 | group = &m.Unsupported 97 | case TagSubscriptionGroup: 98 | group = &m.Subscription 99 | case TagEventNotificationGroup: 100 | group = &m.EventNotification 101 | case TagResourceGroup: 102 | group = &m.Resource 103 | case TagDocumentGroup: 104 | group = &m.Document 105 | case TagSystemGroup: 106 | group = &m.System 107 | case TagFuture11Group: 108 | group = &m.Future11 109 | case TagFuture12Group: 110 | group = &m.Future12 111 | case TagFuture13Group: 112 | group = &m.Future13 113 | case TagFuture14Group: 114 | group = &m.Future14 115 | case TagFuture15Group: 116 | group = &m.Future15 117 | 118 | default: 119 | // Decode attribute 120 | if tag == TagMemberName || tag == TagEndCollection { 121 | err = fmt.Errorf("Unexpected tag %s", tag) 122 | } else { 123 | attr, err = md.decodeAttribute(tag) 124 | } 125 | 126 | if err == nil && tag == TagBeginCollection { 127 | attr.Values[0].V, err = md.decodeCollection() 128 | } 129 | 130 | // If everything is OK, save attribute 131 | switch { 132 | case err != nil: 133 | case attr.Name == "": 134 | if prev != nil { 135 | prev.Values.Add(attr.Values[0].T, attr.Values[0].V) 136 | 137 | // Append value to the last Attribute of the 138 | // last Group in the m.Groups 139 | // 140 | // Note, if we are here, this last Attribute definitely exists, 141 | // because: 142 | // * prev != nil 143 | // * prev is set when new named attribute is added 144 | // * prev is reset when delimiter tag is encountered 145 | gLast := &m.Groups[len(m.Groups)-1] 146 | aLast := &gLast.Attrs[len(gLast.Attrs)-1] 147 | aLast.Values.Add(attr.Values[0].T, attr.Values[0].V) 148 | } else { 149 | err = errors.New("Additional value without preceding attribute") 150 | } 151 | case group != nil: 152 | group.Add(attr) 153 | prev = &(*group)[len(*group)-1] 154 | m.Groups[len(m.Groups)-1].Add(attr) 155 | default: 156 | err = errors.New("Attribute without a group") 157 | } 158 | } 159 | } 160 | 161 | if err != nil { 162 | err = fmt.Errorf("%s at 0x%x", err, md.off) 163 | } 164 | 165 | return err 166 | } 167 | 168 | // Decode a Collection 169 | // 170 | // Collection is like a nested object - an attribute which value is a sequence 171 | // of named attributes. Collections can be nested. 172 | // 173 | // Wire format: 174 | // 175 | // ATTR: Tag = TagBeginCollection, - the outer attribute that 176 | // Name = "name", value - ignored contains the collection 177 | // 178 | // ATTR: Tag = TagMemberName, name = "", - member name \ 179 | // value - string, name of the next | 180 | // member | repeated for 181 | // | each member 182 | // ATTR: Tag = any attribute tag, name = "", - repeated for | 183 | // value = member value multi-value / 184 | // members 185 | // 186 | // ATTR: Tag = TagEndCollection, name = "", 187 | // value - ignored 188 | // 189 | // The format looks a bit baroque, but please note that it was added 190 | // in the IPP 2.0. For IPP 1.x collection looks like a single multi-value 191 | // TagBeginCollection attribute (attributes without names considered 192 | // next value for the previously defined named attributes) and so 193 | // 1.x parser silently ignores collections and doesn't get confused 194 | // with them. 195 | func (md *messageDecoder) decodeCollection() (Collection, error) { 196 | collection := make(Collection, 0) 197 | 198 | memberName := "" 199 | 200 | for { 201 | tag, err := md.decodeTag() 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | // Delimiter cannot be inside a collection 207 | if tag.IsDelimiter() { 208 | err = fmt.Errorf("Collection: unexpected tag %s", tag) 209 | return nil, err 210 | } 211 | 212 | // Check for TagMemberName without the subsequent value attribute 213 | if (tag == TagMemberName || tag == TagEndCollection) && memberName != "" { 214 | err = fmt.Errorf("Collection: unexpected %s, expected value tag", tag) 215 | return nil, err 216 | } 217 | 218 | // Fetch next attribute 219 | attr, err := md.decodeAttribute(tag) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | // Process next attribute 225 | switch tag { 226 | case TagEndCollection: 227 | return collection, nil 228 | 229 | case TagMemberName: 230 | memberName = string(attr.Values[0].V.(String)) 231 | if memberName == "" { 232 | err = fmt.Errorf("Collection: %s value is empty", tag) 233 | return nil, err 234 | } 235 | 236 | case TagBeginCollection: 237 | // Decode nested collection 238 | attr.Values[0].V, err = md.decodeCollection() 239 | if err != nil { 240 | return nil, err 241 | } 242 | fallthrough 243 | 244 | default: 245 | if md.opt.EnableWorkarounds && 246 | memberName == "" && attr.Name != "" { 247 | // Workaround for: Pantum M7300FDW 248 | // 249 | // This device violates collection encoding rules. 250 | // Instead of using TagMemberName, it uses named 251 | // attributes within the collection 252 | memberName = attr.Name 253 | } 254 | 255 | if memberName != "" { 256 | attr.Name = memberName 257 | collection = append(collection, attr) 258 | memberName = "" 259 | } else if len(collection) > 0 { 260 | l := len(collection) 261 | collection[l-1].Values.Add(tag, attr.Values[0].V) 262 | } else { 263 | // We've got a value without preceding TagMemberName 264 | err = fmt.Errorf("Collection: unexpected %s, expected %s", tag, TagMemberName) 265 | return nil, err 266 | } 267 | } 268 | } 269 | } 270 | 271 | // Decode a tag 272 | func (md *messageDecoder) decodeTag() (Tag, error) { 273 | t, err := md.decodeU8() 274 | 275 | return Tag(t), err 276 | } 277 | 278 | // Decode a Version 279 | func (md *messageDecoder) decodeVersion() (Version, error) { 280 | code, err := md.decodeU16() 281 | return Version(code), err 282 | } 283 | 284 | // Decode a Code 285 | func (md *messageDecoder) decodeCode() (Code, error) { 286 | code, err := md.decodeU16() 287 | return Code(code), err 288 | } 289 | 290 | // Decode a single attribute 291 | // 292 | // Wire format: 293 | // 294 | // 1 byte: Tag 295 | // 2+N bytes: Name length (2 bytes) + name string 296 | // 2+N bytes: Value length (2 bytes) + value bytes 297 | // 298 | // For the extended tag format, Tag is encoded as TagExtension and 299 | // 4 bytes of the actual tag value prepended to the value bytes 300 | func (md *messageDecoder) decodeAttribute(tag Tag) (Attribute, error) { 301 | var attr Attribute 302 | var value []byte 303 | var err error 304 | 305 | // Obtain attribute name and raw value 306 | attr.Name, err = md.decodeString() 307 | if err != nil { 308 | goto ERROR 309 | } 310 | 311 | value, err = md.decodeBytes() 312 | if err != nil { 313 | goto ERROR 314 | } 315 | 316 | // Handle TagExtension 317 | if tag == TagExtension { 318 | if len(value) < 4 { 319 | err = errors.New("Extension tag truncated") 320 | goto ERROR 321 | } 322 | 323 | t := binary.BigEndian.Uint32(value[:4]) 324 | 325 | if t > 0x7fffffff { 326 | err = fmt.Errorf( 327 | "Extension tag 0x%8.8x out of range", t) 328 | goto ERROR 329 | } 330 | } 331 | 332 | // Unpack value 333 | err = attr.unpack(tag, value) 334 | if err != nil { 335 | goto ERROR 336 | } 337 | 338 | return attr, nil 339 | 340 | // Return a error 341 | ERROR: 342 | return Attribute{}, err 343 | } 344 | 345 | // Decode a 8-bit integer 346 | func (md *messageDecoder) decodeU8() (uint8, error) { 347 | buf := make([]byte, 1) 348 | err := md.read(buf) 349 | return buf[0], err 350 | } 351 | 352 | // Decode a 16-bit integer 353 | func (md *messageDecoder) decodeU16() (uint16, error) { 354 | buf := make([]byte, 2) 355 | err := md.read(buf) 356 | return binary.BigEndian.Uint16(buf[:]), err 357 | } 358 | 359 | // Decode a 32-bit integer 360 | func (md *messageDecoder) decodeU32() (uint32, error) { 361 | buf := make([]byte, 4) 362 | err := md.read(buf) 363 | return binary.BigEndian.Uint32(buf[:]), err 364 | } 365 | 366 | // Decode sequence of bytes 367 | func (md *messageDecoder) decodeBytes() ([]byte, error) { 368 | length, err := md.decodeU16() 369 | if err != nil { 370 | return nil, err 371 | } 372 | 373 | data := make([]byte, length) 374 | err = md.read(data) 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | return data, nil 380 | } 381 | 382 | // Decode string 383 | func (md *messageDecoder) decodeString() (string, error) { 384 | data, err := md.decodeBytes() 385 | if err != nil { 386 | return "", err 387 | } 388 | 389 | return string(data), nil 390 | } 391 | 392 | // Read a piece of raw data from input stream 393 | func (md *messageDecoder) read(data []byte) error { 394 | md.off = md.cnt 395 | 396 | for len(data) > 0 { 397 | n, err := md.in.Read(data) 398 | if n > 0 { 399 | md.cnt += n 400 | data = data[n:] 401 | } else { 402 | md.off = md.cnt 403 | if err == nil || err == io.EOF { 404 | err = errors.New("Message truncated") 405 | } 406 | return err 407 | } 408 | 409 | } 410 | 411 | return nil 412 | } 413 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Package documentation 7 | */ 8 | 9 | /* 10 | Package goipp implements IPP core protocol, as defined by RFC 8010 11 | 12 | It doesn't implement high-level operations, such as "print a document", 13 | "cancel print job" and so on. It's scope is limited to proper generation 14 | and parsing of IPP requests and responses. 15 | 16 | IPP protocol uses the following simple model: 17 | 1. Send a request 18 | 2. Receive a response 19 | 20 | Request and response both has a similar format, represented here 21 | by type Message, with the only difference, that Code field of 22 | that Message is the Operation code in request and Status code 23 | in response. So most of operations are common for request and 24 | response messages 25 | 26 | # Example (Get-Printer-Attributes): 27 | 28 | package main 29 | 30 | import ( 31 | "bytes" 32 | "net/http" 33 | "os" 34 | 35 | "github.com/OpenPrinting/goipp" 36 | ) 37 | 38 | const uri = "http://192.168.1.102:631" 39 | 40 | // Build IPP OpGetPrinterAttributes request 41 | func makeRequest() ([]byte, error) { 42 | m := goipp.NewRequest(goipp.DefaultVersion, goipp.OpGetPrinterAttributes, 1) 43 | m.Operation.Add(goipp.MakeAttribute("attributes-charset", 44 | goipp.TagCharset, goipp.String("utf-8"))) 45 | m.Operation.Add(goipp.MakeAttribute("attributes-natural-language", 46 | goipp.TagLanguage, goipp.String("en-US"))) 47 | m.Operation.Add(goipp.MakeAttribute("printer-uri", 48 | goipp.TagURI, goipp.String(uri))) 49 | m.Operation.Add(goipp.MakeAttribute("requested-attributes", 50 | goipp.TagKeyword, goipp.String("all"))) 51 | 52 | return m.EncodeBytes() 53 | } 54 | 55 | // Check that there is no error 56 | func check(err error) { 57 | if err != nil { 58 | panic(err) 59 | } 60 | } 61 | 62 | func main() { 63 | request, err := makeRequest() 64 | check(err) 65 | 66 | resp, err := http.Post(uri, goipp.ContentType, bytes.NewBuffer(request)) 67 | check(err) 68 | 69 | var respMsg goipp.Message 70 | 71 | err = respMsg.Decode(resp.Body) 72 | check(err) 73 | 74 | respMsg.Print(os.Stdout, false) 75 | } 76 | 77 | # Example (Print PDF file): 78 | 79 | package main 80 | 81 | import ( 82 | "bytes" 83 | "errors" 84 | "fmt" 85 | "io" 86 | "net/http" 87 | "os" 88 | 89 | "github.com/OpenPrinting/goipp" 90 | ) 91 | 92 | const ( 93 | PrinterURL = "ipp://192.168.1.102:631/ipp/print" 94 | TestPage = "onepage-a4.pdf" 95 | ) 96 | 97 | // checkErr checks for an error. If err != nil, it prints error 98 | // message and exits 99 | func checkErr(err error, format string, args ...interface{}) { 100 | if err != nil { 101 | msg := fmt.Sprintf(format, args...) 102 | fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err) 103 | os.Exit(1) 104 | } 105 | } 106 | 107 | // ExamplePrintPDF demo 108 | func main() { 109 | // Build and encode IPP request 110 | req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1) 111 | req.Operation.Add(goipp.MakeAttribute("attributes-charset", 112 | goipp.TagCharset, goipp.String("utf-8"))) 113 | req.Operation.Add(goipp.MakeAttribute("attributes-natural-language", 114 | goipp.TagLanguage, goipp.String("en-US"))) 115 | req.Operation.Add(goipp.MakeAttribute("printer-uri", 116 | goipp.TagURI, goipp.String(PrinterURL))) 117 | req.Operation.Add(goipp.MakeAttribute("requesting-user-name", 118 | goipp.TagName, goipp.String("John Doe"))) 119 | req.Operation.Add(goipp.MakeAttribute("job-name", 120 | goipp.TagName, goipp.String("job name"))) 121 | req.Operation.Add(goipp.MakeAttribute("document-format", 122 | goipp.TagMimeType, goipp.String("application/pdf"))) 123 | 124 | payload, err := req.EncodeBytes() 125 | checkErr(err, "IPP encode") 126 | 127 | // Open document file 128 | file, err := os.Open(TestPage) 129 | checkErr(err, "Open document file") 130 | 131 | defer file.Close() 132 | 133 | // Build HTTP request 134 | body := io.MultiReader(bytes.NewBuffer(payload), file) 135 | 136 | httpReq, err := http.NewRequest(http.MethodPost, PrinterURL, body) 137 | checkErr(err, "HTTP") 138 | 139 | httpReq.Header.Set("content-type", goipp.ContentType) 140 | httpReq.Header.Set("accept", goipp.ContentType) 141 | 142 | // Execute HTTP request 143 | httpRsp, err := http.DefaultClient.Do(httpReq) 144 | if httpRsp != nil { 145 | defer httpRsp.Body.Close() 146 | } 147 | 148 | checkErr(err, "HTTP") 149 | 150 | if httpRsp.StatusCode/100 != 2 { 151 | checkErr(errors.New(httpRsp.Status), "HTTP") 152 | } 153 | 154 | // Decode IPP response 155 | rsp := &goipp.Message{} 156 | err = rsp.Decode(httpRsp.Body) 157 | checkErr(err, "IPP decode") 158 | 159 | if goipp.Status(rsp.Code) != goipp.StatusOk { 160 | err = errors.New(goipp.Status(rsp.Code).String()) 161 | checkErr(err, "IPP") 162 | } 163 | } 164 | */ 165 | package goipp 166 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Message encoder 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "encoding/binary" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "math" 17 | ) 18 | 19 | // Type messageEncoder represents Message encoder 20 | type messageEncoder struct { 21 | out io.Writer // Output stream 22 | } 23 | 24 | // Encode the message 25 | func (me *messageEncoder) encode(m *Message) error { 26 | // Wire format: 27 | // 28 | // 2 bytes: Version 29 | // 2 bytes: Code (Operation or Status) 30 | // 4 bytes: RequestID 31 | // variable: attributes 32 | // 1 byte: TagEnd 33 | 34 | // Encode message header 35 | var err error 36 | err = me.encodeU16(uint16(m.Version)) 37 | if err == nil { 38 | err = me.encodeU16(uint16(m.Code)) 39 | } 40 | if err == nil { 41 | err = me.encodeU32(uint32(m.RequestID)) 42 | } 43 | 44 | // Encode attributes 45 | for _, grp := range m.AttrGroups() { 46 | err = me.encodeTag(grp.Tag) 47 | if err == nil { 48 | for _, attr := range grp.Attrs { 49 | if attr.Name == "" { 50 | err = errors.New("Attribute without name") 51 | } else { 52 | err = me.encodeAttr(attr, true) 53 | } 54 | } 55 | } 56 | 57 | if err != nil { 58 | break 59 | } 60 | } 61 | 62 | if err == nil { 63 | err = me.encodeTag(TagEnd) 64 | } 65 | 66 | return err 67 | } 68 | 69 | // Encode attribute 70 | func (me *messageEncoder) encodeAttr(attr Attribute, checkTag bool) error { 71 | // Wire format 72 | // 1 byte: Tag 73 | // 2 bytes: len(Name) 74 | // variable: name 75 | // 2 bytes: len(Value) 76 | // variable Value 77 | // 78 | // And each additional value comes as attribute 79 | // without name 80 | if len(attr.Values) == 0 { 81 | return errors.New("Attribute without value") 82 | } 83 | 84 | name := attr.Name 85 | for _, val := range attr.Values { 86 | tag := val.T 87 | 88 | if checkTag { 89 | if tag.IsDelimiter() || tag == TagMemberName || tag == TagEndCollection { 90 | return fmt.Errorf("Tag %s cannot be used with value", tag) 91 | } 92 | 93 | if uint(tag) >= 0x100 { 94 | return fmt.Errorf("Tag %s out of range", tag) 95 | } 96 | } 97 | 98 | err := me.encodeTag(tag) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = me.encodeName(name) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | err = me.encodeValue(val.T, val.V) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | name = "" // Each additional value comes without name 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // Encode 8-bit integer 120 | func (me *messageEncoder) encodeU8(v uint8) error { 121 | return me.write([]byte{v}) 122 | } 123 | 124 | // Encode 16-bit integer 125 | func (me *messageEncoder) encodeU16(v uint16) error { 126 | return me.write([]byte{byte(v >> 8), byte(v)}) 127 | } 128 | 129 | // Encode 32-bit integer 130 | func (me *messageEncoder) encodeU32(v uint32) error { 131 | return me.write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}) 132 | } 133 | 134 | // Encode Tag 135 | func (me *messageEncoder) encodeTag(tag Tag) error { 136 | return me.encodeU8(byte(tag)) 137 | } 138 | 139 | // Encode Attribute name 140 | func (me *messageEncoder) encodeName(name string) error { 141 | if len(name) > math.MaxInt16 { 142 | return fmt.Errorf("Attribute name exceeds %d bytes", 143 | math.MaxInt16) 144 | } 145 | 146 | err := me.encodeU16(uint16(len(name))) 147 | if err == nil { 148 | err = me.write([]byte(name)) 149 | } 150 | 151 | return err 152 | } 153 | 154 | // Encode Attribute value 155 | func (me *messageEncoder) encodeValue(tag Tag, v Value) error { 156 | // Check Value type vs the Tag 157 | tagType := tag.Type() 158 | if tagType == TypeVoid { 159 | v = Void{} // Ignore supplied value 160 | } else if tagType != v.Type() { 161 | return fmt.Errorf("Tag %s: %s value required, %s present", 162 | tag, tagType, v.Type()) 163 | } 164 | 165 | // Convert Value to bytes in wire representation. 166 | data, err := v.encode() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | if len(data) > math.MaxInt16 { 172 | return fmt.Errorf("Attribute value exceeds %d bytes", 173 | math.MaxInt16) 174 | } 175 | 176 | // TagExtension encoding rules enforcement. 177 | if tag == TagExtension { 178 | if len(data) < 4 { 179 | return fmt.Errorf( 180 | "Extension tag truncated (%d bytes)", len(data)) 181 | } 182 | 183 | t := binary.BigEndian.Uint32(data) 184 | if t > 0x7fffffff { 185 | return fmt.Errorf( 186 | "Extension tag 0x%8.8x out of range", t) 187 | } 188 | } 189 | 190 | // Encode the value 191 | err = me.encodeU16(uint16(len(data))) 192 | if err == nil { 193 | err = me.write(data) 194 | } 195 | 196 | // Handle collection 197 | if collection, ok := v.(Collection); ok { 198 | return me.encodeCollection(tag, collection) 199 | } 200 | 201 | return err 202 | } 203 | 204 | // Encode collection 205 | func (me *messageEncoder) encodeCollection(tag Tag, collection Collection) error { 206 | for _, attr := range collection { 207 | if attr.Name == "" { 208 | return errors.New("Collection member without name") 209 | } 210 | 211 | attrName := MakeAttribute("", TagMemberName, String(attr.Name)) 212 | 213 | err := me.encodeAttr(attrName, false) 214 | if err == nil { 215 | err = me.encodeAttr( 216 | Attribute{Name: "", Values: attr.Values}, true) 217 | } 218 | 219 | if err != nil { 220 | return err 221 | } 222 | } 223 | 224 | return me.encodeAttr(MakeAttribute("", TagEndCollection, Void{}), false) 225 | } 226 | 227 | // Write a piece of raw data to output stream 228 | func (me *messageEncoder) write(data []byte) error { 229 | for len(data) > 0 { 230 | n, err := me.out.Write(data) 231 | if err != nil { 232 | return err 233 | } 234 | data = data[n:] 235 | } 236 | 237 | return nil 238 | } 239 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP formatter (pretty-printer) 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "bytes" 13 | "fmt" 14 | "io" 15 | "strings" 16 | ) 17 | 18 | // Formatter parameters: 19 | const ( 20 | // FormatterIndentShift is the indentation shift, number of space 21 | // characters per indentation level. 22 | FormatterIndentShift = 4 23 | ) 24 | 25 | // Formatter formats IPP messages, attributes, groups etc 26 | // for pretty-printing. 27 | // 28 | // It supersedes [Message.Print] method which is now considered 29 | // deprecated. 30 | type Formatter struct { 31 | indent int // Indentation level 32 | userIndent int // User-settable indent 33 | buf bytes.Buffer // Output buffer 34 | } 35 | 36 | // NewFormatter returns a new Formatter. 37 | func NewFormatter() *Formatter { 38 | return &Formatter{} 39 | } 40 | 41 | // Reset resets the formatter. 42 | func (f *Formatter) Reset() { 43 | f.buf.Reset() 44 | f.indent = 0 45 | } 46 | 47 | // SetIndent configures indentation. If parameter is greater that 48 | // zero, the specified amount of white space will prepended to each 49 | // non-empty output line. 50 | func (f *Formatter) SetIndent(n int) { 51 | f.userIndent = 0 52 | if n > 0 { 53 | f.userIndent = n 54 | } 55 | } 56 | 57 | // Bytes returns formatted text as a byte slice. 58 | func (f *Formatter) Bytes() []byte { 59 | return f.buf.Bytes() 60 | } 61 | 62 | // String returns formatted text as a string. 63 | func (f *Formatter) String() string { 64 | return f.buf.String() 65 | } 66 | 67 | // WriteTo writes formatted text to w. 68 | // It implements [io.WriterTo] interface. 69 | func (f *Formatter) WriteTo(w io.Writer) (int64, error) { 70 | return f.buf.WriteTo(w) 71 | } 72 | 73 | // Printf writes formatted line into the [Formatter], automatically 74 | // indented and with added newline at the end. 75 | // 76 | // It returns the number of bytes written and nil as an error (for 77 | // consistency with other printf-like functions). 78 | func (f *Formatter) Printf(format string, args ...interface{}) (int, error) { 79 | s := fmt.Sprintf(format, args...) 80 | lines := strings.Split(s, "\n") 81 | cnt := 0 82 | 83 | for _, line := range lines { 84 | if line != "" { 85 | cnt += f.doIndent() 86 | } 87 | 88 | f.buf.WriteString(line) 89 | f.buf.WriteByte('\n') 90 | cnt += len(line) + 1 91 | } 92 | 93 | return cnt, nil 94 | } 95 | 96 | // FmtRequest formats a request [Message]. 97 | func (f *Formatter) FmtRequest(msg *Message) { 98 | f.fmtMessage(msg, true) 99 | } 100 | 101 | // FmtResponse formats a response [Message]. 102 | func (f *Formatter) FmtResponse(msg *Message) { 103 | f.fmtMessage(msg, false) 104 | } 105 | 106 | // fmtMessage formats a request or response Message. 107 | func (f *Formatter) fmtMessage(msg *Message, request bool) { 108 | f.Printf("{") 109 | f.indent++ 110 | 111 | f.Printf("REQUEST-ID %d", msg.RequestID) 112 | f.Printf("VERSION %s", msg.Version) 113 | 114 | if request { 115 | f.Printf("OPERATION %s", Op(msg.Code)) 116 | } else { 117 | f.Printf("STATUS %s", Status(msg.Code)) 118 | } 119 | 120 | if groups := msg.AttrGroups(); len(groups) != 0 { 121 | f.Printf("") 122 | f.FmtGroups(groups) 123 | } 124 | 125 | f.indent-- 126 | f.Printf("}") 127 | } 128 | 129 | // FmtGroups formats a [Groups] slice. 130 | func (f *Formatter) FmtGroups(groups Groups) { 131 | for i, g := range groups { 132 | if i != 0 { 133 | f.Printf("") 134 | } 135 | f.FmtGroup(g) 136 | } 137 | } 138 | 139 | // FmtGroup formats a single [Group]. 140 | func (f *Formatter) FmtGroup(g Group) { 141 | f.Printf("GROUP %s", g.Tag) 142 | f.FmtAttributes(g.Attrs) 143 | } 144 | 145 | // FmtAttributes formats a [Attributes] slice. 146 | func (f *Formatter) FmtAttributes(attrs Attributes) { 147 | for _, attr := range attrs { 148 | f.FmtAttribute(attr) 149 | } 150 | } 151 | 152 | // FmtAttribute formats a single [Attribute]. 153 | func (f *Formatter) FmtAttribute(attr Attribute) { 154 | f.fmtAttributeOrMember(attr, false) 155 | } 156 | 157 | // FmtAttributes formats a single [Attribute] or collection member. 158 | func (f *Formatter) fmtAttributeOrMember(attr Attribute, member bool) { 159 | buf := &f.buf 160 | 161 | f.doIndent() 162 | if member { 163 | fmt.Fprintf(buf, "MEMBER %q", attr.Name) 164 | } else { 165 | fmt.Fprintf(buf, "ATTR %q", attr.Name) 166 | } 167 | 168 | tag := TagZero 169 | for _, val := range attr.Values { 170 | if val.T != tag { 171 | fmt.Fprintf(buf, " %s:", val.T) 172 | tag = val.T 173 | } 174 | 175 | if collection, ok := val.V.(Collection); ok { 176 | if f.onNL() { 177 | f.Printf("{") 178 | } else { 179 | buf.Write([]byte(" {\n")) 180 | } 181 | 182 | f.indent++ 183 | 184 | for _, attr2 := range collection { 185 | f.fmtAttributeOrMember(attr2, true) 186 | } 187 | 188 | f.indent-- 189 | f.Printf("}") 190 | } else { 191 | fmt.Fprintf(buf, " %s", val.V) 192 | } 193 | } 194 | 195 | f.forceNL() 196 | } 197 | 198 | // onNL returns true if Formatter is at the beginning of new line. 199 | func (f *Formatter) onNL() bool { 200 | b := f.buf.Bytes() 201 | return len(b) == 0 || b[len(b)-1] == '\n' 202 | } 203 | 204 | // forceNL inserts newline character if Formatter is not at the. 205 | // beginning of new line 206 | func (f *Formatter) forceNL() { 207 | if !f.onNL() { 208 | f.buf.WriteByte('\n') 209 | } 210 | } 211 | 212 | // doIndent outputs indentation space. 213 | // It returns number of characters written. 214 | func (f *Formatter) doIndent() int { 215 | cnt := FormatterIndentShift * f.indent 216 | cnt += f.userIndent 217 | 218 | n := cnt 219 | for n > len(formatterSomeSpace) { 220 | f.buf.Write([]byte(formatterSomeSpace[:])) 221 | n -= len(formatterSomeSpace) 222 | } 223 | 224 | f.buf.Write([]byte(formatterSomeSpace[:n])) 225 | 226 | return cnt 227 | } 228 | 229 | // formatterSomeSpace contains some space characters for 230 | // fast output of indentation space. 231 | var formatterSomeSpace [64]byte 232 | 233 | func init() { 234 | for i := range formatterSomeSpace { 235 | formatterSomeSpace[i] = ' ' 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP formatter test 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | // TestFmtAttribute runs Formatter.FmtAttribute tests 17 | func TestFmtAttribute(t *testing.T) { 18 | type testData struct { 19 | attr Attribute // Inpur attribute 20 | out []string // Expected output 21 | indent int // Indentation 22 | } 23 | 24 | tests := []testData{ 25 | // Simple test 26 | { 27 | attr: MakeAttr( 28 | "attributes-charset", 29 | TagCharset, 30 | String("utf-8")), 31 | out: []string{ 32 | `ATTR "attributes-charset" charset: utf-8`, 33 | }, 34 | }, 35 | 36 | // Simple test with indentation 37 | { 38 | attr: MakeAttr( 39 | "attributes-charset", 40 | TagCharset, 41 | String("utf-8")), 42 | indent: 2, 43 | out: []string{ 44 | ` ATTR "attributes-charset" charset: utf-8`, 45 | }, 46 | }, 47 | 48 | // Simple test with huge indentation 49 | { 50 | attr: MakeAttr( 51 | "attributes-charset", 52 | TagCharset, 53 | String("utf-8")), 54 | indent: 123, 55 | out: []string{ 56 | strings.Repeat(" ", 123) + 57 | `ATTR "attributes-charset" charset: utf-8`, 58 | }, 59 | }, 60 | 61 | // Collection 62 | { 63 | attr: MakeAttrCollection("media-col", 64 | MakeAttrCollection("media-size", 65 | MakeAttribute("x-dimension", 66 | TagInteger, Integer(10160)), 67 | MakeAttribute("y-dimension", 68 | TagInteger, Integer(15240)), 69 | ), 70 | MakeAttribute("media-left-margin", 71 | TagInteger, Integer(0)), 72 | MakeAttribute("media-right-margin", 73 | TagInteger, Integer(0)), 74 | MakeAttribute("media-top-margin", 75 | TagInteger, Integer(0)), 76 | MakeAttribute("media-bottom-margin", 77 | TagInteger, Integer(0)), 78 | ), 79 | 80 | out: []string{ 81 | `ATTR "media-col" collection: {`, 82 | ` MEMBER "media-size" collection: {`, 83 | ` MEMBER "x-dimension" integer: 10160`, 84 | ` MEMBER "y-dimension" integer: 15240`, 85 | ` }`, 86 | ` MEMBER "media-left-margin" integer: 0`, 87 | ` MEMBER "media-right-margin" integer: 0`, 88 | ` MEMBER "media-top-margin" integer: 0`, 89 | ` MEMBER "media-bottom-margin" integer: 0`, 90 | `}`, 91 | }, 92 | }, 93 | 94 | // 1SetOf Collection 95 | { 96 | attr: MakeAttr("media-size-supported", 97 | TagBeginCollection, 98 | Collection{ 99 | MakeAttribute("x-dimension", 100 | TagInteger, Integer(20990)), 101 | MakeAttribute("y-dimension", 102 | TagInteger, Integer(29704)), 103 | }, 104 | Collection{ 105 | MakeAttribute("x-dimension", 106 | TagInteger, Integer(14852)), 107 | MakeAttribute("y-dimension", 108 | TagInteger, Integer(20990)), 109 | }, 110 | ), 111 | indent: 2, 112 | out: []string{ 113 | ` ATTR "media-size-supported" collection: {`, 114 | ` MEMBER "x-dimension" integer: 20990`, 115 | ` MEMBER "y-dimension" integer: 29704`, 116 | ` }`, 117 | ` {`, 118 | ` MEMBER "x-dimension" integer: 14852`, 119 | ` MEMBER "y-dimension" integer: 20990`, 120 | ` }`, 121 | }, 122 | }, 123 | 124 | // Multiple values 125 | { 126 | attr: MakeAttr("page-delivery-supported", 127 | TagKeyword, 128 | String("reverse-order"), 129 | String("same-order")), 130 | 131 | out: []string{ 132 | `ATTR "page-delivery-supported" keyword: reverse-order same-order`, 133 | }, 134 | }, 135 | 136 | // Values of mixed type 137 | { 138 | attr: Attribute{ 139 | Name: "page-ranges", 140 | Values: Values{ 141 | {TagInteger, Integer(1)}, 142 | {TagInteger, Integer(2)}, 143 | {TagInteger, Integer(3)}, 144 | {TagRange, Range{5, 7}}, 145 | }, 146 | }, 147 | 148 | out: []string{ 149 | `ATTR "page-ranges" integer: 1 2 3 rangeOfInteger: 5-7`, 150 | }, 151 | }, 152 | } 153 | 154 | f := NewFormatter() 155 | for _, test := range tests { 156 | f.Reset() 157 | f.SetIndent(test.indent) 158 | 159 | expected := strings.Join(test.out, "\n") + "\n" 160 | f.FmtAttribute(test.attr) 161 | out := f.String() 162 | 163 | if out != expected { 164 | t.Errorf("output mismatch\n"+ 165 | "expected:\n%s"+ 166 | "present:\n%s", 167 | expected, out) 168 | } 169 | } 170 | } 171 | 172 | // TestFmtRequestResponse runs Formatter.FmtRequest and 173 | // Formatter.FmtResponse tests 174 | func TestFmtRequestResponse(t *testing.T) { 175 | type testData struct { 176 | msg *Message // Input message 177 | rq bool // This is request 178 | out []string // Expected output 179 | } 180 | 181 | tests := []testData{ 182 | { 183 | msg: &Message{ 184 | Version: MakeVersion(2, 0), 185 | Code: Code(OpGetPrinterAttributes), 186 | RequestID: 1, 187 | 188 | Operation: []Attribute{ 189 | MakeAttribute( 190 | "attributes-charset", 191 | TagCharset, 192 | String("utf-8")), 193 | MakeAttribute( 194 | "attributes-natural-language", 195 | TagLanguage, 196 | String("en-us")), 197 | MakeAttribute( 198 | "requested-attributes", 199 | TagKeyword, 200 | String("printer-name")), 201 | }, 202 | }, 203 | rq: true, 204 | out: []string{ 205 | `{`, 206 | ` REQUEST-ID 1`, 207 | ` VERSION 2.0`, 208 | ` OPERATION Get-Printer-Attributes`, 209 | ``, 210 | ` GROUP operation-attributes-tag`, 211 | ` ATTR "attributes-charset" charset: utf-8`, 212 | ` ATTR "attributes-natural-language" naturalLanguage: en-us`, 213 | ` ATTR "requested-attributes" keyword: printer-name`, 214 | `}`, 215 | }, 216 | }, 217 | 218 | { 219 | msg: &Message{ 220 | Version: MakeVersion(2, 0), 221 | Code: Code(StatusOk), 222 | RequestID: 1, 223 | 224 | Operation: []Attribute{ 225 | MakeAttribute( 226 | "attributes-charset", 227 | TagCharset, 228 | String("utf-8")), 229 | MakeAttribute( 230 | "attributes-natural-language", 231 | TagLanguage, 232 | String("en-us")), 233 | }, 234 | 235 | Printer: []Attribute{ 236 | MakeAttribute( 237 | "printer-name", 238 | TagName, 239 | String("Kyocera_ECOSYS_M2040dn")), 240 | }, 241 | }, 242 | rq: false, 243 | out: []string{ 244 | `{`, 245 | ` REQUEST-ID 1`, 246 | ` VERSION 2.0`, 247 | ` STATUS successful-ok`, 248 | ``, 249 | ` GROUP operation-attributes-tag`, 250 | ` ATTR "attributes-charset" charset: utf-8`, 251 | ` ATTR "attributes-natural-language" naturalLanguage: en-us`, 252 | ``, 253 | ` GROUP printer-attributes-tag`, 254 | ` ATTR "printer-name" nameWithoutLanguage: Kyocera_ECOSYS_M2040dn`, 255 | `}`, 256 | }, 257 | }, 258 | } 259 | 260 | f := NewFormatter() 261 | for _, test := range tests { 262 | f.Reset() 263 | if test.rq { 264 | f.FmtRequest(test.msg) 265 | } else { 266 | f.FmtResponse(test.msg) 267 | } 268 | 269 | out := f.String() 270 | expected := strings.Join(test.out, "\n") + "\n" 271 | 272 | if out != expected { 273 | t.Errorf("output mismatch\n"+ 274 | "expected:\n%s"+ 275 | "present:\n%s", 276 | expected, out) 277 | } 278 | } 279 | } 280 | 281 | // TestFmtBytes tests Formatter.Bytes function 282 | func TestFmtBytes(t *testing.T) { 283 | msg := &Message{ 284 | Version: MakeVersion(2, 0), 285 | Code: Code(OpGetPrinterAttributes), 286 | RequestID: 1, 287 | 288 | Operation: []Attribute{ 289 | MakeAttribute( 290 | "attributes-charset", 291 | TagCharset, 292 | String("utf-8")), 293 | MakeAttribute( 294 | "attributes-natural-language", 295 | TagLanguage, 296 | String("en-us")), 297 | MakeAttribute( 298 | "requested-attributes", 299 | TagKeyword, 300 | String("printer-name")), 301 | }, 302 | } 303 | 304 | f := NewFormatter() 305 | f.FmtRequest(msg) 306 | 307 | s := f.String() 308 | b := f.Bytes() 309 | 310 | // Note, we've already tested f.String() function in the previous 311 | // tests, so f.Bytes() is tested against it 312 | if string(b) != s { 313 | t.Errorf("Formatter.Bytes test failed:\n"+ 314 | "expected: %s\n"+ 315 | "present: %s\n", 316 | s, 317 | b, 318 | ) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenPrinting/goipp 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Groups of attributes 7 | */ 8 | 9 | package goipp 10 | 11 | import "sort" 12 | 13 | // Group represents a group of attributes. 14 | // 15 | // Since 1.1.0 16 | type Group struct { 17 | Tag Tag // Group tag 18 | Attrs Attributes // Group attributes 19 | } 20 | 21 | // Groups represents a sequence of groups 22 | // 23 | // The primary purpose of this type is to represent 24 | // messages with repeated groups with the same group tag 25 | // 26 | // # See Message type documentation for more details 27 | // 28 | // Since 1.1.0 29 | type Groups []Group 30 | 31 | // Add Attribute to the Group 32 | func (g *Group) Add(attr Attribute) { 33 | g.Attrs.Add(attr) 34 | } 35 | 36 | // Equal checks that groups g and g2 are equal 37 | func (g Group) Equal(g2 Group) bool { 38 | return g.Tag == g2.Tag && g.Attrs.Equal(g2.Attrs) 39 | } 40 | 41 | // Similar checks that groups g and g2 are **logically** equal. 42 | func (g Group) Similar(g2 Group) bool { 43 | return g.Tag == g2.Tag && g.Attrs.Similar(g2.Attrs) 44 | } 45 | 46 | // Clone creates a shallow copy of the Group 47 | func (g Group) Clone() Group { 48 | g2 := g 49 | g2.Attrs = g.Attrs.Clone() 50 | return g2 51 | } 52 | 53 | // DeepCopy creates a deep copy of the Group 54 | func (g Group) DeepCopy() Group { 55 | g2 := g 56 | g2.Attrs = g.Attrs.DeepCopy() 57 | return g2 58 | } 59 | 60 | // Add Group to Groups 61 | func (groups *Groups) Add(g Group) { 62 | *groups = append(*groups, g) 63 | } 64 | 65 | // Clone creates a shallow copy of Groups. 66 | // For nil input it returns nil output. 67 | func (groups Groups) Clone() Groups { 68 | var groups2 Groups 69 | if groups != nil { 70 | groups2 = make(Groups, len(groups)) 71 | copy(groups2, groups) 72 | } 73 | return groups2 74 | } 75 | 76 | // DeepCopy creates a deep copy of Groups. 77 | // For nil input it returns nil output. 78 | func (groups Groups) DeepCopy() Groups { 79 | var groups2 Groups 80 | if groups != nil { 81 | groups2 = make(Groups, len(groups)) 82 | for i := range groups { 83 | groups2[i] = groups[i].DeepCopy() 84 | } 85 | } 86 | return groups2 87 | } 88 | 89 | // Equal checks that groups and groups2 are equal. 90 | // 91 | // Note, Groups(nil) and Groups{} are not equal but similar. 92 | func (groups Groups) Equal(groups2 Groups) bool { 93 | if len(groups) != len(groups2) { 94 | return false 95 | } 96 | 97 | if (groups == nil) != (groups2 == nil) { 98 | return false 99 | } 100 | 101 | for i, g := range groups { 102 | g2 := groups2[i] 103 | if !g.Equal(g2) { 104 | return false 105 | } 106 | } 107 | 108 | return true 109 | } 110 | 111 | // Similar checks that groups and groups2 are **logically** equal, 112 | // which means the following: 113 | // - groups and groups2 contain the same set of 114 | // groups, but groups with different tags may 115 | // be reordered between each other. 116 | // - groups with the same tag cannot be reordered. 117 | // - attributes of corresponding groups are similar. 118 | // 119 | // Note, Groups(nil) and Groups{} are not equal but similar. 120 | func (groups Groups) Similar(groups2 Groups) bool { 121 | // Fast check: if lengths are not the same, groups 122 | // are definitely not equal 123 | if len(groups) != len(groups2) { 124 | return false 125 | } 126 | 127 | // Sort groups by tag 128 | groups = groups.Clone() 129 | groups2 = groups2.Clone() 130 | 131 | sort.SliceStable(groups, func(i, j int) bool { 132 | return groups[i].Tag < groups[j].Tag 133 | }) 134 | 135 | sort.SliceStable(groups2, func(i, j int) bool { 136 | return groups2[i].Tag < groups2[j].Tag 137 | }) 138 | 139 | // Now compare, group by group 140 | for i := range groups { 141 | if !groups[i].Similar(groups2[i]) { 142 | return false 143 | } 144 | } 145 | 146 | return true 147 | } 148 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Tests fop groups of attributes 7 | */ 8 | 9 | package goipp 10 | 11 | import "testing" 12 | 13 | // TestGroupEqualSimilar tests Group.Equal and Group.Similar 14 | func TestGroupEqualSimilar(t *testing.T) { 15 | type testData struct { 16 | g1, g2 Group // A pair of Attributes slice 17 | equal bool // Expected g1.Equal(g2) output 18 | similar bool // Expected g2.Similar(g2) output 19 | } 20 | 21 | attrs1 := Attributes{ 22 | MakeAttr("attr1", TagInteger, Integer(1)), 23 | MakeAttr("attr2", TagInteger, Integer(2)), 24 | MakeAttr("attr3", TagInteger, Integer(3)), 25 | } 26 | 27 | attrs2 := Attributes{ 28 | MakeAttr("attr3", TagInteger, Integer(3)), 29 | MakeAttr("attr2", TagInteger, Integer(2)), 30 | MakeAttr("attr1", TagInteger, Integer(1)), 31 | } 32 | 33 | tests := []testData{ 34 | { 35 | g1: Group{TagJobGroup, nil}, 36 | g2: Group{TagJobGroup, nil}, 37 | equal: true, 38 | similar: true, 39 | }, 40 | 41 | { 42 | g1: Group{TagJobGroup, Attributes{}}, 43 | g2: Group{TagJobGroup, Attributes{}}, 44 | equal: true, 45 | similar: true, 46 | }, 47 | 48 | { 49 | g1: Group{TagJobGroup, Attributes{}}, 50 | g2: Group{TagJobGroup, nil}, 51 | equal: false, 52 | similar: true, 53 | }, 54 | 55 | { 56 | g1: Group{TagJobGroup, attrs1}, 57 | g2: Group{TagJobGroup, attrs1}, 58 | equal: true, 59 | similar: true, 60 | }, 61 | 62 | { 63 | g1: Group{TagJobGroup, attrs1}, 64 | g2: Group{TagJobGroup, attrs2}, 65 | equal: false, 66 | similar: true, 67 | }, 68 | } 69 | 70 | for _, test := range tests { 71 | equal := test.g1.Equal(test.g2) 72 | similar := test.g1.Similar(test.g2) 73 | 74 | if equal != test.equal { 75 | t.Errorf("testing Group.Equal:\n"+ 76 | "attrs 1: %s\n"+ 77 | "attrs 2: %s\n"+ 78 | "expected: %v\n"+ 79 | "present: %v\n", 80 | test.g1, test.g2, 81 | test.equal, equal, 82 | ) 83 | } 84 | 85 | if similar != test.similar { 86 | t.Errorf("testing Group.Similar:\n"+ 87 | "attrs 1: %s\n"+ 88 | "attrs 2: %s\n"+ 89 | "expected: %v\n"+ 90 | "present: %v\n", 91 | test.g1, test.g2, 92 | test.similar, similar, 93 | ) 94 | } 95 | } 96 | } 97 | 98 | // TestGroupAdd tests Group.Add 99 | func TestGroupAdd(t *testing.T) { 100 | g1 := Group{ 101 | TagJobGroup, 102 | Attributes{ 103 | MakeAttr("attr1", TagInteger, Integer(1)), 104 | MakeAttr("attr2", TagInteger, Integer(2)), 105 | MakeAttr("attr3", TagInteger, Integer(3)), 106 | }, 107 | } 108 | 109 | g2 := Group{Tag: TagJobGroup} 110 | for _, attr := range g1.Attrs { 111 | g2.Add(attr) 112 | } 113 | 114 | if !g1.Equal(g2) { 115 | t.Errorf("Group.Add test failed:\n"+ 116 | "expected: %#v\n"+ 117 | "present: %#v\n", 118 | g1, g2, 119 | ) 120 | } 121 | } 122 | 123 | // TestGroupCopy tests Group.Clone and Group.DeepCopy 124 | func TestGroupCopy(t *testing.T) { 125 | type testData struct { 126 | g Group 127 | } 128 | 129 | attrs := Attributes{ 130 | MakeAttr("attr1", TagInteger, Integer(1)), 131 | MakeAttr("attr2", TagInteger, Integer(2)), 132 | MakeAttr("attr3", TagInteger, Integer(3)), 133 | } 134 | 135 | tests := []testData{ 136 | {Group{TagJobGroup, nil}}, 137 | {Group{TagJobGroup, Attributes{}}}, 138 | {Group{TagJobGroup, attrs}}, 139 | } 140 | 141 | for _, test := range tests { 142 | clone := test.g.Clone() 143 | 144 | if !test.g.Equal(clone) { 145 | t.Errorf("testing Group.Clone\n"+ 146 | "expected: %#v\n"+ 147 | "present: %#v\n", 148 | test.g, 149 | clone, 150 | ) 151 | } 152 | 153 | copy := test.g.DeepCopy() 154 | if !test.g.Equal(copy) { 155 | t.Errorf("testing Group.DeepCopy\n"+ 156 | "expected: %#v\n"+ 157 | "present: %#v\n", 158 | test.g, 159 | copy, 160 | ) 161 | } 162 | } 163 | } 164 | 165 | // TestGroupEqualSimilar tests Group.Equal and Group.Similar 166 | func TestGroupsEqualSimilar(t *testing.T) { 167 | type testData struct { 168 | groups1, groups2 Groups // A pair of Attributes slice 169 | equal bool // Expected g1.Equal(g2) output 170 | similar bool // Expected g2.Similar(g2) output 171 | } 172 | 173 | g1 := Group{ 174 | TagJobGroup, 175 | Attributes{MakeAttr("attr1", TagInteger, Integer(1))}, 176 | } 177 | 178 | g2 := Group{ 179 | TagJobGroup, 180 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 181 | } 182 | 183 | g3 := Group{ 184 | TagPrinterGroup, 185 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 186 | } 187 | 188 | tests := []testData{ 189 | { 190 | // nil equal and similar to nil 191 | groups1: nil, 192 | groups2: nil, 193 | equal: true, 194 | similar: true, 195 | }, 196 | 197 | { 198 | // Empty groups equal and similar to empty groups 199 | groups1: Groups{}, 200 | groups2: Groups{}, 201 | equal: true, 202 | similar: true, 203 | }, 204 | 205 | { 206 | // nil similar but not equal to empty groups 207 | groups1: nil, 208 | groups2: Groups{}, 209 | equal: false, 210 | similar: true, 211 | }, 212 | 213 | { 214 | // groups of different size neither equal nor similar 215 | groups1: Groups{g1, g2, g3}, 216 | groups2: Groups{g1, g2}, 217 | equal: false, 218 | similar: false, 219 | }, 220 | 221 | { 222 | // Same list of groups: equal and similar 223 | groups1: Groups{g1, g2, g3}, 224 | groups2: Groups{g1, g2, g3}, 225 | equal: true, 226 | similar: true, 227 | }, 228 | 229 | { 230 | // Groups with different group tags reordered. 231 | // Similar but not equal. 232 | groups1: Groups{g1, g2, g3}, 233 | groups2: Groups{g3, g1, g2}, 234 | equal: false, 235 | similar: true, 236 | }, 237 | 238 | { 239 | // Groups with the same group tags reordered. 240 | // Neither equal nor similar 241 | groups1: Groups{g1, g2, g3}, 242 | groups2: Groups{g2, g1, g3}, 243 | equal: false, 244 | similar: false, 245 | }, 246 | } 247 | 248 | for _, test := range tests { 249 | equal := test.groups1.Equal(test.groups2) 250 | similar := test.groups1.Similar(test.groups2) 251 | 252 | if equal != test.equal { 253 | t.Errorf("testing Groups.Equal:\n"+ 254 | "attrs 1: %s\n"+ 255 | "attrs 2: %s\n"+ 256 | "expected: %v\n"+ 257 | "present: %v\n", 258 | test.groups1, test.groups2, 259 | test.equal, equal, 260 | ) 261 | } 262 | 263 | if similar != test.similar { 264 | t.Errorf("testing Groups.Similar:\n"+ 265 | "attrs 1: %s\n"+ 266 | "attrs 2: %s\n"+ 267 | "expected: %v\n"+ 268 | "present: %v\n", 269 | test.groups1, test.groups2, 270 | test.similar, similar, 271 | ) 272 | } 273 | } 274 | } 275 | 276 | // TestGroupsAdd tests Groups.Add 277 | func TestGroupsAdd(t *testing.T) { 278 | g1 := Group{ 279 | TagJobGroup, 280 | Attributes{MakeAttr("attr1", TagInteger, Integer(1))}, 281 | } 282 | 283 | g2 := Group{ 284 | TagJobGroup, 285 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 286 | } 287 | 288 | g3 := Group{ 289 | TagPrinterGroup, 290 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 291 | } 292 | 293 | groups1 := Groups{g1, g2, g3} 294 | 295 | groups2 := Groups{} 296 | groups2.Add(g1) 297 | groups2.Add(g2) 298 | groups2.Add(g3) 299 | 300 | if !groups1.Equal(groups2) { 301 | t.Errorf("Groups.Add test failed:\n"+ 302 | "expected: %#v\n"+ 303 | "present: %#v\n", 304 | groups1, groups2, 305 | ) 306 | } 307 | } 308 | 309 | // TestGroupsCopy tests Groups.Clone and Groups.DeepCopy 310 | func TestGroupsCopy(t *testing.T) { 311 | g1 := Group{ 312 | TagJobGroup, 313 | Attributes{MakeAttr("attr1", TagInteger, Integer(1))}, 314 | } 315 | 316 | g2 := Group{ 317 | TagJobGroup, 318 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 319 | } 320 | 321 | g3 := Group{ 322 | TagPrinterGroup, 323 | Attributes{MakeAttr("attr2", TagInteger, Integer(2))}, 324 | } 325 | 326 | type testData struct { 327 | groups Groups 328 | } 329 | 330 | tests := []testData{ 331 | {nil}, 332 | {Groups{}}, 333 | {Groups{g1, g2, g3}}, 334 | } 335 | 336 | for _, test := range tests { 337 | clone := test.groups.Clone() 338 | 339 | if !test.groups.Equal(clone) { 340 | t.Errorf("testing Groups.Clone\n"+ 341 | "expected: %#v\n"+ 342 | "present: %#v\n", 343 | test.groups, 344 | clone, 345 | ) 346 | } 347 | 348 | copy := test.groups.DeepCopy() 349 | if !test.groups.Equal(copy) { 350 | t.Errorf("testing Groups.DeepCopy\n"+ 351 | "expected: %#v\n"+ 352 | "present: %#v\n", 353 | test.groups, 354 | copy, 355 | ) 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # goipp 2 | 3 | [![godoc.org](https://godoc.org/github.com/OpenPrinting/goipp?status.svg)](http://godoc.org/github.com/OpenPrinting/goipp) 4 | ![GitHub](https://img.shields.io/github/license/OpenPrinting/goipp) 5 | 6 | The goipp library is fairly complete implementation of IPP core protocol in 7 | pure Go. Essentially, it is IPP messages parser/composer. Transport is 8 | not implemented here, because Go standard library has an excellent built-in 9 | HTTP client, and it doesn't make a lot of sense to wrap it here. 10 | 11 | High-level requests, like "print a file" are also not implemented, only the 12 | low-level stuff. 13 | 14 | All documentation is on godoc.org -- follow the link above. Pull requests 15 | are welcomed, assuming they don't break existing API. 16 | 17 | For more information and software downloads, please visit the 18 | [Project's page at GitHub](https://github.com/OpenPrinting/sane-airscan) 19 | 20 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP protocol messages 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "bytes" 13 | "fmt" 14 | "io" 15 | ) 16 | 17 | // Code represents Op(operation) or Status codes 18 | type Code uint16 19 | 20 | // Version represents a protocol version. It consist 21 | // of Major and Minor version codes, packed into a single 22 | // 16-bit word 23 | type Version uint16 24 | 25 | // DefaultVersion is the default IPP version (2.0 for now) 26 | const DefaultVersion Version = 0x0200 27 | 28 | // MakeVersion makes version from major and minor parts 29 | func MakeVersion(major, minor uint8) Version { 30 | return Version(major)<<8 | Version(minor) 31 | } 32 | 33 | // Major returns a major part of version 34 | func (v Version) Major() uint8 { 35 | return uint8(v >> 8) 36 | } 37 | 38 | // Minor returns a minor part of version 39 | func (v Version) Minor() uint8 { 40 | return uint8(v) 41 | } 42 | 43 | // String() converts version to string (i.e., "2.0") 44 | func (v Version) String() string { 45 | return fmt.Sprintf("%d.%d", v.Major(), v.Minor()) 46 | } 47 | 48 | // Message represents a single IPP message, which may be either 49 | // client request or server response 50 | type Message struct { 51 | // Common header 52 | Version Version // Protocol version 53 | Code Code // Operation for request, status for response 54 | RequestID uint32 // Set in request, returned in response 55 | 56 | // Groups of Attributes 57 | // 58 | // This field allows to represent messages with repeated 59 | // groups of attributes with the same group tag. The most 60 | // noticeable use case is the Get-Jobs response which uses 61 | // multiple Job groups, one per returned job. See RFC 8011, 62 | // 4.2.6.2. for more details 63 | // 64 | // See also the following discussions which explain the demand 65 | // to implement this interface: 66 | // https://github.com/OpenPrinting/goipp/issues/2 67 | // https://github.com/OpenPrinting/goipp/pull/3 68 | // 69 | // With respect to backward compatibility, the following 70 | // behavior is implemented here: 71 | // 1. (*Message).Decode() fills both Groups and named per-group 72 | // fields (i.e., Operation, Job etc) 73 | // 2. (*Message).Encode() and (*Message) Print, if Groups != nil, 74 | // uses Groups and ignores named per-group fields. Otherwise, 75 | // named fields are used as in 1.0.0 76 | // 3. (*Message) Equal(), for each message uses Groups if 77 | // it is not nil or named per-group fields otherwise. 78 | // In another words, Equal() compares messages as if 79 | // they were encoded 80 | // 81 | // Since 1.1.0 82 | Groups Groups 83 | 84 | // Attributes, by group 85 | Operation Attributes // Operation attributes 86 | Job Attributes // Job attributes 87 | Printer Attributes // Printer attributes 88 | Unsupported Attributes // Unsupported attributes 89 | Subscription Attributes // Subscription attributes 90 | EventNotification Attributes // Event Notification attributes 91 | Resource Attributes // Resource attributes 92 | Document Attributes // Document attributes 93 | System Attributes // System attributes 94 | Future11 Attributes // \ 95 | Future12 Attributes // \ 96 | Future13 Attributes // | Reserved for future extensions 97 | Future14 Attributes // / 98 | Future15 Attributes // / 99 | } 100 | 101 | // NewRequest creates a new request message 102 | // 103 | // Use DefaultVersion as a first argument, if you don't 104 | // have any specific needs 105 | func NewRequest(v Version, op Op, id uint32) *Message { 106 | return &Message{ 107 | Version: v, 108 | Code: Code(op), 109 | RequestID: id, 110 | } 111 | } 112 | 113 | // NewResponse creates a new response message 114 | // 115 | // Use DefaultVersion as a first argument, if you don't 116 | func NewResponse(v Version, status Status, id uint32) *Message { 117 | return &Message{ 118 | Version: v, 119 | Code: Code(status), 120 | RequestID: id, 121 | } 122 | } 123 | 124 | // NewMessageWithGroups creates a new message with Groups of 125 | // attributes. 126 | // 127 | // Fields like m.Operation, m.Job. m.Printer... and so on will 128 | // be properly filled automatically. 129 | func NewMessageWithGroups(v Version, code Code, 130 | id uint32, groups Groups) *Message { 131 | 132 | m := &Message{ 133 | Version: v, 134 | Code: code, 135 | RequestID: id, 136 | Groups: groups, 137 | } 138 | 139 | for _, grp := range m.Groups { 140 | switch grp.Tag { 141 | case TagOperationGroup: 142 | m.Operation = append(m.Operation, grp.Attrs...) 143 | case TagJobGroup: 144 | m.Job = append(m.Job, grp.Attrs...) 145 | case TagPrinterGroup: 146 | m.Printer = append(m.Printer, grp.Attrs...) 147 | case TagUnsupportedGroup: 148 | m.Unsupported = append(m.Unsupported, grp.Attrs...) 149 | case TagSubscriptionGroup: 150 | m.Subscription = append(m.Subscription, grp.Attrs...) 151 | case TagEventNotificationGroup: 152 | m.EventNotification = append(m.EventNotification, 153 | grp.Attrs...) 154 | case TagResourceGroup: 155 | m.Resource = append(m.Resource, grp.Attrs...) 156 | case TagDocumentGroup: 157 | m.Document = append(m.Document, grp.Attrs...) 158 | case TagSystemGroup: 159 | m.System = append(m.System, grp.Attrs...) 160 | case TagFuture11Group: 161 | m.Future11 = append(m.Future11, grp.Attrs...) 162 | case TagFuture12Group: 163 | m.Future12 = append(m.Future12, grp.Attrs...) 164 | case TagFuture13Group: 165 | m.Future13 = append(m.Future13, grp.Attrs...) 166 | case TagFuture14Group: 167 | m.Future14 = append(m.Future14, grp.Attrs...) 168 | case TagFuture15Group: 169 | m.Future15 = append(m.Future15, grp.Attrs...) 170 | } 171 | } 172 | 173 | return m 174 | } 175 | 176 | // Equal checks that two messages are equal 177 | func (m Message) Equal(m2 Message) bool { 178 | if m.Version != m2.Version || 179 | m.Code != m2.Code || 180 | m.RequestID != m2.RequestID { 181 | return false 182 | } 183 | 184 | groups := m.AttrGroups() 185 | groups2 := m2.AttrGroups() 186 | 187 | return groups.Equal(groups2) 188 | } 189 | 190 | // Similar checks that two messages are **logically** equal, 191 | // which means the following: 192 | // - Version, Code and RequestID are equal 193 | // - Groups of attributes are Similar 194 | func (m Message) Similar(m2 Message) bool { 195 | if m.Version != m2.Version || 196 | m.Code != m2.Code || 197 | m.RequestID != m2.RequestID { 198 | return false 199 | } 200 | 201 | groups := m.AttrGroups() 202 | groups2 := m2.AttrGroups() 203 | 204 | return groups.Similar(groups2) 205 | } 206 | 207 | // Reset the message into initial state 208 | func (m *Message) Reset() { 209 | *m = Message{} 210 | } 211 | 212 | // Encode message 213 | func (m *Message) Encode(out io.Writer) error { 214 | me := messageEncoder{ 215 | out: out, 216 | } 217 | 218 | return me.encode(m) 219 | } 220 | 221 | // EncodeBytes encodes message to byte slice 222 | func (m *Message) EncodeBytes() ([]byte, error) { 223 | var buf bytes.Buffer 224 | 225 | err := m.Encode(&buf) 226 | return buf.Bytes(), err 227 | } 228 | 229 | // Decode reads message from io.Reader 230 | func (m *Message) Decode(in io.Reader) error { 231 | return m.DecodeEx(in, DecoderOptions{}) 232 | } 233 | 234 | // DecodeEx reads message from io.Reader 235 | // 236 | // It is extended version of the Decode method, with additional 237 | // DecoderOptions parameter 238 | func (m *Message) DecodeEx(in io.Reader, opt DecoderOptions) error { 239 | md := messageDecoder{ 240 | in: in, 241 | opt: opt, 242 | } 243 | 244 | m.Reset() 245 | return md.decode(m) 246 | } 247 | 248 | // DecodeBytes decodes message from byte slice 249 | func (m *Message) DecodeBytes(data []byte) error { 250 | return m.Decode(bytes.NewBuffer(data)) 251 | } 252 | 253 | // DecodeBytesEx decodes message from byte slice 254 | // 255 | // It is extended version of the DecodeBytes method, with additional 256 | // DecoderOptions parameter 257 | func (m *Message) DecodeBytesEx(data []byte, opt DecoderOptions) error { 258 | return m.DecodeEx(bytes.NewBuffer(data), opt) 259 | } 260 | 261 | // Print pretty-prints the message. The 'request' parameter affects 262 | // interpretation of Message.Code: it is interpreted either 263 | // as [Op] or as [Status]. 264 | // 265 | // Deprecated. Use [Formatter] instead. 266 | func (m *Message) Print(out io.Writer, request bool) { 267 | f := Formatter{} 268 | 269 | if request { 270 | f.FmtRequest(m) 271 | } else { 272 | f.FmtResponse(m) 273 | } 274 | 275 | f.WriteTo(out) 276 | } 277 | 278 | // AttrGroups returns [Message] attributes as a sequence of 279 | // attribute groups. 280 | // 281 | // If [Message.Groups] is set, it will be returned. 282 | // 283 | // Otherwise, [Groups] will be reconstructed from [Message.Operation], 284 | // [Message.Job], [Message.Printer] and so on. 285 | // 286 | // Groups with nil [Group.Attrs] will be skipped, but groups with non-nil 287 | // will be not, even if len(Attrs) == 0 288 | func (m *Message) AttrGroups() Groups { 289 | // If m.Groups is set, use it 290 | if m.Groups != nil { 291 | return m.Groups 292 | } 293 | 294 | // Initialize slice of groups 295 | groups := Groups{ 296 | {TagOperationGroup, m.Operation}, 297 | {TagJobGroup, m.Job}, 298 | {TagPrinterGroup, m.Printer}, 299 | {TagUnsupportedGroup, m.Unsupported}, 300 | {TagSubscriptionGroup, m.Subscription}, 301 | {TagEventNotificationGroup, m.EventNotification}, 302 | {TagResourceGroup, m.Resource}, 303 | {TagDocumentGroup, m.Document}, 304 | {TagSystemGroup, m.System}, 305 | {TagFuture11Group, m.Future11}, 306 | {TagFuture12Group, m.Future12}, 307 | {TagFuture13Group, m.Future13}, 308 | {TagFuture14Group, m.Future14}, 309 | {TagFuture15Group, m.Future15}, 310 | } 311 | 312 | // Skip all empty groups 313 | out := 0 314 | for in := 0; in < len(groups); in++ { 315 | if groups[in].Attrs != nil { 316 | groups[out] = groups[in] 317 | out++ 318 | } 319 | } 320 | 321 | return groups[:out] 322 | } 323 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Message tests 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "bytes" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | // TestVersion tests Version functions 19 | func TestVersion(t *testing.T) { 20 | type testData struct { 21 | major, minor uint8 // Major/minor parts 22 | ver Version // Resulting version 23 | str string // Its string representation 24 | } 25 | 26 | tests := []testData{ 27 | { 28 | major: 2, 29 | minor: 0, 30 | ver: 0x0200, 31 | str: "2.0", 32 | }, 33 | 34 | { 35 | major: 2, 36 | minor: 1, 37 | ver: 0x0201, 38 | str: "2.1", 39 | }, 40 | 41 | { 42 | major: 1, 43 | minor: 1, 44 | ver: 0x0101, 45 | str: "1.1", 46 | }, 47 | } 48 | 49 | for _, test := range tests { 50 | ver := MakeVersion(test.major, test.minor) 51 | if ver != test.ver { 52 | t.Errorf("MakeVersion test failed:\n"+ 53 | "version expected: 0x%4.4x\n"+ 54 | "version present: 0x%4.4x\n", 55 | uint(test.ver), uint(ver), 56 | ) 57 | continue 58 | } 59 | 60 | str := ver.String() 61 | if str != test.str { 62 | t.Errorf("Version.String test failed:\n"+ 63 | "expected: %s\n"+ 64 | "present: %s\n", 65 | test.str, str, 66 | ) 67 | continue 68 | } 69 | 70 | major := ver.Major() 71 | if major != test.major { 72 | t.Errorf("Version.Major test failed:\n"+ 73 | "expected: %d\n"+ 74 | "present: %d\n", 75 | test.major, major, 76 | ) 77 | continue 78 | } 79 | 80 | minor := ver.Minor() 81 | if minor != test.minor { 82 | t.Errorf("Version.Minor test failed:\n"+ 83 | "expected: %d\n"+ 84 | "present: %d\n", 85 | test.minor, minor, 86 | ) 87 | continue 88 | } 89 | } 90 | } 91 | 92 | // TestNewRequestResponse tests NewRequest and NewResponse functions 93 | func TestNewRequestResponse(t *testing.T) { 94 | msg := &Message{ 95 | Version: MakeVersion(2, 0), 96 | Code: 1, 97 | RequestID: 0x12345, 98 | } 99 | 100 | rq := NewRequest(msg.Version, Op(msg.Code), msg.RequestID) 101 | if !reflect.DeepEqual(msg, rq) { 102 | t.Errorf("NewRequest test failed:\n"+ 103 | "expected: %#v\n"+ 104 | "present: %#v\n", 105 | msg, rq, 106 | ) 107 | } 108 | 109 | rsp := NewResponse(msg.Version, Status(msg.Code), msg.RequestID) 110 | if !reflect.DeepEqual(msg, rsp) { 111 | t.Errorf("NewRequest test failed:\n"+ 112 | "expected: %#v\n"+ 113 | "present: %#v\n", 114 | msg, rsp, 115 | ) 116 | } 117 | } 118 | 119 | // TestNewMessageWithGroups tests the NewMessageWithGroups function. 120 | func TestNewMessageWithGroups(t *testing.T) { 121 | // Populate groups 122 | ops := Group{ 123 | TagOperationGroup, 124 | Attributes{ 125 | MakeAttr("ops", TagInteger, Integer(1)), 126 | }, 127 | } 128 | 129 | prn1 := Group{ 130 | TagPrinterGroup, 131 | Attributes{ 132 | MakeAttr("prn1", TagInteger, Integer(2)), 133 | }, 134 | } 135 | 136 | prn2 := Group{ 137 | TagPrinterGroup, 138 | Attributes{ 139 | MakeAttr("prn2", TagInteger, Integer(3)), 140 | }, 141 | } 142 | 143 | prn3 := Group{ 144 | TagPrinterGroup, 145 | Attributes{ 146 | MakeAttr("prn3", TagInteger, Integer(4)), 147 | }, 148 | } 149 | 150 | job := Group{ 151 | TagJobGroup, 152 | Attributes{ 153 | MakeAttr("job", TagInteger, Integer(5)), 154 | }, 155 | } 156 | 157 | unsupp := Group{ 158 | TagUnsupportedGroup, 159 | Attributes{ 160 | MakeAttr("unsupp", TagInteger, Integer(6)), 161 | }, 162 | } 163 | 164 | sub := Group{ 165 | TagSubscriptionGroup, 166 | Attributes{ 167 | MakeAttr("sub", TagInteger, Integer(7)), 168 | }, 169 | } 170 | 171 | evnt := Group{ 172 | TagEventNotificationGroup, 173 | Attributes{ 174 | MakeAttr("evnt", TagInteger, Integer(8)), 175 | }, 176 | } 177 | 178 | res := Group{ 179 | TagResourceGroup, 180 | Attributes{ 181 | MakeAttr("res", TagInteger, Integer(9)), 182 | }, 183 | } 184 | 185 | doc := Group{ 186 | TagDocumentGroup, 187 | Attributes{ 188 | MakeAttr("doc", TagInteger, Integer(10)), 189 | }, 190 | } 191 | 192 | sys := Group{ 193 | TagSystemGroup, 194 | Attributes{ 195 | MakeAttr("sys", TagInteger, Integer(11)), 196 | }, 197 | } 198 | 199 | future11 := Group{ 200 | TagFuture11Group, 201 | Attributes{ 202 | MakeAttr("future11", TagInteger, Integer(12)), 203 | }, 204 | } 205 | 206 | future12 := Group{ 207 | TagFuture12Group, 208 | Attributes{ 209 | MakeAttr("future12", TagInteger, Integer(13)), 210 | }, 211 | } 212 | 213 | future13 := Group{ 214 | TagFuture13Group, 215 | Attributes{ 216 | MakeAttr("future13", TagInteger, Integer(14)), 217 | }, 218 | } 219 | 220 | future14 := Group{ 221 | TagFuture14Group, 222 | Attributes{ 223 | MakeAttr("future14", TagInteger, Integer(15)), 224 | }, 225 | } 226 | 227 | future15 := Group{ 228 | TagFuture15Group, 229 | Attributes{ 230 | MakeAttr("future15", TagInteger, Integer(16)), 231 | }, 232 | } 233 | 234 | groups := Groups{ 235 | ops, 236 | prn1, prn2, prn3, 237 | job, 238 | unsupp, 239 | sub, 240 | evnt, 241 | res, 242 | doc, 243 | sys, 244 | future11, 245 | future12, 246 | future13, 247 | future14, 248 | future15, 249 | } 250 | 251 | msg := NewMessageWithGroups(DefaultVersion, 1, 123, groups) 252 | expected := &Message{ 253 | Version: DefaultVersion, 254 | Code: 1, 255 | RequestID: 123, 256 | Groups: groups, 257 | Operation: ops.Attrs, 258 | Job: job.Attrs, 259 | Unsupported: unsupp.Attrs, 260 | Subscription: sub.Attrs, 261 | EventNotification: evnt.Attrs, 262 | Resource: res.Attrs, 263 | Document: doc.Attrs, 264 | System: sys.Attrs, 265 | Future11: future11.Attrs, 266 | Future12: future12.Attrs, 267 | Future13: future13.Attrs, 268 | Future14: future14.Attrs, 269 | Future15: future15.Attrs, 270 | } 271 | expected.Printer = prn1.Attrs 272 | expected.Printer = append(expected.Printer, prn2.Attrs...) 273 | expected.Printer = append(expected.Printer, prn3.Attrs...) 274 | 275 | if !reflect.DeepEqual(msg, expected) { 276 | t.Errorf("NewMessageWithGroups test failed:\n"+ 277 | "expected: %#v\n"+ 278 | "present: %#v\n", 279 | expected, 280 | msg, 281 | ) 282 | } 283 | } 284 | 285 | // TestNewMessageWithGroups tests the Message.AttrGroups function. 286 | func TestMessageAttrGroups(t *testing.T) { 287 | // Create a message for testing 288 | uri := "ipp://192/168.0.1/ipp/print" 289 | 290 | m := NewRequest(DefaultVersion, OpCreateJob, 1) 291 | 292 | m.Operation.Add(MakeAttr("attributes-charset", 293 | TagCharset, String("utf-8"))) 294 | m.Operation.Add(MakeAttr("attributes-natural-language", 295 | TagLanguage, String("en-US"))) 296 | m.Operation.Add(MakeAttr("printer-uri", 297 | TagURI, String(uri))) 298 | 299 | m.Job.Add(MakeAttr("copies", TagInteger, Integer(1))) 300 | 301 | // Compare m.AttrGroups() with expectations 302 | groups := m.AttrGroups() 303 | expected := Groups{ 304 | Group{ 305 | Tag: TagOperationGroup, 306 | Attrs: Attributes{ 307 | MakeAttr("attributes-charset", 308 | TagCharset, String("utf-8")), 309 | MakeAttr("attributes-natural-language", 310 | TagLanguage, String("en-US")), 311 | MakeAttr("printer-uri", 312 | TagURI, String(uri)), 313 | }, 314 | }, 315 | Group{ 316 | Tag: TagJobGroup, 317 | Attrs: Attributes{ 318 | MakeAttr("copies", TagInteger, Integer(1)), 319 | }, 320 | }, 321 | } 322 | 323 | if !reflect.DeepEqual(groups, expected) { 324 | t.Errorf("Message.AttrGroups test failed:\n"+ 325 | "expected: %#v\n"+ 326 | "present: %#v\n", 327 | expected, groups, 328 | ) 329 | } 330 | 331 | // Set m.Groups. Check that it takes precedence. 332 | expected = Groups{ 333 | Group{ 334 | Tag: TagOperationGroup, 335 | Attrs: Attributes{ 336 | MakeAttr("attributes-charset", 337 | TagCharset, String("utf-8")), 338 | }, 339 | }, 340 | } 341 | 342 | m.Groups = expected 343 | groups = m.AttrGroups() 344 | 345 | if !reflect.DeepEqual(groups, expected) { 346 | t.Errorf("Message.AttrGroups test failed:\n"+ 347 | "expected: %#v\n"+ 348 | "present: %#v\n", 349 | expected, groups, 350 | ) 351 | } 352 | } 353 | 354 | // TestMessageEqualSimilar tests Message.Equal and Message.Similar functions. 355 | func TestMessageEqualSimilar(t *testing.T) { 356 | type testData struct { 357 | m1, m2 Message // Input messages 358 | equal bool // Expected Message.Equal output 359 | similar bool // Expected Message.Similar output 360 | } 361 | 362 | uri := "ipp://192/168.0.1/ipp/print" 363 | 364 | tests := []testData{ 365 | // Empty messages are equal and similar 366 | { 367 | m1: Message{}, 368 | m2: Message{}, 369 | equal: true, 370 | similar: true, 371 | }, 372 | 373 | // Messages with different Version/Code/RequestID are 374 | // neither equal or similar 375 | { 376 | m1: Message{}, 377 | m2: Message{Version: 1}, 378 | equal: false, 379 | similar: false, 380 | }, 381 | 382 | { 383 | m1: Message{}, 384 | m2: Message{Code: 1}, 385 | equal: false, 386 | similar: false, 387 | }, 388 | 389 | { 390 | m1: Message{}, 391 | m2: Message{RequestID: 1}, 392 | equal: false, 393 | similar: false, 394 | }, 395 | 396 | // If the same attributes represented as Message.Groups in one 397 | // message and via Message.Operation/Job/Printer etc in the 398 | // another message, these messages are equal and similar 399 | { 400 | m1: Message{ 401 | Groups: Groups{ 402 | Group{ 403 | Tag: TagOperationGroup, 404 | Attrs: Attributes{ 405 | MakeAttr("attributes-charset", 406 | TagCharset, String("utf-8")), 407 | MakeAttr("attributes-natural-language", 408 | TagLanguage, String("en-US")), 409 | MakeAttr("printer-uri", 410 | TagURI, String(uri)), 411 | }, 412 | }, 413 | Group{ 414 | Tag: TagJobGroup, 415 | Attrs: Attributes{ 416 | MakeAttr("copies", TagInteger, Integer(1)), 417 | }, 418 | }, 419 | }, 420 | }, 421 | 422 | m2: Message{ 423 | Operation: Attributes{ 424 | MakeAttr("attributes-charset", 425 | TagCharset, String("utf-8")), 426 | MakeAttr("attributes-natural-language", 427 | TagLanguage, String("en-US")), 428 | MakeAttr("printer-uri", 429 | TagURI, String(uri)), 430 | }, 431 | 432 | Job: Attributes{ 433 | MakeAttr("copies", TagInteger, Integer(1)), 434 | }, 435 | }, 436 | 437 | equal: true, 438 | similar: true, 439 | }, 440 | 441 | // Messages with the different order of the same set of attributes 442 | // are similar but not equal. 443 | { 444 | m1: Message{ 445 | Operation: Attributes{ 446 | MakeAttr("attributes-charset", 447 | TagCharset, String("utf-8")), 448 | MakeAttr("attributes-natural-language", 449 | TagLanguage, String("en-US")), 450 | MakeAttr("printer-uri", 451 | TagURI, String(uri)), 452 | }, 453 | }, 454 | 455 | m2: Message{ 456 | Operation: Attributes{ 457 | MakeAttr("attributes-charset", 458 | TagCharset, String("utf-8")), 459 | MakeAttr("printer-uri", 460 | TagURI, String(uri)), 461 | MakeAttr("attributes-natural-language", 462 | TagLanguage, String("en-US")), 463 | }, 464 | }, 465 | 466 | equal: false, 467 | similar: true, 468 | }, 469 | } 470 | 471 | for _, test := range tests { 472 | equal := test.m1.Equal(test.m2) 473 | if equal != test.equal { 474 | var buf1, buf2 bytes.Buffer 475 | test.m1.Print(&buf1, true) 476 | test.m2.Print(&buf2, true) 477 | 478 | t.Errorf("testing Message.Equal:\n"+ 479 | "message 1: %s\n"+ 480 | "message 2: %s\n"+ 481 | "expected: %v\n"+ 482 | "present: %v\n", 483 | &buf1, &buf2, 484 | test.equal, equal, 485 | ) 486 | } 487 | 488 | similar := test.m1.Similar(test.m2) 489 | if similar != test.similar { 490 | var buf1, buf2 bytes.Buffer 491 | test.m1.Print(&buf1, true) 492 | test.m2.Print(&buf2, true) 493 | 494 | t.Errorf("testing Message.Similar:\n"+ 495 | "message 1: %s\n"+ 496 | "message 2: %s\n"+ 497 | "expected: %v\n"+ 498 | "present: %v\n", 499 | &buf1, &buf2, 500 | test.similar, similar, 501 | ) 502 | } 503 | } 504 | } 505 | 506 | // TestMessageReset tests Message.Reset function 507 | func TestMessageReset(t *testing.T) { 508 | uri := "ipp://192/168.0.1/ipp/print" 509 | m := Message{ 510 | Groups: Groups{ 511 | Group{ 512 | Tag: TagOperationGroup, 513 | Attrs: Attributes{ 514 | MakeAttr("attributes-charset", 515 | TagCharset, String("utf-8")), 516 | MakeAttr("attributes-natural-language", 517 | TagLanguage, String("en-US")), 518 | MakeAttr("printer-uri", 519 | TagURI, String(uri)), 520 | }, 521 | }, 522 | Group{ 523 | Tag: TagJobGroup, 524 | Attrs: Attributes{ 525 | MakeAttr("copies", TagInteger, Integer(1)), 526 | }, 527 | }, 528 | }, 529 | } 530 | 531 | m.Reset() 532 | 533 | if !reflect.ValueOf(m).IsZero() { 534 | t.Errorf("Message.Reset test failed") 535 | } 536 | } 537 | 538 | // TestMessagePrint tests Message.Print function 539 | func TestMessagePrint(t *testing.T) { 540 | uri := "ipp://192/168.0.1/ipp/print" 541 | m := Message{ 542 | Code: 2, 543 | Version: MakeVersion(2, 0), 544 | RequestID: 1, 545 | Groups: Groups{ 546 | Group{ 547 | Tag: TagOperationGroup, 548 | Attrs: Attributes{ 549 | MakeAttr("attributes-charset", 550 | TagCharset, String("utf-8")), 551 | MakeAttr("attributes-natural-language", 552 | TagLanguage, String("en-US")), 553 | MakeAttr("printer-uri", 554 | TagURI, String(uri)), 555 | }, 556 | }, 557 | Group{ 558 | Tag: TagJobGroup, 559 | Attrs: Attributes{ 560 | MakeAttr("copies", TagInteger, Integer(1)), 561 | }, 562 | }, 563 | }, 564 | } 565 | 566 | // Check request formatting 567 | reqExpected := []string{ 568 | `{`, 569 | ` REQUEST-ID 1`, 570 | ` VERSION 2.0`, 571 | ` OPERATION Print-Job`, 572 | ``, 573 | ` GROUP operation-attributes-tag`, 574 | ` ATTR "attributes-charset" charset: utf-8`, 575 | ` ATTR "attributes-natural-language" naturalLanguage: en-US`, 576 | ` ATTR "printer-uri" uri: ipp://192/168.0.1/ipp/print`, 577 | ``, 578 | ` GROUP job-attributes-tag`, 579 | ` ATTR "copies" integer: 1`, 580 | `}`, 581 | } 582 | 583 | var buf bytes.Buffer 584 | m.Print(&buf, true) 585 | exp := strings.Join(reqExpected, "\n") + "\n" 586 | 587 | if buf.String() != exp { 588 | t.Errorf("Message.Print test failed for request:\n"+ 589 | "expected: %s\n"+ 590 | "present: %s\n", 591 | exp, &buf, 592 | ) 593 | } 594 | 595 | // Check response formatting 596 | rspExpected := []string{ 597 | `{`, 598 | ` REQUEST-ID 1`, 599 | ` VERSION 2.0`, 600 | ` STATUS successful-ok`, 601 | ``, 602 | ` GROUP operation-attributes-tag`, 603 | ` ATTR "attributes-charset" charset: utf-8`, 604 | ` ATTR "attributes-natural-language" naturalLanguage: en-US`, 605 | ` ATTR "printer-uri" uri: ipp://192/168.0.1/ipp/print`, 606 | ``, 607 | ` GROUP job-attributes-tag`, 608 | ` ATTR "copies" integer: 1`, 609 | `}`, 610 | } 611 | 612 | buf.Reset() 613 | m.Code = 0 614 | m.Print(&buf, false) 615 | exp = strings.Join(rspExpected, "\n") + "\n" 616 | 617 | if buf.String() != exp { 618 | t.Errorf("Message.Print test failed for response:\n"+ 619 | "expected: %s\n"+ 620 | "present: %s\n", 621 | exp, &buf, 622 | ) 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /op.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Operation Codes 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | // Op represents an IPP Operation Code 16 | type Op Code 17 | 18 | // Op codes 19 | const ( 20 | OpPrintJob Op = 0x0002 // Print-Job: Print a single file 21 | OpPrintURI Op = 0x0003 // Print-URI: Print a single URL 22 | OpValidateJob Op = 0x0004 // Validate-Job: Validate job values prior to submission 23 | OpCreateJob Op = 0x0005 // Create-Job: Create an empty print job 24 | OpSendDocument Op = 0x0006 // Send-Document: Add a file to a job 25 | OpSendURI Op = 0x0007 // Send-URI: Add a URL to a job 26 | OpCancelJob Op = 0x0008 // Cancel-Job: Cancel a job 27 | OpGetJobAttributes Op = 0x0009 // Get-Job-Attribute: Get information about a job 28 | OpGetJobs Op = 0x000a // Get-Jobs: Get a list of jobs 29 | OpGetPrinterAttributes Op = 0x000b // Get-Printer-Attributes: Get information about a printer 30 | OpHoldJob Op = 0x000c // Hold-Job: Hold a job for printing 31 | OpReleaseJob Op = 0x000d // Release-Job: Release a job for printing 32 | OpRestartJob Op = 0x000e // Restart-Job: Reprint a job 33 | 34 | OpPausePrinter Op = 0x0010 // Pause-Printer: Stop a printer 35 | OpResumePrinter Op = 0x0011 // Resume-Printer: Start a printer 36 | OpPurgeJobs Op = 0x0012 // Purge-Jobs: Delete all jobs 37 | OpSetPrinterAttributes Op = 0x0013 // Set-Printer-Attributes: Set printer values 38 | OpSetJobAttributes Op = 0x0014 // Set-Job-Attributes: Set job values 39 | OpGetPrinterSupportedValues Op = 0x0015 // Get-Printer-Supported-Values: Get supported values 40 | OpCreatePrinterSubscriptions Op = 0x0016 // Create-Printer-Subscriptions: Create one or more printer subscriptions 41 | OpCreateJobSubscriptions Op = 0x0017 // Create-Job-Subscriptions: Create one of more job subscriptions 42 | OpGetSubscriptionAttributes Op = 0x0018 // Get-Subscription-Attributes: Get subscription information 43 | OpGetSubscriptions Op = 0x0019 // Get-Subscriptions: Get list of subscriptions 44 | OpRenewSubscription Op = 0x001a // Renew-Subscription: Renew a printer subscription 45 | OpCancelSubscription Op = 0x001b // Cancel-Subscription: Cancel a subscription 46 | OpGetNotifications Op = 0x001c // Get-Notifications: Get notification events 47 | OpSendNotifications Op = 0x001d // Send-Notifications: Send notification events 48 | OpGetResourceAttributes Op = 0x001e // Get-Resource-Attributes: Get resource information 49 | OpGetResourceData Op = 0x001f // Get-Resource-Data: Get resource data 50 | 51 | OpGetResources Op = 0x0020 // Get-Resources: Get list of resources 52 | OpGetPrintSupportFiles Op = 0x0021 // Get-Printer-Support-Files: Get printer support files 53 | OpEnablePrinter Op = 0x0022 // Enable-Printer: Accept new jobs for a printer 54 | OpDisablePrinter Op = 0x0023 // Disable-Printer: Reject new jobs for a printer 55 | OpPausePrinterAfterCurrentJob Op = 0x0024 // Pause-Printer-After-Current-Job: Stop printer after the current job 56 | OpHoldNewJobs Op = 0x0025 // Hold-New-Jobs: Hold new jobs 57 | OpReleaseHeldNewJobs Op = 0x0026 // Release-Held-New-Jobs: Release new jobs that were previously held 58 | OpDeactivatePrinter Op = 0x0027 // Deactivate-Printer: Stop a printer and do not accept jobs 59 | OpActivatePrinter Op = 0x0028 // Activate-Printer: Start a printer and accept jobs 60 | OpRestartPrinter Op = 0x0029 // Restart-Printer: Restart a printer 61 | OpShutdownPrinter Op = 0x002a // Shutdown-Printer: Turn a printer off 62 | OpStartupPrinter Op = 0x002b // Startup-Printer: Turn a printer on 63 | OpReprocessJob Op = 0x002c // Reprocess-Job: Reprint a job 64 | OpCancelCurrentJob Op = 0x002d // Cancel-Current-Job: Cancel the current job 65 | OpSuspendCurrentJob Op = 0x002e // Suspend-Current-Job: Suspend the current job 66 | OpResumeJob Op = 0x002f // Resume-Job: Resume the current job 67 | 68 | OpPromoteJob Op = 0x0030 // Promote-Job: Promote a job to print sooner 69 | OpScheduleJobAfter Op = 0x0031 // Schedule-Job-After: Schedule a job to print after another 70 | OpCancelDocument Op = 0x0033 // Cancel-Document: Cancel a document 71 | OpGetDocumentAttributes Op = 0x0034 // Get-Document-Attributes: Get document information 72 | OpGetDocuments Op = 0x0035 // Get-Documents: Get a list of documents in a job 73 | OpDeleteDocument Op = 0x0036 // Delete-Document: Delete a document 74 | OpSetDocumentAttributes Op = 0x0037 // Set-Document-Attributes: Set document values 75 | OpCancelJobs Op = 0x0038 // Cancel-Jobs: Cancel all jobs (administrative) 76 | OpCancelMyJobs Op = 0x0039 // Cancel-My-Jobs: Cancel a user's jobs 77 | OpResubmitJob Op = 0x003a // Resubmit-Job: Copy and reprint a job 78 | OpCloseJob Op = 0x003b // Close-Job: Close a job and start printing 79 | OpIdentifyPrinter Op = 0x003c // Identify-Printer: Make the printer beep, flash, or display a message for identification 80 | OpValidateDocument Op = 0x003d // Validate-Document: Validate document values prior to submission 81 | OpAddDocumentImages Op = 0x003e // Add-Document-Images: Add image(s) from the specified scanner source 82 | OpAcknowledgeDocument Op = 0x003f // Acknowledge-Document: Acknowledge processing of a document 83 | 84 | OpAcknowledgeIdentifyPrinter Op = 0x0040 // Acknowledge-Identify-Printer: Acknowledge action on an Identify-Printer request 85 | OpAcknowledgeJob Op = 0x0041 // Acknowledge-Job: Acknowledge processing of a job 86 | OpFetchDocument Op = 0x0042 // Fetch-Document: Fetch a document for processing 87 | OpFetchJob Op = 0x0043 // Fetch-Job: Fetch a job for processing 88 | OpGetOutputDeviceAttributes Op = 0x0044 // Get-Output-Device-Attributes: Get printer information for a specific output device 89 | OpUpdateActiveJobs Op = 0x0045 // Update-Active-Jobs: Update the list of active jobs that a proxy has processed 90 | OpDeregisterOutputDevice Op = 0x0046 // Deregister-Output-Device: Remove an output device 91 | OpUpdateDocumentStatus Op = 0x0047 // Update-Document-Status: Update document values 92 | OpUpdateJobStatus Op = 0x0048 // Update-Job-Status: Update job values 93 | OpupdateOutputDeviceAttributes Op = 0x0049 // Update-Output-Device-Attributes: Update output device values 94 | OpGetNextDocumentData Op = 0x004a // Get-Next-Document-Data: Scan more document data 95 | OpAllocatePrinterResources Op = 0x004b // Allocate-Printer-Resources: Use resources for a printer 96 | OpCreatePrinter Op = 0x004c // Create-Printer: Create a new service 97 | OpDeallocatePrinterResources Op = 0x004d // Deallocate-Printer-Resources: Stop using resources for a printer 98 | OpDeletePrinter Op = 0x004e // Delete-Printer: Delete an existing service 99 | OpGetPrinters Op = 0x004f // Get-Printers: Get a list of services 100 | 101 | OpShutdownOnePrinter Op = 0x0050 // Shutdown-One-Printer: Shutdown a service 102 | OpStartupOnePrinter Op = 0x0051 // Startup-One-Printer: Start a service 103 | OpCancelResource Op = 0x0052 // Cancel-Resource: Uninstall a resource 104 | OpCreateResource Op = 0x0053 // Create-Resource: Create a new (empty) resource 105 | OpInstallResource Op = 0x0054 // Install-Resource: Install a resource 106 | OpSendResourceData Op = 0x0055 // Send-Resource-Data: Upload the data for a resource 107 | OpSetResourceAttributes Op = 0x0056 // Set-Resource-Attributes: Set resource object attributes 108 | OpCreateResourceSubscriptions Op = 0x0057 // Create-Resource-Subscriptions: Create event subscriptions for a resource 109 | OpCreateSystemSubscriptions Op = 0x0058 // Create-System-Subscriptions: Create event subscriptions for a system 110 | OpDisableAllPrinters Op = 0x0059 // Disable-All-Printers: Stop accepting new jobs on all services 111 | OpEnableAllPrinters Op = 0x005a // Enable-All-Printers: Start accepting new jobs on all services 112 | OpGetSystemAttributes Op = 0x005b // Get-System-Attributes: Get system object attributes 113 | OpGetSystemSupportedValues Op = 0x005c // Get-System-Supported-Values: Get supported values for system object attributes 114 | OpPauseAllPrinters Op = 0x005d // Pause-All-Printers: Stop all services immediately 115 | OpPauseAllPrintersAfterCurrentJob Op = 0x005e // Pause-All-Printers-After-Current-Job: Stop all services after processing the current jobs 116 | OpRegisterOutputDevice Op = 0x005f // Register-Output-Device: Register a remote service 117 | 118 | OpRestartSystem Op = 0x0060 // Restart-System: Restart all services 119 | OpResumeAllPrinters Op = 0x0061 // Resume-All-Printers: Start job processing on all services 120 | OpSetSystemAttributes Op = 0x0062 // Set-System-Attributes: Set system object attributes 121 | OpShutdownAllPrinters Op = 0x0063 // Shutdown-All-Printers: Shutdown all services 122 | OpStartupAllPrinters Op = 0x0064 // Startup-All-Printers: Startup all services 123 | 124 | OpCupsGetDefault Op = 0x4001 // CUPS-Get-Default: Get the default printer 125 | OpCupsGetPrinters Op = 0x4002 // CUPS-Get-Printers: Get a list of printers and/or classes 126 | OpCupsAddModifyPrinter Op = 0x4003 // CUPS-Add-Modify-Printer: Add or modify a printer 127 | OpCupsDeletePrinter Op = 0x4004 // CUPS-Delete-Printer: Delete a printer 128 | OpCupsGetClasses Op = 0x4005 // CUPS-Get-Classes: Get a list of classes 129 | OpCupsAddModifyClass Op = 0x4006 // CUPS-Add-Modify-Class: Add or modify a class 130 | OpCupsDeleteClass Op = 0x4007 // CUPS-Delete-Class: Delete a class 131 | OpCupsAcceptJobs Op = 0x4008 // CUPS-Accept-Jobs: Accept new jobs on a printer 132 | OpCupsRejectJobs Op = 0x4009 // CUPS-Reject-Jobs: Reject new jobs on a printer 133 | OpCupsSetDefault Op = 0x400a // CUPS-Set-Default: Set the default printer 134 | OpCupsGetDevices Op = 0x400b // CUPS-Get-Devices: Get a list of supported devices 135 | OpCupsGetPpds Op = 0x400c // CUPS-Get-PPDs: Get a list of supported drivers 136 | OpCupsMoveJob Op = 0x400d // CUPS-Move-Job: Move a job to a different printer 137 | OpCupsAuthenticateJob Op = 0x400e // CUPS-Authenticate-Job: Authenticate a job 138 | OpCupsGetPpd Op = 0x400f // CUPS-Get-PPD: Get a PPD file 139 | 140 | OpCupsGetDocument Op = 0x4027 // CUPS-Get-Document: Get a document file 141 | OpCupsCreateLocalPrinter Op = 0x4028 // CUPS-Create-Local-Printer: Create a local (temporary) printer 142 | 143 | ) 144 | 145 | // String() returns a Status name, as defined by RFC 8010 146 | func (op Op) String() string { 147 | if s := opNames[op]; s != "" { 148 | return s 149 | } 150 | 151 | return fmt.Sprintf("0x%4.4x", int(op)) 152 | } 153 | 154 | var opNames = map[Op]string{ 155 | OpPrintJob: "Print-Job", 156 | OpPrintURI: "Print-URI", 157 | OpValidateJob: "Validate-Job", 158 | OpCreateJob: "Create-Job", 159 | OpSendDocument: "Send-Document", 160 | OpSendURI: "Send-URI", 161 | OpCancelJob: "Cancel-Job", 162 | OpGetJobAttributes: "Get-Job-Attribute", 163 | OpGetJobs: "Get-Jobs", 164 | OpGetPrinterAttributes: "Get-Printer-Attributes", 165 | OpHoldJob: "Hold-Job", 166 | OpReleaseJob: "Release-Job", 167 | OpRestartJob: "Restart-Job", 168 | OpPausePrinter: "Pause-Printer", 169 | OpResumePrinter: "Resume-Printer", 170 | OpPurgeJobs: "Purge-Jobs", 171 | OpSetPrinterAttributes: "Set-Printer-Attributes", 172 | OpSetJobAttributes: "Set-Job-Attributes", 173 | OpGetPrinterSupportedValues: "Get-Printer-Supported-Values", 174 | OpCreatePrinterSubscriptions: "Create-Printer-Subscriptions", 175 | OpCreateJobSubscriptions: "Create-Job-Subscriptions", 176 | OpGetSubscriptionAttributes: "Get-Subscription-Attributes", 177 | OpGetSubscriptions: "Get-Subscriptions", 178 | OpRenewSubscription: "Renew-Subscription", 179 | OpCancelSubscription: "Cancel-Subscription", 180 | OpGetNotifications: "Get-Notifications", 181 | OpSendNotifications: "Send-Notifications", 182 | OpGetResourceAttributes: "Get-Resource-Attributes", 183 | OpGetResourceData: "Get-Resource-Data", 184 | OpGetResources: "Get-Resources", 185 | OpGetPrintSupportFiles: "Get-Printer-Support-Files", 186 | OpEnablePrinter: "Enable-Printer", 187 | OpDisablePrinter: "Disable-Printer", 188 | OpPausePrinterAfterCurrentJob: "Pause-Printer-After-Current-Job", 189 | OpHoldNewJobs: "Hold-New-Jobs", 190 | OpReleaseHeldNewJobs: "Release-Held-New-Jobs", 191 | OpDeactivatePrinter: "Deactivate-Printer", 192 | OpActivatePrinter: "Activate-Printer", 193 | OpRestartPrinter: "Restart-Printer", 194 | OpShutdownPrinter: "Shutdown-Printer", 195 | OpStartupPrinter: "Startup-Printer", 196 | OpReprocessJob: "Reprocess-Job", 197 | OpCancelCurrentJob: "Cancel-Current-Job", 198 | OpSuspendCurrentJob: "Suspend-Current-Job", 199 | OpResumeJob: "Resume-Job", 200 | OpPromoteJob: "Promote-Job", 201 | OpScheduleJobAfter: "Schedule-Job-After", 202 | OpCancelDocument: "Cancel-Document", 203 | OpGetDocumentAttributes: "Get-Document-Attributes", 204 | OpGetDocuments: "Get-Documents", 205 | OpDeleteDocument: "Delete-Document", 206 | OpSetDocumentAttributes: "Set-Document-Attributes", 207 | OpCancelJobs: "Cancel-Jobs", 208 | OpCancelMyJobs: "Cancel-My-Jobs", 209 | OpResubmitJob: "Resubmit-Job", 210 | OpCloseJob: "Close-Job", 211 | OpIdentifyPrinter: "Identify-Printer", 212 | OpValidateDocument: "Validate-Document", 213 | OpAddDocumentImages: "Add-Document-Images", 214 | OpAcknowledgeDocument: "Acknowledge-Document", 215 | OpAcknowledgeIdentifyPrinter: "Acknowledge-Identify-Printer", 216 | OpAcknowledgeJob: "Acknowledge-Job", 217 | OpFetchDocument: "Fetch-Document", 218 | OpFetchJob: "Fetch-Job", 219 | OpGetOutputDeviceAttributes: "Get-Output-Device-Attributes", 220 | OpUpdateActiveJobs: "Update-Active-Jobs", 221 | OpDeregisterOutputDevice: "Deregister-Output-Device", 222 | OpUpdateDocumentStatus: "Update-Document-Status", 223 | OpUpdateJobStatus: "Update-Job-Status", 224 | OpupdateOutputDeviceAttributes: "Update-Output-Device-Attributes", 225 | OpGetNextDocumentData: "Get-Next-Document-Data", 226 | OpAllocatePrinterResources: "Allocate-Printer-Resources", 227 | OpCreatePrinter: "Create-Printer", 228 | OpDeallocatePrinterResources: "Deallocate-Printer-Resources", 229 | OpDeletePrinter: "Delete-Printer", 230 | OpGetPrinters: "Get-Printers", 231 | OpShutdownOnePrinter: "Shutdown-One-Printer", 232 | OpStartupOnePrinter: "Startup-One-Printer", 233 | OpCancelResource: "Cancel-Resource", 234 | OpCreateResource: "Create-Resource", 235 | OpInstallResource: "Install-Resource", 236 | OpSendResourceData: "Send-Resource-Data", 237 | OpSetResourceAttributes: "Set-Resource-Attributes", 238 | OpCreateResourceSubscriptions: "Create-Resource-Subscriptions", 239 | OpCreateSystemSubscriptions: "Create-System-Subscriptions", 240 | OpDisableAllPrinters: "Disable-All-Printers", 241 | OpEnableAllPrinters: "Enable-All-Printers", 242 | OpGetSystemAttributes: "Get-System-Attributes", 243 | OpGetSystemSupportedValues: "Get-System-Supported-Values", 244 | OpPauseAllPrinters: "Pause-All-Printers", 245 | OpPauseAllPrintersAfterCurrentJob: "Pause-All-Printers-After-Current-Job", 246 | OpRegisterOutputDevice: "Register-Output-Device", 247 | OpRestartSystem: "Restart-System", 248 | OpResumeAllPrinters: "Resume-All-Printers", 249 | OpSetSystemAttributes: "Set-System-Attributes", 250 | OpShutdownAllPrinters: "Shutdown-All-Printers", 251 | OpStartupAllPrinters: "Startup-All-Printers", 252 | OpCupsGetDefault: "CUPS-Get-Default", 253 | OpCupsGetPrinters: "CUPS-Get-Printers", 254 | OpCupsAddModifyPrinter: "CUPS-Add-Modify-Printer", 255 | OpCupsDeletePrinter: "CUPS-Delete-Printer", 256 | OpCupsGetClasses: "CUPS-Get-Classes", 257 | OpCupsAddModifyClass: "CUPS-Add-Modify-Class", 258 | OpCupsDeleteClass: "CUPS-Delete-Class", 259 | OpCupsAcceptJobs: "CUPS-Accept-Jobs", 260 | OpCupsRejectJobs: "CUPS-Reject-Jobs", 261 | OpCupsSetDefault: "CUPS-Set-Default", 262 | OpCupsGetDevices: "CUPS-Get-Devices", 263 | OpCupsGetPpds: "CUPS-Get-PPDs", 264 | OpCupsMoveJob: "CUPS-Move-Job", 265 | OpCupsAuthenticateJob: "CUPS-Authenticate-Job", 266 | OpCupsGetPpd: "CUPS-Get-PPD", 267 | OpCupsGetDocument: "CUPS-Get-Document", 268 | OpCupsCreateLocalPrinter: "CUPS-Create-Local-Printer", 269 | } 270 | -------------------------------------------------------------------------------- /op_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Operation Codes tests 7 | */ 8 | 9 | package goipp 10 | 11 | import "testing" 12 | 13 | // TestOpString tests Op.String method 14 | func TestOpString(t *testing.T) { 15 | type testData struct { 16 | op Op // Input Op code 17 | s string // Expected output string 18 | } 19 | 20 | tests := []testData{ 21 | {OpPrintJob, "Print-Job"}, 22 | {OpPrintURI, "Print-URI"}, 23 | {OpPausePrinter, "Pause-Printer"}, 24 | {OpRestartSystem, "Restart-System"}, 25 | {OpCupsGetDefault, "CUPS-Get-Default"}, 26 | {OpCupsGetPpd, "CUPS-Get-PPD"}, 27 | {OpCupsCreateLocalPrinter, "CUPS-Create-Local-Printer"}, 28 | {0xabcd, "0xabcd"}, 29 | } 30 | 31 | for _, test := range tests { 32 | s := test.op.String() 33 | if s != test.s { 34 | t.Errorf("testing Op.String:\n"+ 35 | "input: 0x%4.4x\n"+ 36 | "expected: %s\n"+ 37 | "present: %s\n", 38 | int(test.op), test.s, s, 39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Status Codes 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | // Status represents an IPP Status Code 16 | type Status Code 17 | 18 | // Status codes 19 | const ( 20 | StatusOk Status = 0x0000 // successful-ok 21 | StatusOkIgnoredOrSubstituted Status = 0x0001 // successful-ok-ignored-or-substituted-attributes 22 | StatusOkConflicting Status = 0x0002 // successful-ok-conflicting-attributes 23 | StatusOkIgnoredSubscriptions Status = 0x0003 // successful-ok-ignored-subscriptions 24 | StatusOkIgnoredNotifications Status = 0x0004 // successful-ok-ignored-notifications 25 | StatusOkTooManyEvents Status = 0x0005 // successful-ok-too-many-events 26 | StatusOkButCancelSubscription Status = 0x0006 // successful-ok-but-cancel-subscription 27 | StatusOkEventsComplete Status = 0x0007 // successful-ok-events-complete 28 | StatusRedirectionOtherSite Status = 0x0200 // redirection-other-site 29 | StatusCupsSeeOther Status = 0x0280 // cups-see-other 30 | StatusErrorBadRequest Status = 0x0400 // client-error-bad-request 31 | StatusErrorForbidden Status = 0x0401 // client-error-forbidden 32 | StatusErrorNotAuthenticated Status = 0x0402 // client-error-not-authenticated 33 | StatusErrorNotAuthorized Status = 0x0403 // client-error-not-authorized 34 | StatusErrorNotPossible Status = 0x0404 // client-error-not-possible 35 | StatusErrorTimeout Status = 0x0405 // client-error-timeout 36 | StatusErrorNotFound Status = 0x0406 // client-error-not-found 37 | StatusErrorGone Status = 0x0407 // client-error-gone 38 | StatusErrorRequestEntity Status = 0x0408 // client-error-request-entity-too-large 39 | StatusErrorRequestValue Status = 0x0409 // client-error-request-value-too-long 40 | StatusErrorDocumentFormatNotSupported Status = 0x040a // client-error-document-format-not-supported 41 | StatusErrorAttributesOrValues Status = 0x040b // client-error-attributes-or-values-not-supported 42 | StatusErrorURIScheme Status = 0x040c // client-error-uri-scheme-not-supported 43 | StatusErrorCharset Status = 0x040d // client-error-charset-not-supported 44 | StatusErrorConflicting Status = 0x040e // client-error-conflicting-attributes 45 | StatusErrorCompressionNotSupported Status = 0x040f // client-error-compression-not-supported 46 | StatusErrorCompressionError Status = 0x0410 // client-error-compression-error 47 | StatusErrorDocumentFormatError Status = 0x0411 // client-error-document-format-error 48 | StatusErrorDocumentAccess Status = 0x0412 // client-error-document-access-error 49 | StatusErrorAttributesNotSettable Status = 0x0413 // client-error-attributes-not-settable 50 | StatusErrorIgnoredAllSubscriptions Status = 0x0414 // client-error-ignored-all-subscriptions 51 | StatusErrorTooManySubscriptions Status = 0x0415 // client-error-too-many-subscriptions 52 | StatusErrorIgnoredAllNotifications Status = 0x0416 // client-error-ignored-all-notifications 53 | StatusErrorPrintSupportFileNotFound Status = 0x0417 // client-error-print-support-file-not-found 54 | StatusErrorDocumentPassword Status = 0x0418 // client-error-document-password-error 55 | StatusErrorDocumentPermission Status = 0x0419 // client-error-document-permission-error 56 | StatusErrorDocumentSecurity Status = 0x041a // client-error-document-security-error 57 | StatusErrorDocumentUnprintable Status = 0x041b // client-error-document-unprintable-error 58 | StatusErrorAccountInfoNeeded Status = 0x041c // client-error-account-info-needed 59 | StatusErrorAccountClosed Status = 0x041d // client-error-account-closed 60 | StatusErrorAccountLimitReached Status = 0x041e // client-error-account-limit-reached 61 | StatusErrorAccountAuthorizationFailed Status = 0x041f // client-error-account-authorization-failed 62 | StatusErrorNotFetchable Status = 0x0420 // client-error-not-fetchable 63 | StatusErrorInternal Status = 0x0500 // server-error-internal-error 64 | StatusErrorOperationNotSupported Status = 0x0501 // server-error-operation-not-supported 65 | StatusErrorServiceUnavailable Status = 0x0502 // server-error-service-unavailable 66 | StatusErrorVersionNotSupported Status = 0x0503 // server-error-version-not-supported 67 | StatusErrorDevice Status = 0x0504 // server-error-device-error 68 | StatusErrorTemporary Status = 0x0505 // server-error-temporary-error 69 | StatusErrorNotAcceptingJobs Status = 0x0506 // server-error-not-accepting-jobs 70 | StatusErrorBusy Status = 0x0507 // server-error-busy 71 | StatusErrorJobCanceled Status = 0x0508 // server-error-job-canceled 72 | StatusErrorMultipleJobsNotSupported Status = 0x0509 // server-error-multiple-document-jobs-not-supported 73 | StatusErrorPrinterIsDeactivated Status = 0x050a // server-error-printer-is-deactivated 74 | StatusErrorTooManyJobs Status = 0x050b // server-error-too-many-jobs 75 | StatusErrorTooManyDocuments Status = 0x050c // server-error-too-many-documents 76 | ) 77 | 78 | // String() returns a Status name, as defined by RFC 8010 79 | func (status Status) String() string { 80 | if s := statusNames[status]; s != "" { 81 | return s 82 | } 83 | 84 | return fmt.Sprintf("0x%4.4x", int(status)) 85 | } 86 | 87 | var statusNames = map[Status]string{ 88 | StatusOk: "successful-ok", 89 | StatusOkIgnoredOrSubstituted: "successful-ok-ignored-or-substituted-attributes", 90 | StatusOkConflicting: "successful-ok-conflicting-attributes", 91 | StatusOkIgnoredSubscriptions: "successful-ok-ignored-subscriptions", 92 | StatusOkIgnoredNotifications: "successful-ok-ignored-notifications", 93 | StatusOkTooManyEvents: "successful-ok-too-many-events", 94 | StatusOkButCancelSubscription: "successful-ok-but-cancel-subscription", 95 | StatusOkEventsComplete: "successful-ok-events-complete", 96 | StatusRedirectionOtherSite: "redirection-other-site", 97 | StatusCupsSeeOther: "cups-see-other", 98 | StatusErrorBadRequest: "client-error-bad-request", 99 | StatusErrorForbidden: "client-error-forbidden", 100 | StatusErrorNotAuthenticated: "client-error-not-authenticated", 101 | StatusErrorNotAuthorized: "client-error-not-authorized", 102 | StatusErrorNotPossible: "client-error-not-possible", 103 | StatusErrorTimeout: "client-error-timeout", 104 | StatusErrorNotFound: "client-error-not-found", 105 | StatusErrorGone: "client-error-gone", 106 | StatusErrorRequestEntity: "client-error-request-entity-too-large", 107 | StatusErrorRequestValue: "client-error-request-value-too-long", 108 | StatusErrorDocumentFormatNotSupported: "client-error-document-format-not-supported", 109 | StatusErrorAttributesOrValues: "client-error-attributes-or-values-not-supported", 110 | StatusErrorURIScheme: "client-error-uri-scheme-not-supported", 111 | StatusErrorCharset: "client-error-charset-not-supported", 112 | StatusErrorConflicting: "client-error-conflicting-attributes", 113 | StatusErrorCompressionNotSupported: "client-error-compression-not-supported", 114 | StatusErrorCompressionError: "client-error-compression-error", 115 | StatusErrorDocumentFormatError: "client-error-document-format-error", 116 | StatusErrorDocumentAccess: "client-error-document-access-error", 117 | StatusErrorAttributesNotSettable: "client-error-attributes-not-settable", 118 | StatusErrorIgnoredAllSubscriptions: "client-error-ignored-all-subscriptions", 119 | StatusErrorTooManySubscriptions: "client-error-too-many-subscriptions", 120 | StatusErrorIgnoredAllNotifications: "client-error-ignored-all-notifications", 121 | StatusErrorPrintSupportFileNotFound: "client-error-print-support-file-not-found", 122 | StatusErrorDocumentPassword: "client-error-document-password-error", 123 | StatusErrorDocumentPermission: "client-error-document-permission-error", 124 | StatusErrorDocumentSecurity: "client-error-document-security-error", 125 | StatusErrorDocumentUnprintable: "client-error-document-unprintable-error", 126 | StatusErrorAccountInfoNeeded: "client-error-account-info-needed", 127 | StatusErrorAccountClosed: "client-error-account-closed", 128 | StatusErrorAccountLimitReached: "client-error-account-limit-reached", 129 | StatusErrorAccountAuthorizationFailed: "client-error-account-authorization-failed", 130 | StatusErrorNotFetchable: "client-error-not-fetchable", 131 | StatusErrorInternal: "server-error-internal-error", 132 | StatusErrorOperationNotSupported: "server-error-operation-not-supported", 133 | StatusErrorServiceUnavailable: "server-error-service-unavailable", 134 | StatusErrorVersionNotSupported: "server-error-version-not-supported", 135 | StatusErrorDevice: "server-error-device-error", 136 | StatusErrorTemporary: "server-error-temporary-error", 137 | StatusErrorNotAcceptingJobs: "server-error-not-accepting-jobs", 138 | StatusErrorBusy: "server-error-busy", 139 | StatusErrorJobCanceled: "server-error-job-canceled", 140 | StatusErrorMultipleJobsNotSupported: "server-error-multiple-document-jobs-not-supported", 141 | StatusErrorPrinterIsDeactivated: "server-error-printer-is-deactivated", 142 | StatusErrorTooManyJobs: "server-error-too-many-jobs", 143 | StatusErrorTooManyDocuments: "server-error-too-many-documents", 144 | } 145 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Status Codes tests 7 | */ 8 | 9 | package goipp 10 | 11 | import "testing" 12 | 13 | // TestStatusString tests Status.String method 14 | func TestStatusString(t *testing.T) { 15 | type testData struct { 16 | status Status // Input Op code 17 | s string // Expected output string 18 | } 19 | 20 | tests := []testData{ 21 | {StatusOk, "successful-ok"}, 22 | {StatusOkConflicting, "successful-ok-conflicting-attributes"}, 23 | {StatusOkEventsComplete, "successful-ok-events-complete"}, 24 | {StatusRedirectionOtherSite, "redirection-other-site"}, 25 | {StatusErrorBadRequest, "client-error-bad-request"}, 26 | {StatusErrorForbidden, "client-error-forbidden"}, 27 | {StatusErrorNotFetchable, "client-error-not-fetchable"}, 28 | {StatusErrorInternal, "server-error-internal-error"}, 29 | {StatusErrorTooManyDocuments, "server-error-too-many-documents"}, 30 | {0xabcd, "0xabcd"}, 31 | } 32 | 33 | for _, test := range tests { 34 | s := test.status.String() 35 | if s != test.s { 36 | t.Errorf("testing Status.String:\n"+ 37 | "input: 0x%4.4x\n"+ 38 | "expected: %s\n"+ 39 | "present: %s\n", 40 | int(test.status), test.s, s, 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Tags 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | // Tag represents a tag used in the binary representation of an IPP message. 16 | // 17 | // Normally, a Tag is a single-byte value ranging from 0x00 to 0xff. 18 | // 19 | // However, IPP also provides an extension tag mechanism that supports 20 | // 32-bit tags. When using this mechanism, the tag is set to [TagExtension], 21 | // and the actual 32-bit tag value is encoded as a big-endian integer 22 | // in the attribute value. 23 | // 24 | // This mechanism is described in RFC 8010, Section 3.5.2. 25 | // 26 | // To send an [Attribute] with an extended tag value: 27 | // 28 | // - Set the value tag to TagExtension 29 | // - Use [Binary] to represent the Attribute value 30 | // - Encode the extended tag value as a big-endian integer in the first 31 | // 4 bytes of the attribute value 32 | // - Note that the extended tag value must be within the allowed range 33 | // (0x00000000 to 0x7fffffff, inclusive) 34 | // 35 | // The goipp library enforces these rules during message encoding and decoding. 36 | // 37 | // Note: This API has changed since version 1.1.0 of this library. 38 | // Previously, the library automatically converted tags with values exceeding 39 | // single-byte range into extended tag representation. However, this caused 40 | // issues during reception because automatically converted tags with extended 41 | // representation format but smaller values became indistinguishable from 42 | // normal tags with the same value, despite requiring different handling. 43 | type Tag int 44 | 45 | // Tag values 46 | const ( 47 | // Delimiter tags 48 | TagZero Tag = 0x00 // Zero tag - used for separators 49 | TagOperationGroup Tag = 0x01 // Operation group 50 | TagJobGroup Tag = 0x02 // Job group 51 | TagEnd Tag = 0x03 // End-of-attributes 52 | TagPrinterGroup Tag = 0x04 // Printer group 53 | TagUnsupportedGroup Tag = 0x05 // Unsupported attributes group 54 | TagSubscriptionGroup Tag = 0x06 // Subscription group 55 | TagEventNotificationGroup Tag = 0x07 // Event group 56 | TagResourceGroup Tag = 0x08 // Resource group 57 | TagDocumentGroup Tag = 0x09 // Document group 58 | TagSystemGroup Tag = 0x0a // System group 59 | TagFuture11Group Tag = 0x0b // Future group 11 60 | TagFuture12Group Tag = 0x0c // Future group 12 61 | TagFuture13Group Tag = 0x0d // Future group 13 62 | TagFuture14Group Tag = 0x0e // Future group 14 63 | TagFuture15Group Tag = 0x0f // Future group 15 64 | 65 | // Value tags 66 | TagUnsupportedValue Tag = 0x10 // Unsupported value 67 | TagDefault Tag = 0x11 // Default value 68 | TagUnknown Tag = 0x12 // Unknown value 69 | TagNoValue Tag = 0x13 // No-value value 70 | TagNotSettable Tag = 0x15 // Not-settable value 71 | TagDeleteAttr Tag = 0x16 // Delete-attribute value 72 | TagAdminDefine Tag = 0x17 // Admin-defined value 73 | TagInteger Tag = 0x21 // Integer value 74 | TagBoolean Tag = 0x22 // Boolean value 75 | TagEnum Tag = 0x23 // Enumeration value 76 | TagString Tag = 0x30 // Octet string value 77 | TagDateTime Tag = 0x31 // Date/time value 78 | TagResolution Tag = 0x32 // Resolution value 79 | TagRange Tag = 0x33 // Range value 80 | TagBeginCollection Tag = 0x34 // Beginning of collection value 81 | TagTextLang Tag = 0x35 // Text-with-language value 82 | TagNameLang Tag = 0x36 // Name-with-language value 83 | TagEndCollection Tag = 0x37 // End of collection value 84 | TagText Tag = 0x41 // Text value 85 | TagName Tag = 0x42 // Name value 86 | TagReservedString Tag = 0x43 // Reserved for future string value 87 | TagKeyword Tag = 0x44 // Keyword value 88 | TagURI Tag = 0x45 // URI value 89 | TagURIScheme Tag = 0x46 // URI scheme value 90 | TagCharset Tag = 0x47 // Character set value 91 | TagLanguage Tag = 0x48 // Language value 92 | TagMimeType Tag = 0x49 // MIME media type value 93 | TagMemberName Tag = 0x4a // Collection member name value 94 | TagExtension Tag = 0x7f // Extension point for 32-bit tags 95 | ) 96 | 97 | // IsDelimiter returns true for delimiter tags 98 | func (tag Tag) IsDelimiter() bool { 99 | return uint(tag) < 0x10 100 | } 101 | 102 | // IsGroup returns true for group tags 103 | func (tag Tag) IsGroup() bool { 104 | return tag.IsDelimiter() && tag != TagZero && tag != TagEnd 105 | } 106 | 107 | // Type returns Type of Value that corresponds to the tag 108 | func (tag Tag) Type() Type { 109 | if tag.IsDelimiter() { 110 | return TypeInvalid 111 | } 112 | 113 | switch tag { 114 | case TagInteger, TagEnum: 115 | return TypeInteger 116 | 117 | case TagBoolean: 118 | return TypeBoolean 119 | 120 | case TagUnsupportedValue, TagDefault, TagUnknown, TagNotSettable, 121 | TagNoValue, TagDeleteAttr, TagAdminDefine: 122 | // These tags not expected to have value 123 | return TypeVoid 124 | 125 | case TagText, TagName, TagReservedString, TagKeyword, TagURI, TagURIScheme, 126 | TagCharset, TagLanguage, TagMimeType, TagMemberName: 127 | return TypeString 128 | 129 | case TagDateTime: 130 | return TypeDateTime 131 | 132 | case TagResolution: 133 | return TypeResolution 134 | 135 | case TagRange: 136 | return TypeRange 137 | 138 | case TagTextLang, TagNameLang: 139 | return TypeTextWithLang 140 | 141 | case TagBeginCollection: 142 | return TypeCollection 143 | 144 | case TagEndCollection: 145 | return TypeVoid 146 | 147 | case TagExtension: 148 | return TypeBinary 149 | 150 | default: 151 | return TypeBinary 152 | } 153 | } 154 | 155 | // String() returns a tag name, as defined by RFC 8010 156 | func (tag Tag) String() string { 157 | if 0 <= tag && int(tag) < len(tagNames) { 158 | if s := tagNames[tag]; s != "" { 159 | return s 160 | } 161 | } 162 | 163 | if tag < 0x100 { 164 | return fmt.Sprintf("0x%2.2x", uint32(tag)) 165 | } 166 | 167 | return fmt.Sprintf("0x%8.8x", uint(tag)) 168 | } 169 | 170 | var tagNames = [...]string{ 171 | // Delimiter tags 172 | TagZero: "zero", 173 | TagOperationGroup: "operation-attributes-tag", 174 | TagJobGroup: "job-attributes-tag", 175 | TagEnd: "end-of-attributes-tag", 176 | TagPrinterGroup: "printer-attributes-tag", 177 | TagUnsupportedGroup: "unsupported-attributes-tag", 178 | TagSubscriptionGroup: "subscription-attributes-tag", 179 | TagEventNotificationGroup: "event-notification-attributes-tag", 180 | TagResourceGroup: "resource-attributes-tag", 181 | TagDocumentGroup: "document-attributes-tag", 182 | TagSystemGroup: "system-attributes-tag", 183 | 184 | // Value tags 185 | TagUnsupportedValue: "unsupported", 186 | TagDefault: "default", 187 | TagUnknown: "unknown", 188 | TagNoValue: "no-value", 189 | TagNotSettable: "not-settable", 190 | TagDeleteAttr: "delete-attribute", 191 | TagAdminDefine: "admin-define", 192 | TagInteger: "integer", 193 | TagBoolean: "boolean", 194 | TagEnum: "enum", 195 | TagString: "octetString", 196 | TagDateTime: "dateTime", 197 | TagResolution: "resolution", 198 | TagRange: "rangeOfInteger", 199 | TagBeginCollection: "collection", 200 | TagTextLang: "textWithLanguage", 201 | TagNameLang: "nameWithLanguage", 202 | TagEndCollection: "endCollection", 203 | TagText: "textWithoutLanguage", 204 | TagName: "nameWithoutLanguage", 205 | TagKeyword: "keyword", 206 | TagURI: "uri", 207 | TagURIScheme: "uriScheme", 208 | TagCharset: "charset", 209 | TagLanguage: "naturalLanguage", 210 | TagMimeType: "mimeMediaType", 211 | TagMemberName: "memberAttrName", 212 | TagExtension: "extension", 213 | } 214 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * IPP Tags tests 7 | */ 8 | 9 | package goipp 10 | 11 | import "testing" 12 | 13 | // TestTagIsDelimiter tests Tag.IsDelimiter function 14 | func TestTagIsDelimiter(t *testing.T) { 15 | type testData struct { 16 | t Tag 17 | answer bool 18 | } 19 | 20 | tests := []testData{ 21 | {TagZero, true}, 22 | {TagOperationGroup, true}, 23 | {TagJobGroup, true}, 24 | {TagEnd, true}, 25 | {TagFuture15Group, true}, 26 | {TagUnsupportedValue, false}, 27 | {TagUnknown, false}, 28 | {TagInteger, false}, 29 | {TagBeginCollection, false}, 30 | {TagEndCollection, false}, 31 | {TagExtension, false}, 32 | } 33 | 34 | for _, test := range tests { 35 | answer := test.t.IsDelimiter() 36 | if answer != test.answer { 37 | t.Errorf("testing Tag.IsDelimiter:\n"+ 38 | "tag: %s (0x%.2x)\n"+ 39 | "expected: %v\n"+ 40 | "present: %v\n", 41 | test.t, uint32(test.t), test.answer, answer, 42 | ) 43 | } 44 | } 45 | } 46 | 47 | // TestTagIsGroup tests Tag.IsGroup function 48 | func TestTagIsGroup(t *testing.T) { 49 | type testData struct { 50 | t Tag 51 | answer bool 52 | } 53 | 54 | tests := []testData{ 55 | {TagZero, false}, 56 | {TagOperationGroup, true}, 57 | {TagJobGroup, true}, 58 | {TagEnd, false}, 59 | {TagPrinterGroup, true}, 60 | {TagUnsupportedGroup, true}, 61 | {TagSubscriptionGroup, true}, 62 | {TagEventNotificationGroup, true}, 63 | {TagResourceGroup, true}, 64 | {TagDocumentGroup, true}, 65 | {TagSystemGroup, true}, 66 | {TagFuture11Group, true}, 67 | {TagFuture12Group, true}, 68 | {TagFuture13Group, true}, 69 | {TagFuture14Group, true}, 70 | {TagFuture15Group, true}, 71 | {TagInteger, false}, 72 | } 73 | 74 | for _, test := range tests { 75 | answer := test.t.IsGroup() 76 | if answer != test.answer { 77 | t.Errorf("testing Tag.IsGroup:\n"+ 78 | "tag: %s (0x%.2x)\n"+ 79 | "expected: %v\n"+ 80 | "present: %v\n", 81 | test.t, uint32(test.t), test.answer, answer, 82 | ) 83 | } 84 | } 85 | } 86 | 87 | // TestTagType tests Tag.Type function 88 | func TestTagType(t *testing.T) { 89 | type testData struct { 90 | t Tag 91 | answer Type 92 | } 93 | 94 | tests := []testData{ 95 | {TagZero, TypeInvalid}, 96 | {TagInteger, TypeInteger}, 97 | {TagEnum, TypeInteger}, 98 | {TagBoolean, TypeBoolean}, 99 | {TagUnsupportedValue, TypeVoid}, 100 | {TagDefault, TypeVoid}, 101 | {TagUnknown, TypeVoid}, 102 | {TagNotSettable, TypeVoid}, 103 | {TagNoValue, TypeVoid}, 104 | {TagDeleteAttr, TypeVoid}, 105 | {TagAdminDefine, TypeVoid}, 106 | {TagText, TypeString}, 107 | {TagName, TypeString}, 108 | {TagReservedString, TypeString}, 109 | {TagKeyword, TypeString}, 110 | {TagURI, TypeString}, 111 | {TagURIScheme, TypeString}, 112 | {TagCharset, TypeString}, 113 | {TagLanguage, TypeString}, 114 | {TagMimeType, TypeString}, 115 | {TagMemberName, TypeString}, 116 | {TagDateTime, TypeDateTime}, 117 | {TagResolution, TypeResolution}, 118 | {TagRange, TypeRange}, 119 | {TagTextLang, TypeTextWithLang}, 120 | {TagNameLang, TypeTextWithLang}, 121 | {TagBeginCollection, TypeCollection}, 122 | {TagEndCollection, TypeVoid}, 123 | {TagExtension, TypeBinary}, 124 | {0x1234, TypeBinary}, 125 | } 126 | 127 | for _, test := range tests { 128 | answer := test.t.Type() 129 | if answer != test.answer { 130 | t.Errorf("testing Tag.Type:\n"+ 131 | "tag: %s (0x%.2x)\n"+ 132 | "expected: %v\n"+ 133 | "present: %v\n", 134 | test.t, uint32(test.t), test.answer, answer, 135 | ) 136 | } 137 | } 138 | } 139 | 140 | // TestTagString tests Tag.String function 141 | func TestTagString(t *testing.T) { 142 | type testData struct { 143 | t Tag 144 | answer string 145 | } 146 | 147 | tests := []testData{ 148 | {TagZero, "zero"}, 149 | {TagUnsupportedValue, "unsupported"}, 150 | {-1, "0xffffffff"}, 151 | {0xff, "0xff"}, 152 | {0x1234, "0x00001234"}, 153 | } 154 | 155 | for _, test := range tests { 156 | answer := test.t.String() 157 | if answer != test.answer { 158 | t.Errorf("testing Tag.IsGroup:\n"+ 159 | "tag: %s (0x%.2x)\n"+ 160 | "expected: %v\n"+ 161 | "present: %v\n", 162 | test.t, uint32(test.t), test.answer, answer, 163 | ) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Enumeration of value types 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | // Type enumerates all possible value types 16 | type Type int 17 | 18 | // Type values 19 | const ( 20 | TypeInvalid Type = -1 // Invalid Value type 21 | TypeVoid Type = iota // Value is Void 22 | TypeInteger // Value is Integer 23 | TypeBoolean // Value is Boolean 24 | TypeString // Value is String 25 | TypeDateTime // Value is Time 26 | TypeResolution // Value is Resolution 27 | TypeRange // Value is Range 28 | TypeTextWithLang // Value is TextWithLang 29 | TypeBinary // Value is Binary 30 | TypeCollection // Value is Collection 31 | ) 32 | 33 | // String converts Type to string, for debugging 34 | func (t Type) String() string { 35 | if t == TypeInvalid { 36 | return "Invalid" 37 | } 38 | 39 | if 0 <= t && int(t) < len(typeNames) { 40 | if s := typeNames[t]; s != "" { 41 | return s 42 | } 43 | } 44 | 45 | return fmt.Sprintf("0x%4.4x", uint(t)) 46 | } 47 | 48 | var typeNames = [...]string{ 49 | TypeVoid: "Void", 50 | TypeInteger: "Integer", 51 | TypeBoolean: "Boolean", 52 | TypeString: "String", 53 | TypeDateTime: "DateTime", 54 | TypeResolution: "Resolution", 55 | TypeRange: "Range", 56 | TypeTextWithLang: "TextWithLang", 57 | TypeBinary: "Binary", 58 | TypeCollection: "Collection", 59 | } 60 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Tests for enumeration of value types 7 | */ 8 | 9 | package goipp 10 | 11 | import "testing" 12 | 13 | // TestTypeString tests Type.String function 14 | func TestTypeString(t *testing.T) { 15 | type testData struct { 16 | ty Type // Input Type 17 | s string // Expected output string 18 | } 19 | 20 | tests := []testData{ 21 | {TypeInvalid, "Invalid"}, 22 | {TypeVoid, "Void"}, 23 | {TypeBoolean, "Boolean"}, 24 | {TypeString, "String"}, 25 | {TypeDateTime, "DateTime"}, 26 | {TypeResolution, "Resolution"}, 27 | {TypeRange, "Range"}, 28 | {TypeTextWithLang, "TextWithLang"}, 29 | {TypeBinary, "Binary"}, 30 | {TypeCollection, "Collection"}, 31 | {0x1234, "0x1234"}, 32 | } 33 | 34 | for _, test := range tests { 35 | s := test.ty.String() 36 | if s != test.s { 37 | t.Errorf("testing Type.String:\n"+ 38 | "input: %d\n"+ 39 | "expected: %s\n"+ 40 | "present: %s\n", 41 | int(test.ty), test.s, s, 42 | ) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Values for message attributes 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "bytes" 13 | "encoding/binary" 14 | "errors" 15 | "fmt" 16 | "math" 17 | "time" 18 | ) 19 | 20 | // Values represents a sequence of values with tags. 21 | // Usually Values used as a "payload" of Attribute 22 | type Values []struct { 23 | T Tag // The tag 24 | V Value // The value 25 | } 26 | 27 | // Add Value to Values 28 | func (values *Values) Add(t Tag, v Value) { 29 | *values = append(*values, struct { 30 | T Tag 31 | V Value 32 | }{t, v}) 33 | } 34 | 35 | // String converts Values to string 36 | func (values Values) String() string { 37 | if len(values) == 1 { 38 | return values[0].V.String() 39 | } 40 | 41 | var buf bytes.Buffer 42 | buf.Write([]byte("[")) 43 | for i, v := range values { 44 | if i != 0 { 45 | buf.Write([]byte(",")) 46 | } 47 | buf.Write([]byte(v.V.String())) 48 | } 49 | buf.Write([]byte("]")) 50 | 51 | return buf.String() 52 | } 53 | 54 | // Clone creates a shallow copy of Values. 55 | // For nil input it returns nil output. 56 | func (values Values) Clone() Values { 57 | var values2 Values 58 | if values != nil { 59 | values2 = make(Values, len(values)) 60 | copy(values2, values) 61 | } 62 | return values2 63 | } 64 | 65 | // DeepCopy creates a deep copy of Values 66 | // For nil input it returns nil output. 67 | func (values Values) DeepCopy() Values { 68 | var values2 Values 69 | if values != nil { 70 | values2 = make(Values, len(values)) 71 | for i := range values { 72 | values2[i].T = values[i].T 73 | values2[i].V = values[i].V.DeepCopy() 74 | } 75 | } 76 | return values2 77 | } 78 | 79 | // Equal performs deep check of equality of two Values. 80 | // 81 | // Note, Values(nil) and Values{} are not Equal but Similar. 82 | func (values Values) Equal(values2 Values) bool { 83 | if len(values) != len(values2) { 84 | return false 85 | } 86 | 87 | if (values == nil) != (values2 == nil) { 88 | return false 89 | } 90 | 91 | for i, v := range values { 92 | v2 := values2[i] 93 | if v.T != v2.T || !ValueEqual(v.V, v2.V) { 94 | return false 95 | } 96 | } 97 | 98 | return true 99 | } 100 | 101 | // Similar performs deep check of **logical** equality of two Values 102 | // 103 | // Note, Values(nil) and Values{} are not Equal but Similar. 104 | func (values Values) Similar(values2 Values) bool { 105 | if len(values) != len(values2) { 106 | return false 107 | } 108 | 109 | for i, v := range values { 110 | v2 := values2[i] 111 | if v.T != v2.T || !ValueSimilar(v.V, v2.V) { 112 | return false 113 | } 114 | } 115 | 116 | return true 117 | } 118 | 119 | // Value represents an attribute value 120 | // 121 | // IPP uses typed values, and type of each value is unambiguously 122 | // defined by the attribute tag 123 | type Value interface { 124 | String() string 125 | Type() Type 126 | DeepCopy() Value 127 | encode() ([]byte, error) 128 | decode([]byte) (Value, error) 129 | } 130 | 131 | var ( 132 | _ = Value(Binary(nil)) 133 | _ = Value(Boolean(false)) 134 | _ = Value(Collection(nil)) 135 | _ = Value(Integer(0)) 136 | _ = Value(Range{}) 137 | _ = Value(Resolution{}) 138 | _ = Value(String("")) 139 | _ = Value(TextWithLang{}) 140 | _ = Value(Time{time.Time{}}) 141 | _ = Value(Void{}) 142 | ) 143 | 144 | // IntegerOrRange is a Value of type Integer or Range 145 | type IntegerOrRange interface { 146 | Value 147 | 148 | // Within checks that x fits within the range: 149 | // 150 | // for Integer: x == Integer's value 151 | // for Range: Lower <= x && x <= Upper 152 | Within(x int) bool 153 | } 154 | 155 | var ( 156 | _ = IntegerOrRange(Integer(0)) 157 | _ = IntegerOrRange(Range{}) 158 | ) 159 | 160 | // ValueEqual checks if two values are equal 161 | // 162 | // Equality means that types and values are equal. For structured 163 | // values, like Collection, deep comparison is performed 164 | func ValueEqual(v1, v2 Value) bool { 165 | if v1.Type() != v2.Type() { 166 | return false 167 | } 168 | 169 | switch v1.Type() { 170 | case TypeDateTime: 171 | return v1.(Time).Equal(v2.(Time).Time) 172 | case TypeBinary: 173 | return bytes.Equal(v1.(Binary), v2.(Binary)) 174 | case TypeCollection: 175 | c1 := Attributes(v1.(Collection)) 176 | c2 := Attributes(v2.(Collection)) 177 | return c1.Equal(c2) 178 | } 179 | 180 | return v1 == v2 181 | } 182 | 183 | // ValueSimilar checks if two values are **logically** equal, 184 | // which means the following: 185 | // - If values are equal (i.e., ValueEqual() returns true), 186 | // they are similar. 187 | // - Binary and String values are similar, if they represent 188 | // the same sequence of bytes. 189 | // - Two collections are similar, if they contain the same 190 | // set of attributes (but may be differently ordered) and 191 | // values of these attributes are similar. 192 | func ValueSimilar(v1, v2 Value) bool { 193 | if ValueEqual(v1, v2) { 194 | return true 195 | } 196 | 197 | t1 := v1.Type() 198 | t2 := v2.Type() 199 | 200 | switch { 201 | case t1 == TypeBinary && t2 == TypeString: 202 | return bytes.Equal(v1.(Binary), []byte(v2.(String))) 203 | 204 | case t1 == TypeString && t2 == TypeBinary: 205 | return bytes.Equal([]byte(v1.(String)), v2.(Binary)) 206 | 207 | case t1 == TypeCollection && t2 == TypeCollection: 208 | return Attributes(v1.(Collection)).Similar( 209 | Attributes(v2.(Collection))) 210 | } 211 | 212 | return false 213 | } 214 | 215 | // Void is the Value that represents "no value" 216 | // 217 | // Use with: TagUnsupportedValue, TagDefault, TagUnknown, 218 | // TagNotSettable, TagDeleteAttr, TagAdminDefine 219 | type Void struct{} 220 | 221 | // String converts Void Value to string 222 | func (Void) String() string { return "" } 223 | 224 | // Type returns type of Value (TypeVoid for Void) 225 | func (Void) Type() Type { return TypeVoid } 226 | 227 | // DeepCopy returns a deep copy of the Void Value 228 | func (v Void) DeepCopy() Value { 229 | return v 230 | } 231 | 232 | // Encode Void Value into wire format 233 | func (v Void) encode() ([]byte, error) { 234 | return []byte{}, nil 235 | } 236 | 237 | // Decode Void Value from wire format 238 | func (Void) decode([]byte) (Value, error) { 239 | return Void{}, nil 240 | } 241 | 242 | // Integer is the Value that represents 32-bit signed int 243 | // 244 | // Use with: TagInteger, TagEnum 245 | type Integer int32 246 | 247 | // String converts Integer value to string 248 | func (v Integer) String() string { return fmt.Sprintf("%d", int32(v)) } 249 | 250 | // Type returns type of Value (TypeInteger for Integer) 251 | func (Integer) Type() Type { return TypeInteger } 252 | 253 | // DeepCopy returns a deep copy of the Integer Value 254 | func (v Integer) DeepCopy() Value { 255 | return v 256 | } 257 | 258 | // Within checks that x fits within the range 259 | // 260 | // It implements IntegerOrRange interface 261 | func (v Integer) Within(x int) bool { 262 | return x == int(v) 263 | } 264 | 265 | // Encode Integer Value into wire format 266 | func (v Integer) encode() ([]byte, error) { 267 | return []byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}, nil 268 | } 269 | 270 | // Decode Integer Value from wire format 271 | func (Integer) decode(data []byte) (Value, error) { 272 | if len(data) != 4 { 273 | return nil, errors.New("value must be 4 bytes") 274 | } 275 | 276 | return Integer(int32(binary.BigEndian.Uint32(data))), nil 277 | } 278 | 279 | // Boolean is the Value that contains true of false 280 | // 281 | // Use with: TagBoolean 282 | type Boolean bool 283 | 284 | // String converts Boolean value to string 285 | func (v Boolean) String() string { return fmt.Sprintf("%t", bool(v)) } 286 | 287 | // Type returns type of Value (TypeBoolean for Boolean) 288 | func (Boolean) Type() Type { return TypeBoolean } 289 | 290 | // DeepCopy returns a deep copy of the Boolean Value 291 | func (v Boolean) DeepCopy() Value { 292 | return v 293 | } 294 | 295 | // Encode Boolean Value into wire format 296 | func (v Boolean) encode() ([]byte, error) { 297 | if v { 298 | return []byte{1}, nil 299 | } 300 | return []byte{0}, nil 301 | } 302 | 303 | // Decode Boolean Value from wire format 304 | func (Boolean) decode(data []byte) (Value, error) { 305 | if len(data) != 1 { 306 | return nil, errors.New("value must be 1 byte") 307 | } 308 | 309 | return Boolean(data[0] != 0), nil 310 | } 311 | 312 | // String is the Value that represents string of text 313 | // 314 | // Use with: TagText, TagName, TagReservedString, TagKeyword, TagURI, 315 | // TagURIScheme, TagCharset, TagLanguage, TagMimeType, TagMemberName 316 | type String string 317 | 318 | // String converts String value to string 319 | func (v String) String() string { return string(v) } 320 | 321 | // Type returns type of Value (TypeString for String) 322 | func (String) Type() Type { return TypeString } 323 | 324 | // DeepCopy returns a deep copy of the String Value 325 | func (v String) DeepCopy() Value { 326 | return v 327 | } 328 | 329 | // Encode String Value into wire format 330 | func (v String) encode() ([]byte, error) { 331 | return []byte(v), nil 332 | } 333 | 334 | // Decode String Value from wire format 335 | func (String) decode(data []byte) (Value, error) { 336 | return String(data), nil 337 | } 338 | 339 | // Time is the Value that represents DataTime 340 | // 341 | // Use with: TagTime 342 | type Time struct{ time.Time } 343 | 344 | // String converts Time value to string 345 | func (v Time) String() string { return v.Time.Format(time.RFC3339) } 346 | 347 | // Type returns type of Value (TypeDateTime for Time) 348 | func (Time) Type() Type { return TypeDateTime } 349 | 350 | // DeepCopy returns a deep copy of the Time Value 351 | func (v Time) DeepCopy() Value { 352 | return v 353 | } 354 | 355 | // Encode Time Value into wire format 356 | func (v Time) encode() ([]byte, error) { 357 | // From RFC2579: 358 | // 359 | // field octets contents range 360 | // ----- ------ -------- ----- 361 | // 1 1-2 year* 0..65536 362 | // 2 3 month 1..12 363 | // 3 4 day 1..31 364 | // 4 5 hour 0..23 365 | // 5 6 minutes 0..59 366 | // 6 7 seconds 0..60 367 | // (use 60 for leap-second) 368 | // 7 8 deci-seconds 0..9 369 | // 8 9 direction from UTC '+' / '-' 370 | // 9 10 hours from UTC* 0..13 371 | // 10 11 minutes from UTC 0..59 372 | // 373 | // * Notes: 374 | // - the value of year is in network-byte order 375 | // - daylight saving time in New Zealand is +13 376 | 377 | year := v.Year() 378 | _, zone := v.Zone() 379 | dir := byte('+') 380 | if zone < 0 { 381 | zone = -zone 382 | dir = '-' 383 | } 384 | 385 | return []byte{ 386 | byte(year >> 8), byte(year), 387 | byte(v.Month()), 388 | byte(v.Day()), 389 | byte(v.Hour()), 390 | byte(v.Minute()), 391 | byte(v.Second()), 392 | byte(v.Nanosecond() / 100000000), 393 | dir, 394 | byte(zone / 3600), 395 | byte((zone / 60) % 60), 396 | }, nil 397 | } 398 | 399 | // Decode Time Value from wire format 400 | func (Time) decode(data []byte) (Value, error) { 401 | // Check size 402 | if len(data) != 11 { 403 | return nil, errors.New("value must be 11 bytes") 404 | } 405 | 406 | // Validate ranges 407 | var err error 408 | switch { 409 | case data[2] < 1 || data[2] > 12: 410 | err = fmt.Errorf("bad month %d", data[2]) 411 | case data[3] < 1 || data[3] > 31: 412 | err = fmt.Errorf("bad day %d", data[3]) 413 | case data[4] > 23: 414 | err = fmt.Errorf("bad hours %d", data[4]) 415 | case data[5] > 59: 416 | err = fmt.Errorf("bad minutes %d", data[5]) 417 | case data[6] > 60: 418 | err = fmt.Errorf("bad seconds %d", data[6]) 419 | case data[7] > 9: 420 | err = fmt.Errorf("bad deciseconds %d", data[7]) 421 | case data[8] != '+' && data[8] != '-': 422 | return nil, errors.New("bad UTC sign") 423 | case data[9] > 11: 424 | err = fmt.Errorf("bad UTC hours %d", data[9]) 425 | case data[10] > 59: 426 | err = fmt.Errorf("bad UTC minutes %d", data[10]) 427 | } 428 | 429 | if err != nil { 430 | return Time{}, err 431 | } 432 | 433 | // Decode time zone 434 | tzName := fmt.Sprintf("UTC%c%d", data[8], data[9]) 435 | if data[10] != 0 { 436 | tzName += fmt.Sprintf(":%d", data[10]) 437 | } 438 | 439 | tzOff := 3600*int(data[9]) + 60*int(data[10]) 440 | if data[8] == '-' { 441 | tzOff = -tzOff 442 | } 443 | 444 | tz := time.FixedZone(tzName, tzOff) 445 | 446 | // Decode time 447 | t := time.Date( 448 | int(binary.BigEndian.Uint16(data[0:2])), // year 449 | time.Month(data[2]), // month 450 | int(data[3]), // day 451 | int(data[4]), // hour 452 | int(data[5]), // min 453 | int(data[6]), // sec 454 | int(data[7])*100000000, // nsec 455 | tz, // time zone 456 | ) 457 | 458 | return Time{t}, nil 459 | } 460 | 461 | // Resolution is the Value that represents image resolution. 462 | // 463 | // Use with: TagResolution 464 | type Resolution struct { 465 | Xres, Yres int // X/Y resolutions 466 | Units Units // Resolution units 467 | } 468 | 469 | // String converts Resolution value to string 470 | func (v Resolution) String() string { 471 | return fmt.Sprintf("%dx%d%s", v.Xres, v.Yres, v.Units) 472 | } 473 | 474 | // Type returns type of Value (TypeResolution for Resolution) 475 | func (Resolution) Type() Type { return TypeResolution } 476 | 477 | // DeepCopy returns a deep copy of the Resolution Value 478 | func (v Resolution) DeepCopy() Value { 479 | return v 480 | } 481 | 482 | // Encode Resolution Value into wire format 483 | func (v Resolution) encode() ([]byte, error) { 484 | // Wire format 485 | // 4 bytes: Xres 486 | // 4 bytes: Yres 487 | // 1 byte: Units 488 | 489 | x, y := v.Xres, v.Yres 490 | 491 | return []byte{ 492 | byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x), 493 | byte(y >> 24), byte(y >> 16), byte(y >> 8), byte(y), 494 | byte(v.Units), 495 | }, nil 496 | } 497 | 498 | // Decode Resolution Value from wire format 499 | func (Resolution) decode(data []byte) (Value, error) { 500 | if len(data) != 9 { 501 | return nil, errors.New("value must be 9 bytes") 502 | } 503 | 504 | return Resolution{ 505 | Xres: int(int32(binary.BigEndian.Uint32(data[0:4]))), 506 | Yres: int(int32(binary.BigEndian.Uint32(data[4:8]))), 507 | Units: Units(data[8]), 508 | }, nil 509 | 510 | } 511 | 512 | // Units represents resolution units 513 | type Units uint8 514 | 515 | // Resolution units codes 516 | const ( 517 | UnitsDpi Units = 3 // Dots per inch 518 | UnitsDpcm Units = 4 // Dots per cm 519 | ) 520 | 521 | // String converts Units to string 522 | func (u Units) String() string { 523 | switch u { 524 | case UnitsDpi: 525 | return "dpi" 526 | case UnitsDpcm: 527 | return "dpcm" 528 | default: 529 | return fmt.Sprintf("unknown(0x%2.2x)", uint8(u)) 530 | } 531 | } 532 | 533 | // Range is the Value that represents a range of 32-bit signed integers 534 | // 535 | // Use with: TagRange 536 | type Range struct { 537 | Lower, Upper int // Lower/upper bounds 538 | } 539 | 540 | // String converts Range value to string 541 | func (v Range) String() string { 542 | return fmt.Sprintf("%d-%d", v.Lower, v.Upper) 543 | } 544 | 545 | // Type returns type of Value (TypeRange for Range) 546 | func (Range) Type() Type { return TypeRange } 547 | 548 | // DeepCopy returns a deep copy of the Range Value 549 | func (v Range) DeepCopy() Value { 550 | return v 551 | } 552 | 553 | // Encode Range Value into wire format 554 | func (v Range) encode() ([]byte, error) { 555 | // Wire format 556 | // 4 bytes: Lower 557 | // 4 bytes: Upper 558 | 559 | l, u := v.Lower, v.Upper 560 | 561 | return []byte{ 562 | byte(l >> 24), byte(l >> 16), byte(l >> 8), byte(l), 563 | byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u), 564 | }, nil 565 | } 566 | 567 | // Within checks that x fits within the range 568 | // 569 | // It implements IntegerOrRange interface 570 | func (v Range) Within(x int) bool { 571 | return v.Lower <= x && x <= v.Upper 572 | } 573 | 574 | // Decode Range Value from wire format 575 | func (Range) decode(data []byte) (Value, error) { 576 | if len(data) != 8 { 577 | return nil, errors.New("value must be 8 bytes") 578 | } 579 | 580 | return Range{ 581 | Lower: int(int32(binary.BigEndian.Uint32(data[0:4]))), 582 | Upper: int(int32(binary.BigEndian.Uint32(data[4:8]))), 583 | }, nil 584 | } 585 | 586 | // TextWithLang is the Value that represents a combination 587 | // of two strings: 588 | // - text on some natural language (i.e., "hello") 589 | // - name of that language (i.e., "en") 590 | // 591 | // Use with: TagTextLang, TagNameLang 592 | type TextWithLang struct { 593 | Lang, Text string // Language and text 594 | } 595 | 596 | // String converts TextWithLang value to string 597 | func (v TextWithLang) String() string { return v.Text + " [" + v.Lang + "]" } 598 | 599 | // Type returns type of Value (TypeTextWithLang for TextWithLang) 600 | func (TextWithLang) Type() Type { return TypeTextWithLang } 601 | 602 | // DeepCopy returns a deep copy of the TextWithLang Value 603 | func (v TextWithLang) DeepCopy() Value { 604 | return v 605 | } 606 | 607 | // Encode TextWithLang Value into wire format 608 | func (v TextWithLang) encode() ([]byte, error) { 609 | // Wire format 610 | // 2 bytes: len(Lang) 611 | // variable: Lang 612 | // 2 bytes: len(Text) 613 | // variable: Text 614 | 615 | lang := []byte(v.Lang) 616 | text := []byte(v.Text) 617 | 618 | if len(lang) > math.MaxUint16 { 619 | return nil, fmt.Errorf("Lang exceeds %d bytes", math.MaxUint16) 620 | } 621 | 622 | if len(text) > math.MaxUint16 { 623 | return nil, fmt.Errorf("Text exceeds %d bytes", math.MaxUint16) 624 | } 625 | 626 | data := make([]byte, 2+2+len(lang)+len(text)) 627 | binary.BigEndian.PutUint16(data, uint16(len(lang))) 628 | copy(data[2:], []byte(lang)) 629 | 630 | data2 := data[2+len(lang):] 631 | binary.BigEndian.PutUint16(data2, uint16(len(text))) 632 | copy(data2[2:], []byte(text)) 633 | 634 | return data, nil 635 | } 636 | 637 | // Decode TextWithLang Value from wire format 638 | func (TextWithLang) decode(data []byte) (Value, error) { 639 | var langLen, textLen int 640 | var lang, text string 641 | 642 | // Unpack language length 643 | if len(data) < 2 { 644 | return nil, errors.New("truncated language length") 645 | } 646 | 647 | langLen = int(binary.BigEndian.Uint16(data[0:2])) 648 | data = data[2:] 649 | 650 | // Unpack language value 651 | if len(data) < langLen { 652 | return nil, errors.New("truncated language name") 653 | } 654 | 655 | lang = string(data[:langLen]) 656 | data = data[langLen:] 657 | 658 | // Unpack text length 659 | if len(data) < 2 { 660 | return nil, errors.New("truncated text length") 661 | } 662 | 663 | textLen = int(binary.BigEndian.Uint16(data[0:2])) 664 | data = data[2:] 665 | 666 | // Unpack text value 667 | if len(data) < textLen { 668 | return nil, errors.New("truncated text string") 669 | } 670 | 671 | text = string(data[:textLen]) 672 | data = data[textLen:] 673 | 674 | // We must have consumed all bytes at this point 675 | if len(data) != 0 { 676 | return nil, fmt.Errorf("extra %d bytes at the end of value", 677 | len(data)) 678 | } 679 | 680 | // Return a value 681 | return TextWithLang{Lang: lang, Text: text}, nil 682 | } 683 | 684 | // Binary is the Value that represents a raw binary data 685 | type Binary []byte 686 | 687 | // String converts Binary value to string 688 | func (v Binary) String() string { 689 | return fmt.Sprintf("%x", []byte(v)) 690 | } 691 | 692 | // Type returns type of Value (TypeBinary for Binary) 693 | func (Binary) Type() Type { return TypeBinary } 694 | 695 | // DeepCopy returns a deep copy of the Binary Value 696 | func (v Binary) DeepCopy() Value { 697 | v2 := make(Binary, len(v)) 698 | copy(v2, v) 699 | return v2 700 | } 701 | 702 | // Encode TextWithLang Value into wire format 703 | func (v Binary) encode() ([]byte, error) { 704 | return []byte(v), nil 705 | } 706 | 707 | // Decode Binary Value from wire format 708 | func (Binary) decode(data []byte) (Value, error) { 709 | return Binary(data), nil 710 | } 711 | 712 | // Collection is the Value that represents collection of attributes 713 | // 714 | // Use with: TagBeginCollection 715 | type Collection Attributes 716 | 717 | // Add Attribute to Attributes 718 | func (v *Collection) Add(attr Attribute) { 719 | *v = append(*v, attr) 720 | } 721 | 722 | // String converts Collection to string 723 | func (v Collection) String() string { 724 | var buf bytes.Buffer 725 | buf.Write([]byte("{")) 726 | for i, attr := range v { 727 | if i > 0 { 728 | buf.Write([]byte(" ")) 729 | } 730 | fmt.Fprintf(&buf, "%s=%s", attr.Name, attr.Values) 731 | } 732 | buf.Write([]byte("}")) 733 | 734 | return buf.String() 735 | } 736 | 737 | // Type returns type of Value (TypeCollection for Collection) 738 | func (Collection) Type() Type { return TypeCollection } 739 | 740 | // DeepCopy returns a deep copy of the Collection Value 741 | func (v Collection) DeepCopy() Value { 742 | return Collection(Attributes(v).DeepCopy()) 743 | } 744 | 745 | // Encode Collection Value into wire format 746 | func (Collection) encode() ([]byte, error) { 747 | // Note, TagBeginCollection attribute contains 748 | // no data, collection itself handled the different way 749 | return []byte{}, nil 750 | } 751 | 752 | // Decode Collection Value from wire format 753 | func (Collection) decode(data []byte) (Value, error) { 754 | panic("internal error") 755 | } 756 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | /* Go IPP - IPP core protocol implementation in pure Go 2 | * 3 | * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) 4 | * See LICENSE for license terms and conditions 5 | * 6 | * Values test 7 | */ 8 | 9 | package goipp 10 | 11 | import ( 12 | "bytes" 13 | "errors" 14 | "reflect" 15 | "strings" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | // TestValueEncode tests Value.encode for all value types 21 | func TestValueEncode(t *testing.T) { 22 | noError := errors.New("") 23 | longstr := strings.Repeat("x", 65536) 24 | loc1 := time.FixedZone("UTC+3:30", 3*3600+1800) 25 | tm1, _ := time.ParseInLocation(time.RFC3339, "2025-03-29T16:48:53+03:30", loc1) 26 | loc2 := time.FixedZone("UTC-3", -3*3600) 27 | tm2, _ := time.ParseInLocation(time.RFC3339, "2025-03-29T16:48:53-03:00", loc2) 28 | 29 | type testData struct { 30 | v Value // Input value 31 | data []byte // Expected output data 32 | err string // Expected error string ("" if no error) 33 | } 34 | 35 | tests := []testData{ 36 | // Simple values 37 | {Binary{}, []byte{}, ""}, 38 | {Binary{1, 2, 3}, []byte{1, 2, 3}, ""}, 39 | {Boolean(false), []byte{0}, ""}, 40 | {Boolean(true), []byte{1}, ""}, 41 | {Integer(0), []byte{0, 0, 0, 0}, ""}, 42 | {Integer(0x01020304), []byte{1, 2, 3, 4}, ""}, 43 | {String(""), []byte{}, ""}, 44 | {String("Hello"), []byte("Hello"), ""}, 45 | {Void{}, []byte{}, ""}, 46 | 47 | // Range 48 | { 49 | v: Range{0x01020304, 0x05060708}, 50 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 51 | }, 52 | { 53 | v: Range{-100, 100}, 54 | data: []byte{ 55 | 0xff, 0xff, 0xff, 0x9c, 0x00, 0x00, 0x00, 0x64, 56 | }, 57 | }, 58 | 59 | // Resolution 60 | { 61 | v: Resolution{0x01020304, 0x05060708, 0x09}, 62 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, 63 | }, 64 | { 65 | v: Resolution{150, 300, UnitsDpi}, 66 | data: []byte{ 67 | 0x00, 0x00, 0x00, 0x96, // 150 68 | 0x00, 0x00, 0x01, 0x2c, // 300 69 | 0x03, 70 | }, 71 | }, 72 | 73 | // TextWithLang 74 | { 75 | v: TextWithLang{"en-US", "Hello!"}, 76 | data: []byte{ 77 | 0x00, 0x05, 78 | 'e', 'n', '-', 'U', 'S', 79 | 0x00, 0x06, 80 | 'H', 'e', 'l', 'l', 'o', '!', 81 | }, 82 | }, 83 | 84 | { 85 | v: TextWithLang{"ru-RU", "Привет!"}, 86 | data: []byte{ 87 | 0x00, 0x05, 88 | 'r', 'u', '-', 'R', 'U', 89 | 0x00, 0x0d, 90 | 0xd0, 0x9f, 0xd1, 0x80, 0xd0, 0xb8, 0xd0, 0xb2, 91 | 0xd0, 0xb5, 0xd1, 0x82, 0x21, 92 | }, 93 | }, 94 | 95 | { 96 | v: TextWithLang{"en-US", longstr}, 97 | err: "Text exceeds 65535 bytes", 98 | }, 99 | 100 | { 101 | v: TextWithLang{longstr, "hello"}, 102 | err: "Lang exceeds 65535 bytes", 103 | }, 104 | 105 | // Time 106 | { 107 | v: Time{tm1}, 108 | data: []byte{ 109 | 0x07, 0xe9, 110 | 0x03, // Month, 1...12 111 | 0x1d, // Day, 1...31 112 | 0x10, // Hour, 0...23 113 | 0x30, // Minutes, 0...59 114 | 0x35, // Seconds, 0...59 115 | 0x00, // Deci-seconds, 0...9 116 | '+', // Direction from UTC, +/- 117 | 0x03, // Hours from UTC 118 | 0x1e, // Minutes from UTC 119 | }, 120 | }, 121 | 122 | { 123 | v: Time{tm2}, 124 | data: []byte{ 125 | 0x07, 0xe9, 126 | 0x03, // Month, 1...12 127 | 0x1d, // Day, 1...31 128 | 0x10, // Hour, 0...23 129 | 0x30, // Minutes, 0...59 130 | 0x35, // Seconds, 0...59 131 | 0x00, // Deci-seconds, 0...9 132 | '-', // Direction from UTC, +/- 133 | 0x03, // Hours from UTC 134 | 0x00, // Minutes from UTC 135 | }, 136 | }, 137 | 138 | // Collection 139 | // 140 | // Note, Collection.encode is the stub and encodes 141 | // as Void. Actual collection encoding handled the 142 | // different way. 143 | { 144 | v: Collection{ 145 | MakeAttribute("test", TagString, String("")), 146 | }, 147 | data: []byte{}, 148 | }, 149 | } 150 | 151 | for _, test := range tests { 152 | data, err := test.v.encode() 153 | if err == nil { 154 | err = noError 155 | } 156 | 157 | vstr := test.v.String() 158 | if len(vstr) > 40 { 159 | vstr = vstr[:40] + "..." 160 | } 161 | 162 | if err.Error() != test.err { 163 | t.Errorf("testing %s.encode:\n"+ 164 | "value: %s\n"+ 165 | "error expected: %q\n"+ 166 | "error present: %q\n", 167 | reflect.TypeOf(test.v).String(), 168 | vstr, 169 | test.err, 170 | err, 171 | ) 172 | continue 173 | } 174 | 175 | if test.err == "" && !bytes.Equal(data, test.data) { 176 | t.Errorf("testing %s.encode:\n"+ 177 | "value: %s\n"+ 178 | "data expected: %x\n"+ 179 | "data present: %x\n", 180 | reflect.TypeOf(test.v).String(), 181 | vstr, 182 | test.data, 183 | data, 184 | ) 185 | } 186 | } 187 | } 188 | 189 | // TestValueEncode tests Value.decode for all value types 190 | func TestValueDecode(t *testing.T) { 191 | noError := errors.New("") 192 | loc1 := time.FixedZone("UTC+3:30", 3*3600+1800) 193 | tm1, _ := time.ParseInLocation(time.RFC3339, "2025-03-29T16:48:53+03:30", loc1) 194 | loc2 := time.FixedZone("UTC-3", -3*3600) 195 | tm2, _ := time.ParseInLocation(time.RFC3339, "2025-03-29T16:48:53-03:00", loc2) 196 | 197 | type testData struct { 198 | data []byte // Input data 199 | v Value // Expected output value 200 | err string // Expected error string ("" if no error) 201 | } 202 | 203 | tests := []testData{ 204 | // Simple types 205 | {[]byte{}, Binary{}, ""}, 206 | {[]byte{1, 2, 3, 4}, Binary{1, 2, 3, 4}, ""}, 207 | {[]byte{0}, Boolean(false), ""}, 208 | {[]byte{1}, Boolean(true), ""}, 209 | {[]byte{0, 1}, Boolean(false), "value must be 1 byte"}, 210 | {[]byte{1, 2, 3, 4}, Integer(0x01020304), ""}, 211 | {[]byte{0xff, 0xff, 0xff, 0xff}, Integer(-1), ""}, 212 | {[]byte{}, Integer(0), "value must be 4 bytes"}, 213 | {[]byte{1, 2, 3, 4, 5}, Integer(0), "value must be 4 bytes"}, 214 | {[]byte{}, Void{}, ""}, 215 | {[]byte("hello"), String("hello"), ""}, 216 | {[]byte{1, 2, 3, 4, 5}, Void{}, ""}, 217 | 218 | // Range 219 | { 220 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 221 | v: Range{0x01020304, 0x05060708}, 222 | }, 223 | 224 | { 225 | data: []byte{ 226 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 227 | }, 228 | v: Range{-1, -1}, 229 | }, 230 | 231 | { 232 | data: []byte{1, 2, 3, 4, 5, 6, 7}, 233 | v: Range{}, 234 | err: "value must be 8 bytes", 235 | }, 236 | 237 | { 238 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, 239 | v: Range{}, 240 | err: "value must be 8 bytes", 241 | }, 242 | 243 | // Resolution 244 | { 245 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, 246 | v: Resolution{0x01020304, 0x05060708, 0x09}, 247 | }, 248 | 249 | { 250 | data: []byte{ 251 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 9, 252 | }, 253 | v: Resolution{-1, -1, 0x09}, 254 | }, 255 | 256 | { 257 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 258 | v: Resolution{}, 259 | err: "value must be 9 bytes", 260 | }, 261 | 262 | { 263 | data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 264 | v: Resolution{}, 265 | err: "value must be 9 bytes", 266 | }, 267 | 268 | // Time 269 | { 270 | // Good time, UTC+... 271 | data: []byte{ 272 | 0x07, 0xe9, 273 | 0x03, // Month, 1...12 274 | 0x1d, // Day, 1...31 275 | 0x10, // Hour, 0...23 276 | 0x30, // Minutes, 0...59 277 | 0x35, // Seconds, 0...59 278 | 0x00, // Deci-seconds, 0...9 279 | '+', // Direction from UTC, +/- 280 | 0x03, // Hours from UTC 281 | 0x1e, // Minutes from UTC 282 | }, 283 | v: Time{tm1}, 284 | }, 285 | 286 | { 287 | // Good time, UTC-... 288 | data: []byte{ 289 | 0x07, 0xe9, 290 | 0x03, // Month, 1...12 291 | 0x1d, // Day, 1...31 292 | 0x10, // Hour, 0...23 293 | 0x30, // Minutes, 0...59 294 | 0x35, // Seconds, 0...59 295 | 0x00, // Deci-seconds, 0...9 296 | '-', // Direction from UTC, +/- 297 | 0x03, // Hours from UTC 298 | 0x00, // Minutes from UTC 299 | }, 300 | v: Time{tm2}, 301 | }, 302 | 303 | { 304 | // Truncated data 305 | data: []byte{ 306 | 0x07, 0xe9, 307 | 0x03, // Month, 1...12 308 | 0x1d, // Day, 1...31 309 | 0x10, // Hour, 0...23 310 | 0x30, // Minutes, 0...59 311 | 0x35, // Seconds, 0...59 312 | 0x00, // Deci-seconds, 0...9 313 | '+', // Direction from UTC, +/- 314 | 0x03, // Hours from UTC 315 | }, 316 | v: Time{}, 317 | err: "value must be 11 bytes", 318 | }, 319 | 320 | { 321 | // Extra data 322 | data: []byte{ 323 | 0x07, 0xe9, 324 | 0x03, // Month, 1...12 325 | 0x1d, // Day, 1...31 326 | 0x10, // Hour, 0...23 327 | 0x30, // Minutes, 0...59 328 | 0x35, // Seconds, 0...59 329 | 0x00, // Deci-seconds, 0...9 330 | '+', // Direction from UTC, +/- 331 | 0x03, // Hours from UTC 332 | 0x00, // Minutes from UTC 333 | 0, 334 | }, 335 | v: Time{}, 336 | err: "value must be 11 bytes", 337 | }, 338 | 339 | { 340 | // Bad month 341 | data: []byte{ 342 | 0x07, 0xe9, 343 | 0, // Month, 1...12 344 | 0x1d, // Day, 1...31 345 | 0x10, // Hour, 0...23 346 | 0x30, // Minutes, 0...59 347 | 0x35, // Seconds, 0...59 348 | 0x00, // Deci-seconds, 0...9 349 | '+', // Direction from UTC, +/- 350 | 0x03, // Hours from UTC 351 | 0x00, // Minutes from UTC 352 | }, 353 | v: Time{}, 354 | err: "bad month 0", 355 | }, 356 | 357 | { 358 | // Bad day 359 | data: []byte{ 360 | 0x07, 0xe9, 361 | 0x03, // Month, 1...12 362 | 32, // Day, 1...31 363 | 0x10, // Hour, 0...23 364 | 0x30, // Minutes, 0...59 365 | 0x35, // Seconds, 0...59 366 | 0x00, // Deci-seconds, 0...9 367 | '+', // Direction from UTC, +/- 368 | 0x03, // Hours from UTC 369 | 0x00, // Minutes from UTC 370 | }, 371 | v: Time{}, 372 | err: "bad day 32", 373 | }, 374 | 375 | { 376 | // Bad hours 377 | data: []byte{ 378 | 0x07, 0xe9, 379 | 0x03, // Month, 1...12 380 | 0x1d, // Day, 1...31 381 | 99, // Hour, 0...23 382 | 0x30, // Minutes, 0...59 383 | 0x35, // Seconds, 0...59 384 | 0x00, // Deci-seconds, 0...9 385 | '+', // Direction from UTC, +/- 386 | 0x03, // Hours from UTC 387 | 0x00, // Minutes from UTC 388 | }, 389 | v: Time{}, 390 | err: "bad hours 99", 391 | }, 392 | 393 | { 394 | // Bad minutes 395 | data: []byte{ 396 | 0x07, 0xe9, 397 | 0x03, // Month, 1...12 398 | 0x1d, // Day, 1...31 399 | 0x10, // Hour, 0...23 400 | 88, // Minutes, 0...59 401 | 0x35, // Seconds, 0...59 402 | 0x00, // Deci-seconds, 0...9 403 | '+', // Direction from UTC, +/- 404 | 0x03, // Hours from UTC 405 | 0x00, // Minutes from UTC 406 | }, 407 | v: Time{}, 408 | err: "bad minutes 88", 409 | }, 410 | 411 | { 412 | // Bad seconds 413 | data: []byte{ 414 | 0x07, 0xe9, 415 | 0x03, // Month, 1...12 416 | 0x1d, // Day, 1...31 417 | 0x10, // Hour, 0...23 418 | 0x30, // Minutes, 0...59 419 | 77, // Seconds, 0...59 420 | 0x00, // Deci-seconds, 0...9 421 | '+', // Direction from UTC, +/- 422 | 0x03, // Hours from UTC 423 | 0x00, // Minutes from UTC 424 | }, 425 | v: Time{}, 426 | err: "bad seconds 77", 427 | }, 428 | 429 | { 430 | // Bad deciseconds 431 | data: []byte{ 432 | 0x07, 0xe9, 433 | 0x03, // Month, 1...12 434 | 0x1d, // Day, 1...31 435 | 0x10, // Hour, 0...23 436 | 0x30, // Minutes, 0...59 437 | 0x35, // Seconds, 0...59 438 | 100, // Deci-seconds, 0...9 439 | '+', // Direction from UTC, +/- 440 | 0x03, // Hours from UTC 441 | 0x00, // Minutes from UTC 442 | }, 443 | v: Time{}, 444 | err: "bad deciseconds 100", 445 | }, 446 | 447 | { 448 | // Bad UTC sign 449 | data: []byte{ 450 | 0x07, 0xe9, 451 | 0x03, // Month, 1...12 452 | 0x1d, // Day, 1...31 453 | 0x10, // Hour, 0...23 454 | 0x30, // Minutes, 0...59 455 | 0x35, // Seconds, 0...59 456 | 0x00, // Deci-seconds, 0...9 457 | '?', // Direction from UTC, +/- 458 | 0x03, // Hours from UTC 459 | 0x00, // Minutes from UTC 460 | }, 461 | v: Time{}, 462 | err: "bad UTC sign", 463 | }, 464 | 465 | { 466 | // Bad UTC hours 467 | data: []byte{ 468 | 0x07, 0xe9, 469 | 0x03, // Month, 1...12 470 | 0x1d, // Day, 1...31 471 | 0x10, // Hour, 0...23 472 | 0x30, // Minutes, 0...59 473 | 0x35, // Seconds, 0...59 474 | 0x00, // Deci-seconds, 0...9 475 | '+', // Direction from UTC, +/- 476 | 66, // Hours from UTC 477 | 0x00, // Minutes from UTC 478 | }, 479 | v: Time{}, 480 | err: "bad UTC hours 66", 481 | }, 482 | 483 | { 484 | // Bad UTC minutes 485 | data: []byte{ 486 | 0x07, 0xe9, 487 | 0x03, // Month, 1...12 488 | 0x1d, // Day, 1...31 489 | 0x10, // Hour, 0...23 490 | 0x30, // Minutes, 0...59 491 | 0x35, // Seconds, 0...59 492 | 0x00, // Deci-seconds, 0...9 493 | '+', // Direction from UTC, +/- 494 | 0x03, // Hours from UTC 495 | 166, // Minutes from UTC 496 | }, 497 | v: Time{}, 498 | err: "bad UTC minutes 166", 499 | }, 500 | 501 | // TextWithLang 502 | { 503 | data: []byte{ 504 | 0x00, 0x05, 505 | 'e', 'n', '-', 'U', 'S', 506 | 0x00, 0x06, 507 | 'H', 'e', 'l', 'l', 'o', '!', 508 | }, 509 | v: TextWithLang{"en-US", "Hello!"}, 510 | }, 511 | 512 | { 513 | data: []byte{ 514 | 0x00, 0x05, 515 | 'r', 'u', '-', 'R', 'U', 516 | 0x00, 0x0d, 517 | 0xd0, 0x9f, 0xd1, 0x80, 0xd0, 0xb8, 0xd0, 0xb2, 518 | 0xd0, 0xb5, 0xd1, 0x82, 0x21, 519 | }, 520 | v: TextWithLang{"ru-RU", "Привет!"}, 521 | }, 522 | 523 | { 524 | // truncated language length 525 | data: []byte{}, 526 | v: TextWithLang{}, 527 | err: "truncated language length", 528 | }, 529 | 530 | { 531 | // truncated language name 532 | data: []byte{ 533 | 0x00, 0x05, 534 | 'e', 535 | }, 536 | v: TextWithLang{}, 537 | err: "truncated language name", 538 | }, 539 | 540 | { 541 | // truncated text length 542 | data: []byte{ 543 | 0x00, 0x05, 544 | 'e', 'n', '-', 'U', 'S', 545 | 0x00, 546 | }, 547 | v: TextWithLang{}, 548 | err: "truncated text length", 549 | }, 550 | 551 | { 552 | // truncated text string 553 | data: []byte{ 554 | 0x00, 0x05, 555 | 'e', 'n', '-', 'U', 'S', 556 | 0x00, 0x06, 557 | 'H', 'e', 558 | }, 559 | v: TextWithLang{}, 560 | err: "truncated text string", 561 | }, 562 | 563 | { 564 | // extra data bytes 565 | data: []byte{ 566 | 0x00, 0x05, 567 | 'e', 'n', '-', 'U', 'S', 568 | 0x00, 0x06, 569 | 'H', 'e', 'l', 'l', 'o', '!', 570 | 0, 2, 3, 571 | }, 572 | v: TextWithLang{}, 573 | err: "extra 3 bytes at the end of value", 574 | }, 575 | } 576 | 577 | for _, test := range tests { 578 | v, err := test.v.decode(test.data) 579 | if err == nil { 580 | err = noError 581 | } 582 | 583 | if err.Error() != test.err { 584 | t.Errorf("testing %s.decode:\n"+ 585 | "value: %s\n"+ 586 | "error expected: %q\n"+ 587 | "error present: %q\n", 588 | reflect.TypeOf(test.v).String(), 589 | v, 590 | test.err, 591 | err, 592 | ) 593 | continue 594 | } 595 | 596 | if test.err == "" && !reflect.DeepEqual(v, test.v) { 597 | t.Errorf("testing %s.decode:\n"+ 598 | "data: %x\n"+ 599 | "value expected: %#v\n"+ 600 | "value present: %#v\n", 601 | reflect.TypeOf(test.v).String(), 602 | test.data, 603 | test.v, 604 | v, 605 | ) 606 | } 607 | 608 | } 609 | } 610 | 611 | // TestValueCollectionDecode tests Collection.decode for all value types 612 | func TestValueCollectionDecode(t *testing.T) { 613 | // Collection.decode is a stub and must panic 614 | defer func() { 615 | recover() 616 | }() 617 | 618 | v := Collection{} 619 | v.decode([]byte{}) 620 | 621 | t.Errorf("Collection.decode() method is a stub and must panic") 622 | } 623 | 624 | // TestValueString rests Value.String method for various 625 | // kinds of the Value 626 | func TestValueString(t *testing.T) { 627 | loc1 := time.FixedZone("UTC+3:30", 3*3600+1800) 628 | tm1, _ := time.ParseInLocation(time.RFC3339, "2025-03-29T16:48:53+03:30", loc1) 629 | 630 | type testData struct { 631 | v Value // Input value 632 | s string // Expected output string 633 | } 634 | 635 | tests := []testData{ 636 | // Simple types 637 | {Binary{}, ""}, 638 | {Binary{1, 2, 3}, "010203"}, 639 | {Integer(123), "123"}, 640 | {Integer(-321), "-321"}, 641 | {Range{-100, 200}, "-100-200"}, 642 | {Range{-100, -50}, "-100--50"}, 643 | {Resolution{150, 300, UnitsDpi}, "150x300dpi"}, 644 | {Resolution{100, 200, UnitsDpcm}, "100x200dpcm"}, 645 | {Resolution{75, 150, 10}, "75x150unknown(0x0a)"}, 646 | {String("hello"), "hello"}, 647 | {TextWithLang{"en-US", "hello"}, "hello [en-US]"}, 648 | {Time{tm1}, "2025-03-29T16:48:53+03:30"}, 649 | {Void{}, ""}, 650 | 651 | // Collections 652 | {Collection{}, "{}"}, 653 | 654 | { 655 | v: Collection{ 656 | MakeAttr("attr1", TagInteger, Integer(1)), 657 | MakeAttr("attr2", TagString, String("hello")), 658 | }, 659 | s: "{attr1=1 attr2=hello}", 660 | }, 661 | } 662 | 663 | for _, test := range tests { 664 | s := test.v.String() 665 | if s != test.s { 666 | t.Errorf("testing %s.String:\n"+ 667 | "value: %#v\n"+ 668 | "expected: %q\n"+ 669 | "present: %q\n", 670 | reflect.TypeOf(test.v).String(), 671 | test.v, test.s, s, 672 | ) 673 | } 674 | } 675 | } 676 | 677 | // TestValueType rests Value.Type method for various 678 | // kinds of the Value 679 | func TestValueType(t *testing.T) { 680 | type testData struct { 681 | v Value // Input value 682 | tp Type // Expected output type 683 | } 684 | 685 | tests := []testData{ 686 | {Binary(nil), TypeBinary}, 687 | {Boolean(false), TypeBoolean}, 688 | {Collection(nil), TypeCollection}, 689 | {Integer(0), TypeInteger}, 690 | {Range{}, TypeRange}, 691 | {Resolution{}, TypeResolution}, 692 | {String(""), TypeString}, 693 | {TextWithLang{}, TypeTextWithLang}, 694 | {Time{time.Time{}}, TypeDateTime}, 695 | {Void{}, TypeVoid}, 696 | } 697 | 698 | for _, test := range tests { 699 | tp := test.v.Type() 700 | if tp != test.tp { 701 | t.Errorf("testing %s.Type:\n"+ 702 | "expected: %q\n"+ 703 | "present: %q\n", 704 | reflect.TypeOf(test.v).String(), 705 | test.tp, tp, 706 | ) 707 | } 708 | } 709 | } 710 | 711 | // TestValueEqualSimilar tests ValueEqual and ValueSimilar 712 | func TestValueEqualSimilar(t *testing.T) { 713 | tm1 := time.Now() 714 | tm2 := tm1.Add(time.Hour) 715 | 716 | type testData struct { 717 | v1, v2 Value // A pair of values 718 | equal bool // Expected ValueEqual(v1,v2) output 719 | similar bool // Expected ValueSimilar(v1,v2) output 720 | } 721 | 722 | tests := []testData{ 723 | // Simple types 724 | {Integer(0), Integer(0), true, true}, 725 | {Integer(0), Integer(1), false, false}, 726 | {Integer(0), String("hello"), false, false}, 727 | {Time{tm1}, Time{tm1}, true, true}, 728 | {Time{tm1}, Time{tm2}, false, false}, 729 | {Binary{}, Binary{}, true, true}, 730 | {Binary{}, Binary{1, 2, 3}, false, false}, 731 | {Binary{1, 2, 3}, Binary{4, 5, 6}, false, false}, 732 | {Binary("hello"), Binary("hello"), true, true}, 733 | {String("hello"), String("hello"), true, true}, 734 | {Binary("hello"), String("hello"), false, true}, 735 | {String("hello"), Binary("hello"), false, true}, 736 | 737 | // Collections 738 | // 739 | // Note, ValueEqual for Collection values is a thin wrapper 740 | // around Attributes.Equal. So the serious testing will be 741 | // performed there. Here we only test a couple of simple 742 | // cases. 743 | // 744 | // The same is true for ValueSimilar. 745 | {Collection{}, Collection{}, true, true}, 746 | 747 | { 748 | v1: Collection{ 749 | MakeAttr("attr1", TagInteger, Integer(1)), 750 | MakeAttr("attr2", TagString, String("hello")), 751 | }, 752 | 753 | v2: Collection{ 754 | MakeAttr("attr2", TagString, String("hello")), 755 | MakeAttr("attr1", TagInteger, Integer(1)), 756 | }, 757 | 758 | equal: false, 759 | similar: true, 760 | }, 761 | } 762 | 763 | for _, test := range tests { 764 | equal := ValueEqual(test.v1, test.v2) 765 | similar := ValueSimilar(test.v1, test.v2) 766 | 767 | if equal != test.equal { 768 | t.Errorf("testing ValueEqual:\n"+ 769 | "value 1: %s\n"+ 770 | "value 2: %s\n"+ 771 | "expected: %v\n"+ 772 | "present: %v\n", 773 | test.v1, test.v2, 774 | test.equal, equal, 775 | ) 776 | } 777 | 778 | if similar != test.similar { 779 | t.Errorf("testing ValueSimilar:\n"+ 780 | "value 1: %s\n"+ 781 | "value 2: %s\n"+ 782 | "expected: %v\n"+ 783 | "present: %v\n", 784 | test.v1, test.v2, 785 | test.similar, similar, 786 | ) 787 | } 788 | } 789 | } 790 | 791 | // TestValuesString tests Values.String function 792 | func TestValuesString(t *testing.T) { 793 | type testData struct { 794 | v Values // Input Values 795 | s string // Expected output 796 | } 797 | 798 | tests := []testData{ 799 | { 800 | v: nil, 801 | s: "[]", 802 | }, 803 | 804 | { 805 | v: Values{}, 806 | s: "[]", 807 | }, 808 | 809 | { 810 | v: Values{{TagInteger, Integer(5)}}, 811 | s: "5", 812 | }, 813 | 814 | { 815 | v: Values{{TagInteger, Integer(5)}, {TagEnum, Integer(6)}}, 816 | s: "[5,6]", 817 | }, 818 | } 819 | 820 | for _, test := range tests { 821 | s := test.v.String() 822 | if s != test.s { 823 | t.Errorf("testing Values.String:\n"+ 824 | "value: %#v\n"+ 825 | "expected: %q\n"+ 826 | "present: %q\n", 827 | test.v, test.s, s, 828 | ) 829 | } 830 | } 831 | } 832 | 833 | // TestValuesEqualSimilar tests Values.Equal and Values.Similar 834 | func TestValuesEqualSimilar(t *testing.T) { 835 | type testData struct { 836 | v1, v2 Values // A pair of values 837 | equal bool // Expected v1.Equal(v2) output 838 | similar bool // Expected v2.Similar(v2) output 839 | } 840 | 841 | tests := []testData{ 842 | { 843 | v1: nil, 844 | v2: nil, 845 | equal: true, 846 | similar: true, 847 | }, 848 | 849 | { 850 | v1: Values{}, 851 | v2: Values{}, 852 | equal: true, 853 | similar: true, 854 | }, 855 | 856 | { 857 | v1: Values{}, 858 | v2: nil, 859 | equal: false, 860 | similar: true, 861 | }, 862 | 863 | { 864 | v1: Values{}, 865 | v2: Values{{TagInteger, Integer(5)}}, 866 | equal: false, 867 | similar: false, 868 | }, 869 | 870 | { 871 | v1: Values{ 872 | {TagInteger, Integer(5)}, 873 | {TagEnum, Integer(6)}, 874 | }, 875 | v2: Values{ 876 | {TagInteger, Integer(5)}, 877 | {TagEnum, Integer(6)}, 878 | }, 879 | equal: true, 880 | similar: true, 881 | }, 882 | 883 | { 884 | v1: Values{ 885 | {TagInteger, Integer(5)}, 886 | {TagEnum, Integer(6)}}, 887 | v2: Values{ 888 | {TagInteger, Integer(5)}, 889 | {TagInteger, Integer(6)}}, 890 | equal: false, 891 | similar: false, 892 | }, 893 | 894 | { 895 | v1: Values{{TagInteger, Integer(6)}, {TagEnum, Integer(5)}}, 896 | v2: Values{{TagInteger, Integer(5)}, {TagEnum, Integer(6)}}, 897 | equal: false, 898 | similar: false, 899 | }, 900 | 901 | { 902 | v1: Values{ 903 | {TagString, String("hello")}, 904 | {TagString, Binary("world")}, 905 | }, 906 | v2: Values{ 907 | {TagString, String("hello")}, 908 | {TagString, Binary("world")}, 909 | }, 910 | equal: true, 911 | similar: true, 912 | }, 913 | 914 | { 915 | v1: Values{ 916 | {TagString, Binary("hello")}, 917 | {TagString, String("world")}, 918 | }, 919 | v2: Values{ 920 | {TagString, String("hello")}, 921 | {TagString, Binary("world")}, 922 | }, 923 | equal: false, 924 | similar: true, 925 | }, 926 | } 927 | 928 | for _, test := range tests { 929 | equal := test.v1.Equal(test.v2) 930 | similar := test.v1.Similar(test.v2) 931 | 932 | if equal != test.equal { 933 | t.Errorf("testing Values.Equal:\n"+ 934 | "values 1: %s\n"+ 935 | "values 2: %s\n"+ 936 | "expected: %v\n"+ 937 | "present: %v\n", 938 | test.v1, test.v2, 939 | test.equal, equal, 940 | ) 941 | } 942 | 943 | if similar != test.similar { 944 | t.Errorf("testing Values.Similar:\n"+ 945 | "values 1: %s\n"+ 946 | "values 2: %s\n"+ 947 | "expected: %v\n"+ 948 | "present: %v\n", 949 | test.v1, test.v2, 950 | test.similar, similar, 951 | ) 952 | } 953 | } 954 | } 955 | 956 | // TestValuesCopy tests Values.Clone and Values.DeepCopy methods 957 | func TestValuesCopy(t *testing.T) { 958 | values := Values{} 959 | values.Add(TagBoolean, Boolean(true)) 960 | values.Add(TagExtension, Binary{}) 961 | values.Add(TagString, Binary{1, 2, 3}) 962 | values.Add(TagInteger, Integer(123)) 963 | values.Add(TagEnum, Integer(-321)) 964 | values.Add(TagRange, Range{-100, 200}) 965 | values.Add(TagRange, Range{-100, -50}) 966 | values.Add(TagResolution, Resolution{150, 300, UnitsDpi}) 967 | values.Add(TagResolution, Resolution{100, 200, UnitsDpcm}) 968 | values.Add(TagResolution, Resolution{75, 150, 10}) 969 | values.Add(TagString, String("hello")) 970 | values.Add(TagTextLang, TextWithLang{"en-US", "hello"}) 971 | values.Add(TagDateTime, Time{time.Now()}) 972 | values.Add(TagNoValue, Void{}) 973 | values.Add(TagBeginCollection, 974 | Collection{MakeAttribute("test", TagString, String(""))}) 975 | 976 | type testData struct { 977 | values Values 978 | } 979 | 980 | tests := []testData{ 981 | {nil}, 982 | {Values{}}, 983 | {values}, 984 | } 985 | 986 | for _, test := range tests { 987 | clone := test.values.Clone() 988 | if !test.values.Equal(clone) { 989 | t.Errorf("testing Attributes.Clone\n"+ 990 | "expected: %#v\n"+ 991 | "present: %#v\n", 992 | test.values, 993 | clone, 994 | ) 995 | } 996 | 997 | copy := test.values.DeepCopy() 998 | if !test.values.Equal(copy) { 999 | t.Errorf("testing Attributes.DeepCopy\n"+ 1000 | "expected: %#v\n"+ 1001 | "present: %#v\n", 1002 | test.values, 1003 | copy, 1004 | ) 1005 | } 1006 | } 1007 | } 1008 | 1009 | // TestIntegerOrRange tests IntegerOrRange interface implementation 1010 | // by Integer and Range types 1011 | func TestIntegerOrRange(t *testing.T) { 1012 | type testData struct { 1013 | v IntegerOrRange // Value being tested 1014 | x int // IntegerOrRange.Within input 1015 | within bool // IntegerOrRange.Within expected output 1016 | } 1017 | 1018 | tests := []testData{ 1019 | { 1020 | v: Integer(5), 1021 | x: 5, 1022 | within: true, 1023 | }, 1024 | 1025 | { 1026 | v: Integer(5), 1027 | x: 6, 1028 | within: false, 1029 | }, 1030 | 1031 | { 1032 | v: Range{-5, 5}, 1033 | x: 0, 1034 | within: true, 1035 | }, 1036 | 1037 | { 1038 | v: Range{-5, 5}, 1039 | x: -5, 1040 | within: true, 1041 | }, 1042 | 1043 | { 1044 | v: Range{-5, 5}, 1045 | x: 5, 1046 | within: true, 1047 | }, 1048 | 1049 | { 1050 | v: Range{-5, 5}, 1051 | x: -6, 1052 | within: false, 1053 | }, 1054 | 1055 | { 1056 | v: Range{-5, 5}, 1057 | x: 6, 1058 | within: false, 1059 | }, 1060 | } 1061 | 1062 | for _, test := range tests { 1063 | within := test.v.Within(test.x) 1064 | if within != test.within { 1065 | t.Errorf("testing %s.Within:\n"+ 1066 | "value: %#v\n"+ 1067 | "param: %v\n"+ 1068 | "expected: %v\n"+ 1069 | "present: %v\n", 1070 | reflect.TypeOf(test.v).String(), 1071 | test.v, test.x, 1072 | test.within, within, 1073 | ) 1074 | } 1075 | } 1076 | } 1077 | 1078 | // TestCollection tests Collection.Add method 1079 | func TestCollectionAdd(t *testing.T) { 1080 | col1 := Collection{ 1081 | MakeAttr("attr1", TagInteger, Integer(1)), 1082 | MakeAttr("attr2", TagString, String("hello")), 1083 | } 1084 | 1085 | col2 := Collection{} 1086 | col2.Add(MakeAttr("attr1", TagInteger, Integer(1))) 1087 | col2.Add(MakeAttr("attr2", TagString, String("hello"))) 1088 | 1089 | if !ValueEqual(col1, col2) { 1090 | t.Errorf("Collection.Add test failed") 1091 | } 1092 | } 1093 | --------------------------------------------------------------------------------