diff -Nru golang-github-openprinting-goipp-1.0.0/attr.go golang-github-openprinting-goipp-1.1.0/attr.go --- golang-github-openprinting-goipp-1.0.0/attr.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/attr.go 2023-12-05 12:34:36.000000000 +0100 @@ -36,7 +36,8 @@ return true } -// Attribute represents a single attribute +// Attribute represents a single attribute, which consist of +// the Name and one or more Values type Attribute struct { Name string // Attribute name Values Values // Slice of values @@ -50,12 +51,12 @@ } // Equal checks that Attribute is equal to another Attribute -// (i.e., names are the same and values are equal +// (i.e., names are the same and values are equal) func (a Attribute) Equal(a2 Attribute) bool { return a.Name == a2.Name && a.Values.Equal(a2.Values) } -// Unpack attribute value +// Unpack attribute value from its wire representation func (a *Attribute) unpack(tag Tag, value []byte) error { var err error var val Value diff -Nru golang-github-openprinting-goipp-1.0.0/debian/changelog golang-github-openprinting-goipp-1.1.0/debian/changelog --- golang-github-openprinting-goipp-1.0.0/debian/changelog 2021-09-02 16:03:52.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/debian/changelog 2024-02-28 21:35:17.000000000 +0100 @@ -1,3 +1,10 @@ +golang-github-openprinting-goipp (1.1.0-0ubuntu1) noble; urgency=medium + + * New upstream release. + - Needed by ipp-usb 0.99.24 + + -- Till Kamppeter Wed, 28 Feb 2024 21:35:17 +0100 + golang-github-openprinting-goipp (1.0.0-2) unstable; urgency=medium * Remove myself from Uploaders diff -Nru golang-github-openprinting-goipp-1.0.0/decoder.go golang-github-openprinting-goipp-1.1.0/decoder.go --- golang-github-openprinting-goipp-1.0.0/decoder.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/decoder.go 2023-12-05 12:34:36.000000000 +0100 @@ -15,11 +15,27 @@ "io" ) -// Type messageDecoder represents Message decoder +// DecoderOptions represents message decoder options +type DecoderOptions struct { + // EnableWorkarounds, if set to true, enables various workarounds + // for decoding IPP messages that violate IPP protocol specification + // + // Currently it includes the following workarounds: + // * Pantum M7300FDW violates collection encoding rules. + // Instead of using TagMemberName, it uses named attributes + // within the collection + // + // The list of implemented workarounds may grow in the + // future + EnableWorkarounds bool +} + +// messageDecoder represents Message decoder type messageDecoder struct { - in io.Reader // Input stream - off int // Offset of last read - cnt int // Count of read bytes + in io.Reader // Input stream + off int // Offset of last read + cnt int // Count of read bytes + opt DecoderOptions // Options } // Decode the message @@ -52,10 +68,18 @@ var tag Tag tag, err = md.decodeTag() + if err != nil { + break + } + if tag.IsDelimiter() { prev = nil } + if tag.IsGroup() { + m.Groups.Add(Group{tag, nil}) + } + switch tag { case TagZero: err = errors.New("Invalid tag 0") @@ -109,12 +133,25 @@ case attr.Name == "": if prev != nil { prev.Values.Add(attr.Values[0].T, attr.Values[0].V) + + // Append value to the last Attribute of the + // last Group in the m.Groups + // + // Note, if we are here, this last Attribute definitely exists, + // because: + // * prev != nil + // * prev is set when new named attribute is added + // * prev is reset when delimiter tag is encountered + gLast := &m.Groups[len(m.Groups)-1] + aLast := &gLast.Attrs[len(gLast.Attrs)-1] + aLast.Values.Add(attr.Values[0].T, attr.Values[0].V) } else { err = errors.New("Additional value without preceding attribute") } case group != nil: group.Add(attr) prev = &(*group)[len(*group)-1] + m.Groups[len(m.Groups)-1].Add(attr) default: err = errors.New("Attribute without a group") } @@ -129,9 +166,35 @@ } // Decode a Collection +// +// Collection is like a nested object - an attribute which value is a sequence +// of named attributes. Collections can be nested. +// +// Wire format: +// ATTR: Tag = TagBeginCollection, - the outer attribute that +// Name = "name", value - ignored contains the collection +// +// ATTR: Tag = TagMemberName, name = "", - member name \ +// value - string, name of the next | +// member | repeated for +// | each member +// ATTR: Tag = any attribute tag, name = "", - repeated for | +// value = member value multi-value / +// members +// +// ATTR: Tag = TagEndCollection, name = "", +// value - ignored +// +// The format looks a bit baroque, but please note that it was added +// in the IPP 2.0. For IPP 1.x collection looks like a single multi-value +// TagBeginCollection attribute (attributes without names considered +// next value for the previously defined named attributes) and so +// 1.x parser silently ignores collections and doesn't get confused +// with them. func (md *messageDecoder) decodeCollection() (Collection, error) { collection := make(Collection, 0) - //var name string + + memberName := "" for { tag, err := md.decodeTag() @@ -141,20 +204,14 @@ // Delimiter cannot be inside a collection if tag.IsDelimiter() { - err = fmt.Errorf("collection: unexpected %s", tag) + err = fmt.Errorf("Collection: unexpected tag %s", tag) return nil, err } - // We are about to finish with current attribute (if any), - // either because we've got an end of collection, or a next - // attribute name. Check that we are leaving the current - // attribute in a consistent state (i.e., with at least one value) - if tag == TagMemberName || tag == TagEndCollection { - l := len(collection) - if l > 0 && len(collection[l-1].Values) == 0 { - err = fmt.Errorf("collection: unexpected %s, expected value tag", tag) - return nil, err - } + // Check for TagMemberName without the subsequent value attribute + if (tag == TagMemberName || tag == TagEndCollection) && memberName != "" { + err = fmt.Errorf("Collection: unexpected %s, expected value tag", tag) + return nil, err } // Fetch next attribute @@ -164,36 +221,48 @@ } // Process next attribute - switch { - case tag == TagEndCollection: + switch tag { + case TagEndCollection: return collection, nil - case tag == TagMemberName: - attr.Name = string(attr.Values[0].V.(String)) - attr.Values = nil - - if attr.Name == "" { - err = fmt.Errorf("collection: %s contains empty attribute name", tag) + case TagMemberName: + memberName = string(attr.Values[0].V.(String)) + if memberName == "" { + err = fmt.Errorf("Collection: %s value is empty", tag) return nil, err } - collection = append(collection, attr) - - case len(collection) == 0: - // We've got a value without preceding TagMemberName - err = fmt.Errorf("collection: unexpected %s, expected %s", tag, TagMemberName) - return nil, err + case TagBeginCollection: + // Decode nested collection + attr.Values[0].V, err = md.decodeCollection() + if err != nil { + return nil, err + } + fallthrough default: - if tag == TagBeginCollection { - attr.Values[0].V, err = md.decodeCollection() - if err != nil { - return nil, err - } + if md.opt.EnableWorkarounds && + memberName == "" && attr.Name != "" { + // Workaround for: Pantum M7300FDW + // + // This device violates collection encoding rules. + // Instead of using TagMemberName, it uses named + // attributes within the collection + memberName = attr.Name } - l := len(collection) - collection[l-1].Values.Add(tag, attr.Values[0].V) + if memberName != "" { + attr.Name = memberName + collection = append(collection, attr) + memberName = "" + } else if len(collection) > 0 { + l := len(collection) + collection[l-1].Values.Add(tag, attr.Values[0].V) + } else { + // We've got a value without preceding TagMemberName + err = fmt.Errorf("Collection: unexpected %s, expected %s", tag, TagMemberName) + return nil, err + } } } } @@ -201,6 +270,7 @@ // Decode a tag func (md *messageDecoder) decodeTag() (Tag, error) { t, err := md.decodeU8() + return Tag(t), err } @@ -217,6 +287,14 @@ } // Decode a single attribute +// +// Wire format: +// 1 byte: Tag +// 2+N bytes: Name length (2 bytes) + name string +// 2+N bytes: Value length (2 bytes) + value bytes +// +// For the extended tag format, Tag is encoded as TagExtension and +// 4 bytes of the actual tag value prepended to the value bytes func (md *messageDecoder) decodeAttribute(tag Tag) (Attribute, error) { var attr Attribute var value []byte @@ -320,8 +398,11 @@ if n > 0 { md.cnt += n data = data[n:] - } else if err != nil { + } else { md.off = md.cnt + if err == nil || err == io.EOF { + err = errors.New("Message truncated") + } return err } diff -Nru golang-github-openprinting-goipp-1.0.0/doc.go golang-github-openprinting-goipp-1.1.0/doc.go --- golang-github-openprinting-goipp-1.0.0/doc.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/doc.go 2023-12-05 12:34:36.000000000 +0100 @@ -23,7 +23,7 @@ in response. So most of operations are common for request and response messages -Example: +# Example (Get-Printer-Attributes): package main import ( @@ -72,5 +72,97 @@ respMsg.Print(os.Stdout, false) } + +# Example (Print PDF file): + + package main + + import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "os" + + "github.com/OpenPrinting/goipp" + ) + + const ( + PrinterURL = "http://192.168.1.102:631/ipp/print" + TestPage = "onepage-a4.pdf" + ) + + type T int + + // checkErr checks for an error. If err != nil, it prints error + // message and exits + func checkErr(err error, format string, args ...interface{}) { + if err != nil { + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err) + os.Exit(1) + } + } + + // ExamplePrintPDF demo + func main() { + // Build and encode IPP request + req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1) + req.Operation.Add(goipp.MakeAttribute("attributes-charset", + goipp.TagCharset, goipp.String("utf-8"))) + req.Operation.Add(goipp.MakeAttribute("attributes-natural-language", + goipp.TagLanguage, goipp.String("en-US"))) + req.Operation.Add(goipp.MakeAttribute("printer-uri", + goipp.TagURI, goipp.String(PrinterURL))) + req.Operation.Add(goipp.MakeAttribute("requesting-user-name", + goipp.TagName, goipp.String("John Doe"))) + req.Operation.Add(goipp.MakeAttribute("job-name", + goipp.TagName, goipp.String("job name"))) + req.Operation.Add(goipp.MakeAttribute("document-format", + goipp.TagMimeType, goipp.String("application/pdf"))) + + payload, err := req.EncodeBytes() + checkErr(err, "IPP encode") + + // Open document file + file, err := os.Open(TestPage) + checkErr(err, "Open document file") + + defer file.Close() + + // Build HTTP request + body := io.MultiReader(bytes.NewBuffer(payload), file) + + httpReq, err := http.NewRequest(http.MethodPost, PrinterURL, body) + checkErr(err, "HTTP") + + httpReq.Header.Set("content-type", goipp.ContentType) + httpReq.Header.Set("accept", goipp.ContentType) + httpReq.Header.Set("accept-encoding", "gzip, deflate, identity") + + // Execute HTTP request + httpRsp, err := http.DefaultClient.Do(httpReq) + if httpRsp != nil { + defer httpRsp.Body.Close() + } + + checkErr(err, "HTTP") + + if httpRsp.StatusCode/100 != 2 { + checkErr(errors.New(httpRsp.Status), "HTTP") + } + + // Decode IPP response + rsp := &goipp.Message{} + err = rsp.Decode(httpRsp.Body) + checkErr(err, "IPP decode") + + if goipp.Status(rsp.Code) != goipp.StatusOk { + err = errors.New(goipp.Status(rsp.Code).String()) + checkErr(err, "IPP") + } + } + */ package goipp diff -Nru golang-github-openprinting-goipp-1.0.0/encoder.go golang-github-openprinting-goipp-1.1.0/encoder.go --- golang-github-openprinting-goipp-1.0.0/encoder.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/encoder.go 2023-12-05 12:34:36.000000000 +0100 @@ -42,9 +42,9 @@ // Encode attributes for _, grp := range m.attrGroups() { - err = me.encodeTag(grp.tag) + err = me.encodeTag(grp.Tag) if err == nil { - for _, attr := range grp.attrs { + for _, attr := range grp.Attrs { if attr.Name == "" { err = errors.New("Attribute without name") } else { @@ -88,9 +88,19 @@ if tag.IsDelimiter() || tag == TagMemberName || tag == TagEndCollection { return fmt.Errorf("Tag %s cannot be used with value", tag) } + + if uint(tag)&0x80000000 != 0 { + return fmt.Errorf("Tag %s exceeds extension tag range", tag) + } + } + + var err error + if tag >= 0x100 { + err = me.encodeTag(TagExtension) + } else { + err = me.encodeTag(tag) } - err := me.encodeTag(tag) if err != nil { return err } @@ -150,30 +160,37 @@ func (me *messageEncoder) encodeValue(tag Tag, v Value) error { // Check Value type vs the Tag tagType := tag.Type() - switch tagType { - case TypeInvalid: - return fmt.Errorf("Tag %s cannot be used for value", tag) - case TypeVoid: + if tagType == TypeVoid { v = Void{} // Ignore supplied value - default: - if tagType != v.Type() { - return fmt.Errorf("Tag %s: %s value required, %s present", - tag, tagType, v.Type()) - } + } else if tagType != v.Type() { + return fmt.Errorf("Tag %s: %s value required, %s present", + tag, tagType, v.Type()) } // Encode the value + // + // If tag >= 0x100, tag is replaced with TagExtension, and actual + // tag value prepended to the data bytes. See RFC 8010, 3.5.2 for + // details data, err := v.encode() if err != nil { return err } - if len(data) > math.MaxInt16 { + valueLen := len(data) + if tag >= 0x100 { + valueLen += 4 // Prepend extension tag value to the data + } + + if valueLen > math.MaxInt16 { return fmt.Errorf("Attribute value exceeds %d bytes", math.MaxInt16) } - err = me.encodeU16(uint16(len(data))) + err = me.encodeU16(uint16(valueLen)) + if err == nil && tag >= 0x100 { + err = me.encodeU32(uint32(tag)) + } if err == nil { err = me.write(data) } diff -Nru golang-github-openprinting-goipp-1.0.0/goipp_test.go golang-github-openprinting-goipp-1.1.0/goipp_test.go --- golang-github-openprinting-goipp-1.0.0/goipp_test.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/goipp_test.go 2023-12-05 12:34:36.000000000 +0100 @@ -8,12 +8,52 @@ import ( "bytes" + "errors" + "fmt" + "io" "io/ioutil" "reflect" + "strings" "testing" "time" ) +// errWriter implements io.Writer interface +// +// it accepts some first bytes and after that always returns an error +type errWriter struct{ skip int } + +var _ = io.Writer(&errWriter{}) + +func (ewr *errWriter) Write(data []byte) (int, error) { + if len(data) <= ewr.skip { + ewr.skip -= len(data) + return len(data), nil + } + + n := ewr.skip + ewr.skip = 0 + + return n, errors.New("I/O error") +} + +// errValue implements Value interface and returns +// error from its encode() and decode() methods +type errValue struct{} + +var _ = Value(errValue{}) + +func (errValue) String() string { return "" } +func (errValue) Type() Type { return TypeInteger } + +func (errValue) encode() ([]byte, error) { + return nil, errors.New("encode error") +} + +func (errValue) decode([]byte) (Value, error) { + return nil, errors.New("decode error") +} + // Check that err == nil func assertNoError(t *testing.T, err error) { if err != nil { @@ -28,6 +68,20 @@ } } +// Check that err != nil and contains expected test +func assertErrorIs(t *testing.T, err error, s string) { + if err == nil { + if s != "" { + t.Errorf("Error expected") + } + return + } + + if !strings.HasPrefix(err.Error(), s) { + t.Errorf("Error is %q, expected %q", err, s) + } +} + // Check that value type is as specified func assertValueType(t *testing.T, val Value, typ Type) { if val.Type() != typ { @@ -66,6 +120,29 @@ } } +// parseTime parses time given in time.Layout format. +// this function panics if time cannot be parsed +func parseTime(s string) Time { + t, err := time.Parse(time.Layout, s) + if err != nil { + panic(fmt.Sprintf("parseTime(%q): %s", s, err)) + } + return Time{t} +} + +// Test String() methods for various types +func TestString(t *testing.T) { + // Here we test that T.String() doesn't crash for + // values out of range + for i := -1000; i <= 10000; i++ { + _ = Op(i).String() + _ = Status(i).String() + _ = Type(i).String() + _ = Tag(i).String() + _ = Units(i).String() + } +} + // Test Void Value func TestVoidValue(t *testing.T) { var v Void @@ -220,6 +297,31 @@ } } +// Test (Message) Equal() +func TestMessageEqual(t *testing.T) { + var m1, m2 Message + // Test: Version, Code, RequestID mismatch makes messages non-equal + m1 = Message{Version: 0, Code: 1, RequestID: 2} + + m2 = m1 + m2.Version++ + if m1.Equal(m2) { + t.Errorf("(Message) Equal(): ignores difference in Version") + } + + m2 = m1 + m2.Code++ + if m1.Equal(m2) { + t.Errorf("(Message) Equal(): ignores difference in Code") + } + + m2 = m1 + m2.RequestID++ + if m1.Equal(m2) { + t.Errorf("(Message) Equal(): ignores difference in RequestID") + } +} + // Test Version func TestVersion(t *testing.T) { v := MakeVersion(1, 2) @@ -231,10 +333,744 @@ } } +// testEncodeDecodeMessage creates a quite complex message +// for Encode/Decode test +func testEncodeDecodeMessage() *Message { + m := &Message{ + Version: DefaultVersion, + Code: 0x1234, + RequestID: 0x87654321, + } + + // Populate all groups + m.Operation.Add(MakeAttribute("grp_operation", TagInteger, + Integer(1))) + m.Job.Add(MakeAttribute("grp_job", TagInteger, + Integer(2))) + m.Printer.Add(MakeAttribute("grp_printer", TagInteger, + Integer(3))) + m.Unsupported.Add(MakeAttribute("grp_unsupported", TagInteger, + Integer(4))) + m.Subscription.Add(MakeAttribute("grp_subscription", TagInteger, + Integer(5))) + m.EventNotification.Add(MakeAttribute("grp_eventnotification", TagInteger, + Integer(6))) + m.Resource.Add(MakeAttribute("grp_resource", TagInteger, + Integer(7))) + m.Document.Add(MakeAttribute("grp_document", TagInteger, + Integer(8))) + m.System.Add(MakeAttribute("grp_system", TagInteger, + Integer(9))) + m.Future11.Add(MakeAttribute("grp_future11", TagInteger, + Integer(10))) + m.Future12.Add(MakeAttribute("grp_future12", TagInteger, + Integer(11))) + m.Future13.Add(MakeAttribute("grp_future13", TagInteger, + Integer(12))) + m.Future14.Add(MakeAttribute("grp_future14", TagInteger, + Integer(13))) + m.Future15.Add(MakeAttribute("grp_future15", TagInteger, + Integer(14))) + + // Use all possible attribute types + m.Operation.Add(MakeAttribute("type_integer", TagInteger, Integer(123))) + + m.Operation.Add(MakeAttribute("type_boolean_t", TagBoolean, Boolean(true))) + m.Operation.Add(MakeAttribute("type_boolean_f", TagBoolean, Boolean(false))) + + m.Operation.Add(MakeAttribute("type_void", TagUnsupportedValue, Void{})) + + m.Operation.Add(MakeAttribute("type_string_1", TagText, String("hello"))) + m.Operation.Add(MakeAttribute("type_string_2", TagText, String(""))) + + m.Operation.Add(MakeAttribute("type_time_1", TagDateTime, + parseTime("01/02 03:04:05PM '06 -0700"))) + m.Operation.Add(MakeAttribute("type_time_2", TagDateTime, + parseTime("01/02 03:04:05PM '06 +0700"))) + m.Operation.Add(MakeAttribute("type_time_3", TagDateTime, + parseTime("01/02 03:04:05PM '06 -0730"))) + + m.Operation.Add(MakeAttribute("type_resolution_1", TagResolution, + Resolution{123, 456, UnitsDpi})) + m.Operation.Add(MakeAttribute("type_resolution_2", TagResolution, + Resolution{78, 90, UnitsDpcm})) + + m.Operation.Add(MakeAttribute("type_range", TagRange, + Range{100, 1000})) + + m.Operation.Add(MakeAttribute("type_textlang_1", TagTextLang, + TextWithLang{"hello", "en"})) + m.Operation.Add(MakeAttribute("type_textlang_1", TagTextLang, + TextWithLang{"привет", "ru"})) + + return m +} + +// Test Message Encode, then Decode +func TestEncodeDecode(t *testing.T) { + m1 := testEncodeDecodeMessage() + + data, err := m1.EncodeBytes() + assertNoError(t, err) + + m2 := &Message{} + err = m2.DecodeBytes(data) + assertNoError(t, err) +} + +// Test encode errors +func TestEncodeErrors(t *testing.T) { + // Attribute without name + m := NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a := MakeAttribute("attr", TagInteger, Integer(123)) + a.Name = "" + m.Operation.Add(a) + err := m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Attribute without name") + + // Attribute without value + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagInteger, Integer(123)) + a.Values = nil + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Attribute without value") + + // Attribute name exceeds... + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagInteger, Integer(123)) + a.Name = strings.Repeat("x", 32767) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertNoError(t, err) + + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagInteger, Integer(123)) + a.Name = strings.Repeat("x", 32767+1) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Attribute name exceeds 32767 bytes") + + // Attribute value exceeds... + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagText, String(strings.Repeat("x", 32767))) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertNoError(t, err) + + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagText, String(strings.Repeat("x", 32767+1))) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Attribute value exceeds 32767 bytes") + + // Tag XXX cannot be used with value + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagJobGroup, Integer(123)) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Tag job-attributes-tag cannot be used with value") + + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagMemberName, Integer(123)) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Tag memberAttrName cannot be used with value") + + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagEndCollection, Integer(123)) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Tag endCollection cannot be used with value") + + // Collection member without name + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagBeginCollection, Collection{ + MakeAttribute("", TagInteger, Integer(123)), + }) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Collection member without name") + + // Tag XXX: YYY value required, ZZZ present + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagText, Integer(123)) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "Tag textWithoutLanguage: String value required, Integer present") + + // I/O error + m = testEncodeDecodeMessage() + + data, err := m.EncodeBytes() + assertNoError(t, err) + + for skip := 0; skip < len(data); skip++ { + err = m.Encode(&errWriter{skip}) + assertErrorIs(t, err, "I/O error") + } + + // encode error + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagInteger, errValue{}) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "encode error") + + m = NewRequest(DefaultVersion, OpGetPrinterAttributes, 0x12345678) + a = MakeAttribute("attr", TagBeginCollection, Collection{ + MakeAttribute("attr", TagInteger, errValue{}), + }) + m.Operation.Add(a) + err = m.Encode(ioutil.Discard) + assertErrorIs(t, err, "encode error") +} + +// Test decode errors +func TestDecodeErrors(t *testing.T) { + var d []byte + var err error + var m = &Message{} + + hdr := []byte{ + 0x01, 0x01, // IPP version + 0x00, 0x02, // Print-Job operation + 0x01, 0x02, 0x03, 0x04, // Request ID + } + + body := []byte{} + + // Message truncated + body = []byte{ + uint8(TagJobGroup), + uint8(TagInteger), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Message truncated at") + + d, err = testEncodeDecodeMessage().EncodeBytes() + assertNoError(t, err) + + for i := 0; i < len(d); i++ { + err = m.DecodeBytes(d[:i]) + assertErrorIs(t, err, "Message truncated at") + } + + d = goodMessage1 + for i := 0; i < len(d); i++ { + err = m.DecodeBytes(d[:i]) + assertErrorIs(t, err, "Message truncated at") + } + + // Invalid tag 0 + d = append(hdr, 0) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Invalid tag 0 at 0x8") + + // Attribute without a group + body = []byte{ + uint8(TagInteger), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, uint8(TagJobGroup)) + d = append(d, body...) + err = m.DecodeBytes(d) + assertNoError(t, err) + + err = m.DecodeBytes(append(hdr, body...)) + assertErrorIs(t, err, "Attribute without a group at") + + // Additional value without preceding attribute + body = []byte{ + uint8(TagJobGroup), + uint8(TagInteger), + 0x00, 0x00, // No name + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + err = m.DecodeBytes(append(hdr, body...)) + assertErrorIs(t, err, "Additional value without preceding attribute") + + // "Unexpected tag XXX" + for _, tag := range []Tag{TagMemberName, TagEndCollection} { + body = []byte{ + uint8(TagJobGroup), + uint8(TagInteger), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + } + + d = append(hdr, body...) + d = append(d, uint8(tag)) + d = append(d, uint8(TagEnd)) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Unexpected tag") + } + + // Collection: unexpected tag XXX + for tag := TagZero; tag.IsDelimiter(); tag++ { + body = []byte{ + uint8(TagJobGroup), + + uint8(TagBeginCollection), + 0x00, 0x0a, // Name length + name + 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', + 0x00, 0x00, // No value + + uint8(TagMemberName), + 0x00, 0x00, // No name + 0x00, 0x06, // Value length + value + 'm', 'e', 'm', 'b', 'e', 'r', + + uint8(TagInteger), + 0x00, 0x00, // No name + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + + uint8(tag), + + uint8(TagEndCollection), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + } + + d = append(hdr, body...) + d = append(d, uint8(TagEnd)) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Collection: unexpected tag") + } + + // Collection: unexpected endCollection, expected value tag + body = []byte{ + uint8(TagJobGroup), + + uint8(TagBeginCollection), + 0x00, 0x0a, // Name length + name + 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', + 0x00, 0x00, // No value + + uint8(TagMemberName), + 0x00, 0x00, // No name + 0x00, 0x06, // Value length + value + 'm', 'e', 'm', 'b', 'e', 'r', + + uint8(TagEndCollection), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Collection: unexpected endCollection, expected value tag") + + // Collection: unexpected memberAttrName, expected value tag + body = []byte{ + uint8(TagJobGroup), + + uint8(TagBeginCollection), + 0x00, 0x0a, // Name length + name + 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', + 0x00, 0x00, // No value + + uint8(TagMemberName), + 0x00, 0x00, // No name + 0x00, 0x07, // Value length + value + 'm', 'e', 'm', 'b', 'e', 'r', '1', + + uint8(TagMemberName), + 0x00, 0x00, // No name + 0x00, 0x07, // Value length + value + 'm', 'e', 'm', 'b', 'e', 'r', '2', + + uint8(TagInteger), + 0x00, 0x00, // No name + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + + uint8(TagEndCollection), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Collection: unexpected memberAttrName, expected value tag") + + // Collection: memberAttrName value is empty + body = []byte{ + uint8(TagJobGroup), + + uint8(TagBeginCollection), + 0x00, 0x0a, // Name length + name + 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', + 0x00, 0x00, // No value + + uint8(TagMemberName), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + + uint8(TagInteger), + 0x00, 0x00, // No name + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + + uint8(TagEndCollection), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Collection: memberAttrName value is empty") + + // Collection: unexpected integer, expected memberAttrName + body = []byte{ + uint8(TagJobGroup), + + uint8(TagBeginCollection), + 0x00, 0x0a, // Name length + name + 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', + 0x00, 0x00, // No value + + //uint8(TagMemberName), + //0x00, 0x00, // No name + //0x00, 0x06, // Value length + value + //'m', 'e', 'm', 'b', 'e', 'r', + + uint8(TagInteger), + 0x00, 0x00, // No name + 0x00, 0x04, // Value length + value + 0x00, 0x00, 0x54, 0x56, + + uint8(TagEndCollection), + 0x00, 0x00, // No name + 0x00, 0x00, // No value + + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Collection: unexpected integer, expected memberAttrName") +} + +// Test errors in decoding values +func TestDecodeValueErrors(t *testing.T) { + var d []byte + var err error + var m = &Message{} + + hdr := []byte{ + 0x01, 0x01, // IPP version + 0x00, 0x02, // Print-Job operation + 0x01, 0x02, 0x03, 0x04, // Request ID + } + + body := []byte{} + + // integer: value must be 4 bytes + body = []byte{ + uint8(TagJobGroup), + uint8(TagInteger), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "integer: value must be 4 bytes") + + // boolean: value must be 1 byte + body = []byte{ + uint8(TagJobGroup), + uint8(TagBoolean), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "boolean: value must be 1 byte") + + // dateTime: value must be 11 bytes + body = []byte{ + uint8(TagJobGroup), + uint8(TagDateTime), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "dateTime: value must be 11 bytes") + + // dateTime: bad XXX + var datetest = []struct { + in []byte + err string + }{ + // year month day hour min sec s/10 + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0x32, 0x05, '+', 0x04, 0x00}, + ""}, + {[]byte{0x07, 0xe7, 0xff, 0x15, 0x11, 0x23, 0x32, 0x05, '+', 0x04, 0x00}, + "dateTime: bad month 255"}, + {[]byte{0x07, 0xe7, 0x02, 0xff, 0x11, 0x23, 0x32, 0x05, '+', 0x04, 0x00}, + "dateTime: bad day 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0xff, 0x23, 0x32, 0x05, '+', 0x04, 0x00}, + "dateTime: bad hours 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0xff, 0x32, 0x05, '+', 0x04, 0x00}, + "dateTime: bad minutes 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0xff, 0x05, '+', 0x04, 0x00}, + "dateTime: bad seconds 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0x32, 0xff, '+', 0x04, 0x00}, + "dateTime: bad deciseconds 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0x32, 0x05, '?', 0x04, 0x00}, + "dateTime: bad UTC sign"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0x32, 0x05, '-', 0xff, 0x00}, + "dateTime: bad UTC hours 255"}, + {[]byte{0x07, 0xe7, 0x02, 0x15, 0x11, 0x23, 0x32, 0x05, '-', 0x04, 0xff}, + "dateTime: bad UTC minutes 255"}, + } + + for _, test := range datetest { + body = []byte{ + uint8(TagJobGroup), + uint8(TagDateTime), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x0b, // Value length + value + } + + d = append(hdr, body...) + d = append(d, test.in...) + d = append(d, uint8(TagEnd)) + + err = m.DecodeBytes(d) + assertErrorIs(t, err, test.err) + } + + // resolution: value must be 9 bytes + body = []byte{ + uint8(TagJobGroup), + uint8(TagResolution), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "resolution: value must be 9 bytes ") + + // rangeOfInteger: value must be 8 bytes + body = []byte{ + uint8(TagJobGroup), + uint8(TagRange), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "rangeOfInteger: value must be 8 bytes") + + // textWithLanguage: truncated language length + body = []byte{ + uint8(TagJobGroup), + uint8(TagTextLang), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x01, // Value length + 0x00, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "textWithLanguage: truncated language length") + + // textWithLanguage: truncated language name + body = []byte{ + uint8(TagJobGroup), + uint8(TagTextLang), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + 0x00, 0x02, // Language length + 'e', + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "textWithLanguage: truncated language name") + + // textWithLanguage: truncated text length + body = []byte{ + uint8(TagJobGroup), + uint8(TagTextLang), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x05, // Value length + 0x00, 0x02, // Language length + 'e', 'n', // Language name + 0x00, // Text length + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "textWithLanguage: truncated text length") + + // textWithLanguage: truncated text string + body = []byte{ + uint8(TagJobGroup), + uint8(TagTextLang), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x09, // Value length + 0x00, 0x02, // Language length + 'e', 'n', // Language name + 0x00, 0x05, // Text length + 'h', 'e', 'l', + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "textWithLanguage: truncated text string") + + // textWithLanguage: extra 2 bytes at the end of value + body = []byte{ + uint8(TagJobGroup), + uint8(TagTextLang), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x0d, // Value length + 0x00, 0x02, // Language length + 'e', 'n', // Language name + 0x00, 0x05, // Text length + 'h', 'e', 'l', 'l', 'o', // Test string + '?', '?', // Extra 2 bytes + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "textWithLanguage: extra 2 bytes at the end of value") +} + +// Test TagExtension +func TestTagExtension(t *testing.T) { + // Ensure extension tag encodes and decodes well + m1 := NewResponse(DefaultVersion, StatusOk, 0x12345678) + m1.Operation.Add(MakeAttribute("attr", 0x12345678, + Binary{1, 2, 3, 4, 5})) + + data, err := m1.EncodeBytes() + assertNoError(t, err) + + m2 := Message{} + err = m2.DecodeBytes(data) + assertNoError(t, err) + + if !m1.Equal(m2) { + t.Errorf("Message is not the same after encoding and decoding") + } + + // Tag can't exceed 0x7fffffff, check that encoder validates it + m1 = NewResponse(DefaultVersion, StatusOk, 0x12345678) + tmp := uint32(0x81234567) + m1.Operation.Add(MakeAttribute("attr", Tag(tmp), + Binary{1, 2, 3, 4, 5})) + + _, err = m1.EncodeBytes() + assertErrorIs(t, err, "Tag 0x81234567 exceeds extension tag range") + + // Now prepare to decoder tests + var d []byte + var m = &Message{} + + hdr := []byte{ + 0x01, 0x01, // IPP version + 0x00, 0x02, // Print-Job operation + 0x01, 0x02, 0x03, 0x04, // Request ID + } + + body := []byte{} + + // Extension tag truncated + body = []byte{ + uint8(TagJobGroup), + uint8(TagExtension), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x03, // Value length + value + 0x00, 0x54, 0x56, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Extension tag truncated") + + // Extension tag out of range + body = []byte{ + uint8(TagJobGroup), + uint8(TagExtension), + 0x00, 0x04, // Name length + name + 'a', 't', 't', 'r', + 0x00, 0x08, // Value length + value + 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, + uint8(TagEnd), + } + + d = append(hdr, body...) + err = m.DecodeBytes(d) + assertErrorIs(t, err, "Extension tag out of range") +} + // Test message decoding -func testDecode(t *testing.T, data []byte, mustFail bool) { +func testDecode(t *testing.T, data []byte, opt DecoderOptions, + mustFail, mustEncode bool) { + var m Message - err := m.Decode(bytes.NewBuffer(data)) + err := m.DecodeEx(bytes.NewBuffer(data), opt) if mustFail { assertWithError(t, err) @@ -246,15 +1082,19 @@ return } + //m.Print(os.Stdout, true) + if !m.Equal(m) { t.Errorf("Message is not equal to itself") } - buf, err := m.EncodeBytes() - assertNoError(t, err) + if mustEncode { + buf, err := m.EncodeBytes() + assertNoError(t, err) - if !bytes.Equal(buf, data) { - t.Errorf("Message is not the same after decoding and encoding") + if !bytes.Equal(buf, data) { + t.Errorf("Message is not the same after decoding and encoding") + } } // We can't test a lot of (*Message) Print(), so lets test @@ -263,24 +1103,34 @@ } func TestDecodeGoodMessage1(t *testing.T) { - testDecode(t, good_message_1, false) + testDecode(t, goodMessage1, DecoderOptions{}, false, true) } func TestDecodeGoodMessage2(t *testing.T) { - testDecode(t, good_message_2, false) + testDecode(t, goodMessage2, DecoderOptions{}, false, true) } func TestDecodeBadMessage1(t *testing.T) { - testDecode(t, bad_message_1, true) + testDecode(t, badMessage1, DecoderOptions{}, true, false) + testDecode(t, badMessage1, + DecoderOptions{EnableWorkarounds: true}, false, false) } -func TestDecodeBigMessage(t *testing.T) { - testDecode(t, big_message, false) +func TestDecodeHPOfficeJetPro8730(t *testing.T) { + testDecode(t, attrsHPOfficeJetPro8730, DecoderOptions{}, false, true) +} + +func TestDecodePantumM7300FDW(t *testing.T) { + testDecode(t, attrsPantumM7300FDW, + DecoderOptions{EnableWorkarounds: false}, true, false) + + testDecode(t, attrsPantumM7300FDW, + DecoderOptions{EnableWorkarounds: true}, false, false) } // ------------------------ Test Data ------------------------ // The good message - 1 -var good_message_1 = []byte{ +var goodMessage1 = []byte{ 0x01, 0x01, // IPP version 0x00, 0x02, // Print-Job operation 0x00, 0x00, 0x00, 0x01, // Request ID @@ -439,7 +1289,7 @@ } // The good message - 2 -var good_message_2 = []byte{ +var goodMessage2 = []byte{ 0x01, 0x01, // IPP version 0x00, 0x02, // Print-Job operation 0x00, 0x00, 0x00, 0x01, // Request ID @@ -464,7 +1314,12 @@ } // The bad message - 1 -var bad_message_1 = []byte{ +// +// This message violates IPP encoding rules: instead of +// using TagMemberName it uses named attributes +// +// It must not decode normally, but must decode with workarounds +var badMessage1 = []byte{ 0x01, 0x01, // IPP version */ 0x00, 0x02, // Print-Job operation */ 0x00, 0x00, 0x00, 0x01, // Request ID */ @@ -532,7 +1387,7 @@ // The big real example, Get-Printer-Attributes output // from the HP OfficeJet Pro 8730 -var big_message = []byte{ +var attrsHPOfficeJetPro8730 = []byte{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x47, 0x00, 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x00, 0x05, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x48, 0x00, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, @@ -1500,3 +2355,133 @@ 0x69, 0x70, 0x6c, 0x65, 0x2d, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2d, 0x6a, 0x6f, 0x62, 0x73, 0x2d, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x00, 0x01, 0x00, 0x03, } + +// Get-Printer-Attributes output from Pantum M7300FDW +var attrsPantumM7300FDW = []byte{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x47, 0x00, 0x12, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2d, 0x63, + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x00, 0x05, 0x75, 0x74, 0x66, 0x2d, + 0x38, 0x48, 0x00, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2d, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x61, 0x6c, 0x2d, 0x6c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, 0x05, 0x65, 0x6e, 0x2d, + 0x55, 0x53, 0x04, 0x22, 0x00, 0x0f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x2d, + 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x00, 0x01, 0x00, + 0x49, 0x00, 0x19, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2d, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x2d, 0x73, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, + 0x75, 0x72, 0x66, 0x49, 0x00, 0x00, 0x00, 0x10, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x2f, 0x70, 0x77, 0x67, 0x2d, 0x72, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x49, 0x00, 0x00, 0x00, 0x18, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6f, 0x63, 0x74, 0x65, 0x74, 0x2d, 0x73, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x41, 0x00, 0x10, 0x6d, 0x6f, 0x70, 0x72, + 0x69, 0x61, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x65, 0x64, + 0x00, 0x03, 0x31, 0x2e, 0x33, 0x41, 0x00, 0x10, 0x70, 0x72, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x2d, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x00, 0x0e, 0x70, 0x61, 0x6e, 0x74, 0x75, 0x6d, 0x20, 0x70, 0x72, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x41, 0x00, 0x0c, 0x70, 0x72, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x00, 0x12, 0x50, 0x61, 0x6e, + 0x74, 0x75, 0x6d, 0x20, 0x49, 0x50, 0x50, 0x20, 0x70, 0x72, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x45, 0x00, 0x11, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x2d, 0x6d, 0x6f, 0x72, 0x65, 0x2d, 0x69, 0x6e, 0x66, 0x6f, 0x00, + 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x36, 0x30, 0x30, 0x30, 0x30, 0x2f, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2d, 0x6a, 0x75, 0x6d, 0x70, 0x2e, 0x68, + 0x74, 0x6d, 0x6c, 0x41, 0x00, 0x16, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x2d, 0x6d, 0x61, 0x6b, 0x65, 0x2d, 0x61, 0x6e, 0x64, 0x2d, 0x6d, + 0x6f, 0x64, 0x65, 0x6c, 0x00, 0x16, 0x50, 0x61, 0x6e, 0x74, 0x75, 0x6d, + 0x20, 0x4d, 0x37, 0x33, 0x30, 0x30, 0x46, 0x44, 0x57, 0x20, 0x53, 0x65, + 0x72, 0x69, 0x65, 0x73, 0x41, 0x00, 0x11, 0x70, 0x72, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x69, 0x64, + 0x00, 0x45, 0x4d, 0x46, 0x47, 0x3a, 0x50, 0x61, 0x6e, 0x74, 0x75, 0x6d, + 0x3b, 0x43, 0x4d, 0x44, 0x3a, 0x3a, 0x44, 0x57, 0x2d, 0x50, 0x53, 0x2c, + 0x44, 0x57, 0x2d, 0x50, 0x43, 0x4c, 0x2c, 0x55, 0x52, 0x46, 0x3b, 0x4d, + 0x44, 0x4c, 0x3a, 0x2d, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x3b, + 0x43, 0x4c, 0x53, 0x3a, 0x50, 0x52, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x3b, + 0x44, 0x45, 0x53, 0x3a, 0x50, 0x61, 0x6e, 0x74, 0x75, 0x6d, 0x3b, 0x45, + 0x00, 0x0c, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x75, 0x75, + 0x69, 0x64, 0x00, 0x2d, 0x75, 0x72, 0x6e, 0x3a, 0x75, 0x75, 0x69, 0x64, + 0x3a, 0x30, 0x37, 0x62, 0x63, 0x35, 0x61, 0x34, 0x37, 0x2d, 0x65, 0x64, + 0x66, 0x32, 0x2d, 0x34, 0x36, 0x36, 0x31, 0x2d, 0x38, 0x37, 0x34, 0x38, + 0x2d, 0x38, 0x36, 0x32, 0x30, 0x66, 0x37, 0x39, 0x64, 0x34, 0x61, 0x63, + 0x34, 0x45, 0x00, 0x0d, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, + 0x69, 0x63, 0x6f, 0x6e, 0x73, 0x00, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, + 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, + 0x36, 0x30, 0x30, 0x30, 0x30, 0x2f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x68, + 0x69, 0x70, 0x34, 0x38, 0x2e, 0x70, 0x6e, 0x67, 0x45, 0x00, 0x00, 0x00, + 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x36, 0x30, 0x30, 0x30, 0x30, 0x2f, + 0x66, 0x6c, 0x61, 0x67, 0x73, 0x68, 0x69, 0x70, 0x31, 0x32, 0x38, 0x2e, + 0x70, 0x6e, 0x67, 0x45, 0x00, 0x00, 0x00, 0x26, 0x68, 0x74, 0x74, 0x70, + 0x3a, 0x2f, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, + 0x3a, 0x36, 0x30, 0x30, 0x30, 0x30, 0x2f, 0x66, 0x6c, 0x61, 0x67, 0x73, + 0x68, 0x69, 0x70, 0x35, 0x31, 0x32, 0x2e, 0x70, 0x6e, 0x67, 0x42, 0x00, + 0x13, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x64, 0x6e, 0x73, + 0x2d, 0x73, 0x64, 0x2d, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x1d, 0x50, 0x61, + 0x6e, 0x74, 0x75, 0x6d, 0x20, 0x4d, 0x37, 0x33, 0x30, 0x30, 0x46, 0x44, + 0x57, 0x20, 0x53, 0x65, 0x72, 0x69, 0x65, 0x73, 0x20, 0x31, 0x43, 0x36, + 0x46, 0x43, 0x44, 0x44, 0x00, 0x0d, 0x75, 0x72, 0x66, 0x2d, 0x73, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x00, 0x04, 0x56, 0x31, 0x2e, + 0x34, 0x44, 0x00, 0x00, 0x00, 0x02, 0x57, 0x38, 0x44, 0x00, 0x00, 0x00, + 0x03, 0x49, 0x53, 0x31, 0x44, 0x00, 0x00, 0x00, 0x04, 0x43, 0x50, 0x39, + 0x39, 0x44, 0x00, 0x00, 0x00, 0x03, 0x50, 0x51, 0x34, 0x44, 0x00, 0x00, + 0x00, 0x04, 0x4f, 0x42, 0x31, 0x30, 0x44, 0x00, 0x00, 0x00, 0x05, 0x52, + 0x53, 0x36, 0x30, 0x30, 0x44, 0x00, 0x00, 0x00, 0x03, 0x44, 0x4d, 0x31, + 0x44, 0x00, 0x0c, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x2d, 0x6b, + 0x69, 0x6e, 0x64, 0x00, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x44, 0x00, 0x00, 0x00, 0x08, 0x65, 0x6e, 0x76, 0x65, 0x6c, 0x6f, + 0x70, 0x65, 0x34, 0x00, 0x14, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x2d, 0x73, + 0x69, 0x7a, 0x65, 0x2d, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x54, 0x56, 0x21, + 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x00, 0x04, 0x00, 0x00, 0x6d, 0x24, 0x37, 0x00, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x52, + 0x08, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x74, 0x04, 0x37, 0x00, 0x00, + 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, + 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, + 0x00, 0x39, 0xd0, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x52, 0x08, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, + 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, + 0x04, 0x00, 0x00, 0x29, 0x04, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x39, + 0xd0, 0x37, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, + 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x00, 0x04, 0x00, 0x00, 0x2c, 0x88, 0x21, 0x00, 0x0b, 0x79, 0x2d, + 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, + 0x00, 0x3f, 0x48, 0x37, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, + 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x2a, 0xf8, 0x21, 0x00, 0x0b, + 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, + 0x04, 0x00, 0x00, 0x55, 0xf0, 0x37, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, + 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x27, 0x10, 0x21, + 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x00, 0x04, 0x00, 0x00, 0x39, 0xd0, 0x37, 0x00, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x2e, + 0xe0, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, + 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x5b, 0xcc, 0x37, 0x00, 0x00, + 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, 0x78, 0x2d, + 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, + 0x00, 0x28, 0xed, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, + 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x5e, 0x42, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x0b, + 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, + 0x04, 0x00, 0x00, 0x54, 0x56, 0x21, 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, + 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x00, 0x00, 0x8a, + 0xe8, 0x37, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x33, + 0x00, 0x0b, 0x78, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x00, 0x08, 0x00, 0x00, 0x27, 0x10, 0x00, 0x00, 0x39, 0xd0, 0x33, + 0x00, 0x0b, 0x79, 0x2d, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x00, 0x08, 0x00, 0x00, 0x54, 0x60, 0x00, 0x00, 0x8b, 0x10, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x0f, 0x73, 0x69, 0x64, 0x65, 0x73, + 0x2d, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x00, 0x09, + 0x6f, 0x6e, 0x65, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x64, 0x44, 0x00, 0x00, + 0x00, 0x14, 0x74, 0x77, 0x6f, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x64, 0x2d, + 0x73, 0x68, 0x6f, 0x72, 0x74, 0x2d, 0x65, 0x64, 0x67, 0x65, 0x44, 0x00, + 0x00, 0x00, 0x13, 0x74, 0x77, 0x6f, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x64, + 0x2d, 0x6c, 0x6f, 0x6e, 0x67, 0x2d, 0x65, 0x64, 0x67, 0x65, 0x03, +} diff -Nru golang-github-openprinting-goipp-1.0.0/group.go golang-github-openprinting-goipp-1.1.0/group.go --- golang-github-openprinting-goipp-1.0.0/group.go 1970-01-01 01:00:00.000000000 +0100 +++ golang-github-openprinting-goipp-1.1.0/group.go 2023-12-05 12:34:36.000000000 +0100 @@ -0,0 +1,58 @@ +/* Go IPP - IPP core protocol implementation in pure Go + * + * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com) + * See LICENSE for license terms and conditions + * + * Groups of attributes + */ + +package goipp + +// Group represents a group of attributes. +// +// Since 1.1.0 +type Group struct { + Tag Tag // Group tag + Attrs Attributes // Group attributes +} + +// Groups represents a sequence of groups +// +// The primary purpose of this type is to represent +// messages with repeated groups with the same group tag +// +// See Message type documentation for more details +// +// Since 1.1.0 +type Groups []Group + +// Add Attribute to the Group +func (g *Group) Add(attr Attribute) { + g.Attrs.Add(attr) +} + +// Equal checks that groups g and g2 are equal +func (g Group) Equal(g2 Group) bool { + return g.Tag == g2.Tag && g.Attrs.Equal(g2.Attrs) +} + +// Add Group to Groups +func (groups *Groups) Add(g Group) { + *groups = append(*groups, g) +} + +// Equal checks that groups and groups2 are equal +func (groups Groups) Equal(groups2 Groups) bool { + if len(groups) != len(groups2) { + return false + } + + for i, g := range groups { + g2 := groups2[i] + if !g.Equal(g2) { + return false + } + } + + return true +} diff -Nru golang-github-openprinting-goipp-1.0.0/message.go golang-github-openprinting-goipp-1.1.0/message.go --- golang-github-openprinting-goipp-1.0.0/message.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/message.go 2023-12-05 12:34:36.000000000 +0100 @@ -22,7 +22,7 @@ // 16-bit word type Version uint16 -// DefaultVersion is the default IPP version +// DefaultVersion is the default IPP version (2.0 for now) const DefaultVersion Version = 0x0200 // MakeVersion makes version from major and minor parts @@ -40,7 +40,7 @@ return uint8(v) } -// String() converts version to string (i.e., 2.0) +// String() converts version to string (i.e., "2.0") func (v Version) String() string { return fmt.Sprintf("%d.%d", v.Major(), v.Minor()) } @@ -53,6 +53,34 @@ Code Code // Operation for request, status for response RequestID uint32 // Set in request, returned in response + // Groups of Attributes + // + // This field allows to represent messages with repeated + // groups of attributes with the same group tag. The most + // noticeable use case is the Get-Jobs response which uses + // multiple Job groups, one per returned job. See RFC 8011, + // 4.2.6.2. for more details + // + // See also the following discussions which explain the demand + // to implement this interface: + // https://github.com/OpenPrinting/goipp/issues/2 + // https://github.com/OpenPrinting/goipp/pull/3 + // + // With respect to backward compatibility, the following + // behavior is implemented here: + // 1. (*Message).Decode() fills both Groups and named per-group + // fields (i.e., Operation, Job etc) + // 2. (*Message).Encode() and (*Message) Print, if Groups != nil, + // uses Groups and ignores named per-group fields. Otherwise, + // named fields are used as in 1.0.0 + // 3. (*Message) Equal(), for each message uses Groups if + // it is not nil or named per-group fields otherwise. + // In another words, Equal() compares messages as if + // they were encoded + // + // Since 1.1.0 + Groups Groups + // Attributes, by group Operation Attributes // Operation attributes Job Attributes // Job attributes @@ -101,22 +129,10 @@ return false } - groups1 := m.attrGroups() + groups := m.attrGroups() groups2 := m2.attrGroups() - if len(groups1) != len(groups2) { - return false - } - - for i, grp1 := range groups1 { - grp2 := groups2[i] - - if grp1.tag != grp2.tag || !grp1.attrs.Equal(grp2.attrs) { - return false - } - } - - return true + return groups.Equal(groups2) } // Reset the message into initial state @@ -141,10 +157,19 @@ return buf.Bytes(), err } -// Decode message +// Decode reads message from io.Reader func (m *Message) Decode(in io.Reader) error { + return m.DecodeEx(in, DecoderOptions{}) +} + +// DecodeEx reads message from io.Reader +// +// It is extended version of the Decode method, with additional +// DecoderOptions parameter +func (m *Message) DecodeEx(in io.Reader, opt DecoderOptions) error { md := messageDecoder{ - in: in, + in: in, + opt: opt, } m.Reset() @@ -156,6 +181,14 @@ return m.Decode(bytes.NewBuffer(data)) } +// DecodeBytesEx decodes message from byte slice +// +// It is extended version of the DecodeBytes method, with additional +// DecoderOptions parameter +func (m *Message) DecodeBytesEx(data []byte, opt DecoderOptions) error { + return m.DecodeEx(bytes.NewBuffer(data), opt) +} + // Print pretty-prints the message. The 'request' parameter affects // interpretation of Message.Code: it is interpreted either // as Op or as Status @@ -171,8 +204,8 @@ } for _, grp := range m.attrGroups() { - fmt.Fprintf(out, "\n"+msgPrintIndent+"GROUP %s\n", grp.tag) - for _, attr := range grp.attrs { + fmt.Fprintf(out, "\n"+msgPrintIndent+"GROUP %s\n", grp.Tag) + for _, attr := range grp.Attrs { m.printAttribute(out, attr, 1) out.Write([]byte("\n")) } @@ -219,15 +252,14 @@ // but groups with non-nil are not, even if len(Attributes) == 0 // // This is a helper function for message encoder and pretty-printer -func (m *Message) attrGroups() []struct { - tag Tag - attrs Attributes -} { +func (m *Message) attrGroups() Groups { + // If m.Groups is set, use it + if m.Groups != nil { + return m.Groups + } + // Initialize slice of groups - groups := []struct { - tag Tag - attrs Attributes - }{ + groups := Groups{ {TagOperationGroup, m.Operation}, {TagJobGroup, m.Job}, {TagPrinterGroup, m.Printer}, @@ -247,7 +279,7 @@ // Skip all empty groups out := 0 for in := 0; in < len(groups); in++ { - if groups[in].attrs != nil { + if groups[in].Attrs != nil { groups[out] = groups[in] out++ } diff -Nru golang-github-openprinting-goipp-1.0.0/op.go golang-github-openprinting-goipp-1.1.0/op.go --- golang-github-openprinting-goipp-1.0.0/op.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/op.go 2023-12-05 12:34:36.000000000 +0100 @@ -15,6 +15,7 @@ // Op represents an IPP Operation Code type Op Code +// Op codes const ( OpPrintJob Op = 0x0002 // Print-Job: Print a single file OpPrintURI Op = 0x0003 // Print-URI: Print a single URL @@ -143,236 +144,128 @@ // String() returns a Status name, as defined by RFC 8010 func (op Op) String() string { - switch op { - case OpPrintJob: - return "Print-Job" - case OpPrintURI: - return "Print-URI" - case OpValidateJob: - return "Validate-Job" - case OpCreateJob: - return "Create-Job" - case OpSendDocument: - return "Send-Document" - case OpSendURI: - return "Send-URI" - case OpCancelJob: - return "Cancel-Job" - case OpGetJobAttributes: - return "Get-Job-Attribute" - case OpGetJobs: - return "Get-Jobs" - case OpGetPrinterAttributes: - return "Get-Printer-Attributes" - case OpHoldJob: - return "Hold-Job" - case OpReleaseJob: - return "Release-Job" - case OpRestartJob: - return "Restart-Job" - case OpPausePrinter: - return "Pause-Printer" - case OpResumePrinter: - return "Resume-Printer" - case OpPurgeJobs: - return "Purge-Jobs" - case OpSetPrinterAttributes: - return "Set-Printer-Attributes" - case OpSetJobAttributes: - return "Set-Job-Attributes" - case OpGetPrinterSupportedValues: - return "Get-Printer-Supported-Values" - case OpCreatePrinterSubscriptions: - return "Create-Printer-Subscriptions" - case OpCreateJobSubscriptions: - return "Create-Job-Subscriptions" - case OpGetSubscriptionAttributes: - return "Get-Subscription-Attributes" - case OpGetSubscriptions: - return "Get-Subscriptions" - case OpRenewSubscription: - return "Renew-Subscription" - case OpCancelSubscription: - return "Cancel-Subscription" - case OpGetNotifications: - return "Get-Notifications" - case OpSendNotifications: - return "Send-Notifications" - case OpGetResourceAttributes: - return "Get-Resource-Attributes" - case OpGetResourceData: - return "Get-Resource-Data" - case OpGetResources: - return "Get-Resources" - case OpGetPrintSupportFiles: - return "Get-Printer-Support-Files" - case OpEnablePrinter: - return "Enable-Printer" - case OpDisablePrinter: - return "Disable-Printer" - case OpPausePrinterAfterCurrentJob: - return "Pause-Printer-After-Current-Job" - case OpHoldNewJobs: - return "Hold-New-Jobs" - case OpReleaseHeldNewJobs: - return "Release-Held-New-Jobs" - case OpDeactivatePrinter: - return "Deactivate-Printer" - case OpActivatePrinter: - return "Activate-Printer" - case OpRestartPrinter: - return "Restart-Printer" - case OpShutdownPrinter: - return "Shutdown-Printer" - case OpStartupPrinter: - return "Startup-Printer" - case OpReprocessJob: - return "Reprocess-Job" - case OpCancelCurrentJob: - return "Cancel-Current-Job" - case OpSuspendCurrentJob: - return "Suspend-Current-Job" - case OpResumeJob: - return "Resume-Job" - case OpPromoteJob: - return "Promote-Job" - case OpScheduleJobAfter: - return "Schedule-Job-After" - case OpCancelDocument: - return "Cancel-Document" - case OpGetDocumentAttributes: - return "Get-Document-Attributes" - case OpGetDocuments: - return "Get-Documents" - case OpDeleteDocument: - return "Delete-Document" - case OpSetDocumentAttributes: - return "Set-Document-Attributes" - case OpCancelJobs: - return "Cancel-Jobs" - case OpCancelMyJobs: - return "Cancel-My-Jobs" - case OpResubmitJob: - return "Resubmit-Job" - case OpCloseJob: - return "Close-Job" - case OpIdentifyPrinter: - return "Identify-Printer" - case OpValidateDocument: - return "Validate-Document" - case OpAddDocumentImages: - return "Add-Document-Images" - case OpAcknowledgeDocument: - return "Acknowledge-Document" - case OpAcknowledgeIdentifyPrinter: - return "Acknowledge-Identify-Printer" - case OpAcknowledgeJob: - return "Acknowledge-Job" - case OpFetchDocument: - return "Fetch-Document" - case OpFetchJob: - return "Fetch-Job" - case OpGetOutputDeviceAttributes: - return "Get-Output-Device-Attributes" - case OpUpdateActiveJobs: - return "Update-Active-Jobs" - case OpDeregisterOutputDevice: - return "Deregister-Output-Device" - case OpUpdateDocumentStatus: - return "Update-Document-Status" - case OpUpdateJobStatus: - return "Update-Job-Status" - case OpupdateOutputDeviceAttributes: - return "Update-Output-Device-Attributes" - case OpGetNextDocumentData: - return "Get-Next-Document-Data" - case OpAllocatePrinterResources: - return "Allocate-Printer-Resources" - case OpCreatePrinter: - return "Create-Printer" - case OpDeallocatePrinterResources: - return "Deallocate-Printer-Resources" - case OpDeletePrinter: - return "Delete-Printer" - case OpGetPrinters: - return "Get-Printers" - case OpShutdownOnePrinter: - return "Shutdown-One-Printer" - case OpStartupOnePrinter: - return "Startup-One-Printer" - case OpCancelResource: - return "Cancel-Resource" - case OpCreateResource: - return "Create-Resource" - case OpInstallResource: - return "Install-Resource" - case OpSendResourceData: - return "Send-Resource-Data" - case OpSetResourceAttributes: - return "Set-Resource-Attributes" - case OpCreateResourceSubscriptions: - return "Create-Resource-Subscriptions" - case OpCreateSystemSubscriptions: - return "Create-System-Subscriptions" - case OpDisableAllPrinters: - return "Disable-All-Printers" - case OpEnableAllPrinters: - return "Enable-All-Printers" - case OpGetSystemAttributes: - return "Get-System-Attributes" - case OpGetSystemSupportedValues: - return "Get-System-Supported-Values" - case OpPauseAllPrinters: - return "Pause-All-Printers" - case OpPauseAllPrintersAfterCurrentJob: - return "Pause-All-Printers-After-Current-Job" - case OpRegisterOutputDevice: - return "Register-Output-Device" - case OpRestartSystem: - return "Restart-System" - case OpResumeAllPrinters: - return "Resume-All-Printers" - case OpSetSystemAttributes: - return "Set-System-Attributes" - case OpShutdownAllPrinters: - return "Shutdown-All-Printers" - case OpStartupAllPrinters: - return "Startup-All-Printers" - case OpCupsGetDefault: - return "CUPS-Get-Default" - case OpCupsGetPrinters: - return "CUPS-Get-Printers" - case OpCupsAddModifyPrinter: - return "CUPS-Add-Modify-Printer" - case OpCupsDeletePrinter: - return "CUPS-Delete-Printer" - case OpCupsGetClasses: - return "CUPS-Get-Classes" - case OpCupsAddModifyClass: - return "CUPS-Add-Modify-Class" - case OpCupsDeleteClass: - return "CUPS-Delete-Class" - case OpCupsAcceptJobs: - return "CUPS-Accept-Jobs" - case OpCupsRejectJobs: - return "CUPS-Reject-Jobs" - case OpCupsSetDefault: - return "CUPS-Set-Default" - case OpCupsGetDevices: - return "CUPS-Get-Devices" - case OpCupsGetPpds: - return "CUPS-Get-PPDs" - case OpCupsMoveJob: - return "CUPS-Move-Job" - case OpCupsAuthenticateJob: - return "CUPS-Authenticate-Job" - case OpCupsGetPpd: - return "CUPS-Get-PPD" - case OpCupsGetDocument: - return "CUPS-Get-Document" - case OpCupsCreateLocalPrinter: - return "CUPS-Create-Local-Printer" + if int(op) < len(opNames) { + if s := opNames[op]; s != "" { + return s + } } return fmt.Sprintf("0x%4.4x", int(op)) } + +var opNames = [...]string{ + OpPrintJob: "Print-Job", + OpPrintURI: "Print-URI", + OpValidateJob: "Validate-Job", + OpCreateJob: "Create-Job", + OpSendDocument: "Send-Document", + OpSendURI: "Send-URI", + OpCancelJob: "Cancel-Job", + OpGetJobAttributes: "Get-Job-Attribute", + OpGetJobs: "Get-Jobs", + OpGetPrinterAttributes: "Get-Printer-Attributes", + OpHoldJob: "Hold-Job", + OpReleaseJob: "Release-Job", + OpRestartJob: "Restart-Job", + OpPausePrinter: "Pause-Printer", + OpResumePrinter: "Resume-Printer", + OpPurgeJobs: "Purge-Jobs", + OpSetPrinterAttributes: "Set-Printer-Attributes", + OpSetJobAttributes: "Set-Job-Attributes", + OpGetPrinterSupportedValues: "Get-Printer-Supported-Values", + OpCreatePrinterSubscriptions: "Create-Printer-Subscriptions", + OpCreateJobSubscriptions: "Create-Job-Subscriptions", + OpGetSubscriptionAttributes: "Get-Subscription-Attributes", + OpGetSubscriptions: "Get-Subscriptions", + OpRenewSubscription: "Renew-Subscription", + OpCancelSubscription: "Cancel-Subscription", + OpGetNotifications: "Get-Notifications", + OpSendNotifications: "Send-Notifications", + OpGetResourceAttributes: "Get-Resource-Attributes", + OpGetResourceData: "Get-Resource-Data", + OpGetResources: "Get-Resources", + OpGetPrintSupportFiles: "Get-Printer-Support-Files", + OpEnablePrinter: "Enable-Printer", + OpDisablePrinter: "Disable-Printer", + OpPausePrinterAfterCurrentJob: "Pause-Printer-After-Current-Job", + OpHoldNewJobs: "Hold-New-Jobs", + OpReleaseHeldNewJobs: "Release-Held-New-Jobs", + OpDeactivatePrinter: "Deactivate-Printer", + OpActivatePrinter: "Activate-Printer", + OpRestartPrinter: "Restart-Printer", + OpShutdownPrinter: "Shutdown-Printer", + OpStartupPrinter: "Startup-Printer", + OpReprocessJob: "Reprocess-Job", + OpCancelCurrentJob: "Cancel-Current-Job", + OpSuspendCurrentJob: "Suspend-Current-Job", + OpResumeJob: "Resume-Job", + OpPromoteJob: "Promote-Job", + OpScheduleJobAfter: "Schedule-Job-After", + OpCancelDocument: "Cancel-Document", + OpGetDocumentAttributes: "Get-Document-Attributes", + OpGetDocuments: "Get-Documents", + OpDeleteDocument: "Delete-Document", + OpSetDocumentAttributes: "Set-Document-Attributes", + OpCancelJobs: "Cancel-Jobs", + OpCancelMyJobs: "Cancel-My-Jobs", + OpResubmitJob: "Resubmit-Job", + OpCloseJob: "Close-Job", + OpIdentifyPrinter: "Identify-Printer", + OpValidateDocument: "Validate-Document", + OpAddDocumentImages: "Add-Document-Images", + OpAcknowledgeDocument: "Acknowledge-Document", + OpAcknowledgeIdentifyPrinter: "Acknowledge-Identify-Printer", + OpAcknowledgeJob: "Acknowledge-Job", + OpFetchDocument: "Fetch-Document", + OpFetchJob: "Fetch-Job", + OpGetOutputDeviceAttributes: "Get-Output-Device-Attributes", + OpUpdateActiveJobs: "Update-Active-Jobs", + OpDeregisterOutputDevice: "Deregister-Output-Device", + OpUpdateDocumentStatus: "Update-Document-Status", + OpUpdateJobStatus: "Update-Job-Status", + OpupdateOutputDeviceAttributes: "Update-Output-Device-Attributes", + OpGetNextDocumentData: "Get-Next-Document-Data", + OpAllocatePrinterResources: "Allocate-Printer-Resources", + OpCreatePrinter: "Create-Printer", + OpDeallocatePrinterResources: "Deallocate-Printer-Resources", + OpDeletePrinter: "Delete-Printer", + OpGetPrinters: "Get-Printers", + OpShutdownOnePrinter: "Shutdown-One-Printer", + OpStartupOnePrinter: "Startup-One-Printer", + OpCancelResource: "Cancel-Resource", + OpCreateResource: "Create-Resource", + OpInstallResource: "Install-Resource", + OpSendResourceData: "Send-Resource-Data", + OpSetResourceAttributes: "Set-Resource-Attributes", + OpCreateResourceSubscriptions: "Create-Resource-Subscriptions", + OpCreateSystemSubscriptions: "Create-System-Subscriptions", + OpDisableAllPrinters: "Disable-All-Printers", + OpEnableAllPrinters: "Enable-All-Printers", + OpGetSystemAttributes: "Get-System-Attributes", + OpGetSystemSupportedValues: "Get-System-Supported-Values", + OpPauseAllPrinters: "Pause-All-Printers", + OpPauseAllPrintersAfterCurrentJob: "Pause-All-Printers-After-Current-Job", + OpRegisterOutputDevice: "Register-Output-Device", + OpRestartSystem: "Restart-System", + OpResumeAllPrinters: "Resume-All-Printers", + OpSetSystemAttributes: "Set-System-Attributes", + OpShutdownAllPrinters: "Shutdown-All-Printers", + OpStartupAllPrinters: "Startup-All-Printers", + OpCupsGetDefault: "CUPS-Get-Default", + OpCupsGetPrinters: "CUPS-Get-Printers", + OpCupsAddModifyPrinter: "CUPS-Add-Modify-Printer", + OpCupsDeletePrinter: "CUPS-Delete-Printer", + OpCupsGetClasses: "CUPS-Get-Classes", + OpCupsAddModifyClass: "CUPS-Add-Modify-Class", + OpCupsDeleteClass: "CUPS-Delete-Class", + OpCupsAcceptJobs: "CUPS-Accept-Jobs", + OpCupsRejectJobs: "CUPS-Reject-Jobs", + OpCupsSetDefault: "CUPS-Set-Default", + OpCupsGetDevices: "CUPS-Get-Devices", + OpCupsGetPpds: "CUPS-Get-PPDs", + OpCupsMoveJob: "CUPS-Move-Job", + OpCupsAuthenticateJob: "CUPS-Authenticate-Job", + OpCupsGetPpd: "CUPS-Get-PPD", + OpCupsGetDocument: "CUPS-Get-Document", + OpCupsCreateLocalPrinter: "CUPS-Create-Local-Printer", +} diff -Nru golang-github-openprinting-goipp-1.0.0/status.go golang-github-openprinting-goipp-1.1.0/status.go --- golang-github-openprinting-goipp-1.0.0/status.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/status.go 2023-12-05 12:34:36.000000000 +0100 @@ -15,6 +15,7 @@ // Status represents an IPP Status Code type Status Code +// Status codes const ( StatusOk Status = 0x0000 // successful-ok StatusOkIgnoredOrSubstituted Status = 0x0001 // successful-ok-ignored-or-substituted-attributes @@ -75,121 +76,71 @@ ) // String() returns a Status name, as defined by RFC 8010 -func (s Status) String() string { - switch s { - case StatusOk: - return "successful-ok" - case StatusOkIgnoredOrSubstituted: - return "successful-ok-ignored-or-substituted-attributes" - case StatusOkConflicting: - return "successful-ok-conflicting-attributes" - case StatusOkIgnoredSubscriptions: - return "successful-ok-ignored-subscriptions" - case StatusOkIgnoredNotifications: - return "successful-ok-ignored-notifications" - case StatusOkTooManyEvents: - return "successful-ok-too-many-events" - case StatusOkButCancelSubscription: - return "successful-ok-but-cancel-subscription" - case StatusOkEventsComplete: - return "successful-ok-events-complete" - case StatusRedirectionOtherSite: - return "redirection-other-site" - case StatusCupsSeeOther: - return "cups-see-other" - case StatusErrorBadRequest: - return "client-error-bad-request" - case StatusErrorForbidden: - return "client-error-forbidden" - case StatusErrorNotAuthenticated: - return "client-error-not-authenticated" - case StatusErrorNotAuthorized: - return "client-error-not-authorized" - case StatusErrorNotPossible: - return "client-error-not-possible" - case StatusErrorTimeout: - return "client-error-timeout" - case StatusErrorNotFound: - return "client-error-not-found" - case StatusErrorGone: - return "client-error-gone" - case StatusErrorRequestEntity: - return "client-error-request-entity-too-large" - case StatusErrorRequestValue: - return "client-error-request-value-too-long" - case StatusErrorDocumentFormatNotSupported: - return "client-error-document-format-not-supported" - case StatusErrorAttributesOrValues: - return "client-error-attributes-or-values-not-supported" - case StatusErrorURIScheme: - return "client-error-uri-scheme-not-supported" - case StatusErrorCharset: - return "client-error-charset-not-supported" - case StatusErrorConflicting: - return "client-error-conflicting-attributes" - case StatusErrorCompressionNotSupported: - return "client-error-compression-not-supported" - case StatusErrorCompressionError: - return "client-error-compression-error" - case StatusErrorDocumentFormatError: - return "client-error-document-format-error" - case StatusErrorDocumentAccess: - return "client-error-document-access-error" - case StatusErrorAttributesNotSettable: - return "client-error-attributes-not-settable" - case StatusErrorIgnoredAllSubscriptions: - return "client-error-ignored-all-subscriptions" - case StatusErrorTooManySubscriptions: - return "client-error-too-many-subscriptions" - case StatusErrorIgnoredAllNotifications: - return "client-error-ignored-all-notifications" - case StatusErrorPrintSupportFileNotFound: - return "client-error-print-support-file-not-found" - case StatusErrorDocumentPassword: - return "client-error-document-password-error" - case StatusErrorDocumentPermission: - return "client-error-document-permission-error" - case StatusErrorDocumentSecurity: - return "client-error-document-security-error" - case StatusErrorDocumentUnprintable: - return "client-error-document-unprintable-error" - case StatusErrorAccountInfoNeeded: - return "client-error-account-info-needed" - case StatusErrorAccountClosed: - return "client-error-account-closed" - case StatusErrorAccountLimitReached: - return "client-error-account-limit-reached" - case StatusErrorAccountAuthorizationFailed: - return "client-error-account-authorization-failed" - case StatusErrorNotFetchable: - return "client-error-not-fetchable" - case StatusErrorInternal: - return "server-error-internal-error" - case StatusErrorOperationNotSupported: - return "server-error-operation-not-supported" - case StatusErrorServiceUnavailable: - return "server-error-service-unavailable" - case StatusErrorVersionNotSupported: - return "server-error-version-not-supported" - case StatusErrorDevice: - return "server-error-device-error" - case StatusErrorTemporary: - return "server-error-temporary-error" - case StatusErrorNotAcceptingJobs: - return "server-error-not-accepting-jobs" - case StatusErrorBusy: - return "server-error-busy" - case StatusErrorJobCanceled: - return "server-error-job-canceled" - case StatusErrorMultipleJobsNotSupported: - return "server-error-multiple-document-jobs-not-supported" - case StatusErrorPrinterIsDeactivated: - return "server-error-printer-is-deactivated" - case StatusErrorTooManyJobs: - return "server-error-too-many-jobs" - case StatusErrorTooManyDocuments: - return "server-error-too-many-documents" +func (status Status) String() string { + if int(status) < len(statusNames) { + if s := statusNames[status]; s != "" { + return s + } } - return fmt.Sprintf("0x%4.4x", int(s)) + return fmt.Sprintf("0x%4.4x", int(status)) +} + +var statusNames = [...]string{ + StatusOk: "successful-ok", + StatusOkIgnoredOrSubstituted: "successful-ok-ignored-or-substituted-attributes", + StatusOkConflicting: "successful-ok-conflicting-attributes", + StatusOkIgnoredSubscriptions: "successful-ok-ignored-subscriptions", + StatusOkIgnoredNotifications: "successful-ok-ignored-notifications", + StatusOkTooManyEvents: "successful-ok-too-many-events", + StatusOkButCancelSubscription: "successful-ok-but-cancel-subscription", + StatusOkEventsComplete: "successful-ok-events-complete", + StatusRedirectionOtherSite: "redirection-other-site", + StatusCupsSeeOther: "cups-see-other", + StatusErrorBadRequest: "client-error-bad-request", + StatusErrorForbidden: "client-error-forbidden", + StatusErrorNotAuthenticated: "client-error-not-authenticated", + StatusErrorNotAuthorized: "client-error-not-authorized", + StatusErrorNotPossible: "client-error-not-possible", + StatusErrorTimeout: "client-error-timeout", + StatusErrorNotFound: "client-error-not-found", + StatusErrorGone: "client-error-gone", + StatusErrorRequestEntity: "client-error-request-entity-too-large", + StatusErrorRequestValue: "client-error-request-value-too-long", + StatusErrorDocumentFormatNotSupported: "client-error-document-format-not-supported", + StatusErrorAttributesOrValues: "client-error-attributes-or-values-not-supported", + StatusErrorURIScheme: "client-error-uri-scheme-not-supported", + StatusErrorCharset: "client-error-charset-not-supported", + StatusErrorConflicting: "client-error-conflicting-attributes", + StatusErrorCompressionNotSupported: "client-error-compression-not-supported", + StatusErrorCompressionError: "client-error-compression-error", + StatusErrorDocumentFormatError: "client-error-document-format-error", + StatusErrorDocumentAccess: "client-error-document-access-error", + StatusErrorAttributesNotSettable: "client-error-attributes-not-settable", + StatusErrorIgnoredAllSubscriptions: "client-error-ignored-all-subscriptions", + StatusErrorTooManySubscriptions: "client-error-too-many-subscriptions", + StatusErrorIgnoredAllNotifications: "client-error-ignored-all-notifications", + StatusErrorPrintSupportFileNotFound: "client-error-print-support-file-not-found", + StatusErrorDocumentPassword: "client-error-document-password-error", + StatusErrorDocumentPermission: "client-error-document-permission-error", + StatusErrorDocumentSecurity: "client-error-document-security-error", + StatusErrorDocumentUnprintable: "client-error-document-unprintable-error", + StatusErrorAccountInfoNeeded: "client-error-account-info-needed", + StatusErrorAccountClosed: "client-error-account-closed", + StatusErrorAccountLimitReached: "client-error-account-limit-reached", + StatusErrorAccountAuthorizationFailed: "client-error-account-authorization-failed", + StatusErrorNotFetchable: "client-error-not-fetchable", + StatusErrorInternal: "server-error-internal-error", + StatusErrorOperationNotSupported: "server-error-operation-not-supported", + StatusErrorServiceUnavailable: "server-error-service-unavailable", + StatusErrorVersionNotSupported: "server-error-version-not-supported", + StatusErrorDevice: "server-error-device-error", + StatusErrorTemporary: "server-error-temporary-error", + StatusErrorNotAcceptingJobs: "server-error-not-accepting-jobs", + StatusErrorBusy: "server-error-busy", + StatusErrorJobCanceled: "server-error-job-canceled", + StatusErrorMultipleJobsNotSupported: "server-error-multiple-document-jobs-not-supported", + StatusErrorPrinterIsDeactivated: "server-error-printer-is-deactivated", + StatusErrorTooManyJobs: "server-error-too-many-jobs", + StatusErrorTooManyDocuments: "server-error-too-many-documents", } diff -Nru golang-github-openprinting-goipp-1.0.0/tag.go golang-github-openprinting-goipp-1.1.0/tag.go --- golang-github-openprinting-goipp-1.0.0/tag.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/tag.go 2023-12-05 12:34:36.000000000 +0100 @@ -16,6 +16,7 @@ // of the IPP message type Tag int +// Tag values const ( // Delimiter tags TagZero Tag = 0x00 // Zero tag - used for separators @@ -30,10 +31,10 @@ TagDocumentGroup Tag = 0x09 // Document group TagSystemGroup Tag = 0x0a // System group TagFuture11Group Tag = 0x0b // Future group 11 - TagFuture12Group Tag = 0x0c // Future group 11 - TagFuture13Group Tag = 0x0d // Future group 11 - TagFuture14Group Tag = 0x0e // Future group 11 - TagFuture15Group Tag = 0x0f // Future group 11 + TagFuture12Group Tag = 0x0c // Future group 12 + TagFuture13Group Tag = 0x0d // Future group 13 + TagFuture14Group Tag = 0x0e // Future group 14 + TagFuture15Group Tag = 0x0f // Future group 15 // Value tags TagUnsupportedValue Tag = 0x10 // Unsupported value @@ -69,7 +70,12 @@ // IsDelimiter returns true for delimiter tags func (tag Tag) IsDelimiter() bool { - return tag < 0x10 + return uint(tag) < 0x10 +} + +// IsGroup returns true for group tags +func (tag Tag) IsGroup() bool { + return tag.IsDelimiter() && tag != TagZero && tag != TagEnd } // Type returns Type of Value that corresponds to the tag @@ -119,86 +125,59 @@ // String() returns a tag name, as defined by RFC 8010 func (tag Tag) String() string { - switch tag { - case TagZero: - return "zero" - case TagOperationGroup: - return "operation-attributes-tag" - case TagJobGroup: - return "job-attributes-tag" - case TagEnd: - return "end-of-attributes-tag" - case TagPrinterGroup: - return "printer-attributes-tag" - case TagUnsupportedGroup: - return "unsupported-attributes-tag" - case TagSubscriptionGroup: - return "subscription-attributes-tag" - case TagEventNotificationGroup: - return "event-notification-attributes-tag" - case TagResourceGroup: - return "resource-attributes-tag" - case TagDocumentGroup: - return "document-attributes-tag" - case TagSystemGroup: - return "system-attributes-tag" + if 0 <= tag && int(tag) < len(tagNames) { + if s := tagNames[tag]; s != "" { + return s + } + } - // Value tags - case TagUnsupportedValue: - return "unsupported" - case TagDefault: - return "default" - case TagUnknown: - return "unknown" - case TagNoValue: - return "no-value" - case TagNotSettable: - return "not-settable" - case TagDeleteAttr: - return "delete-attribute" - case TagAdminDefine: - return "admin-define" - case TagInteger: - return "integer" - case TagBoolean: - return "boolean" - case TagEnum: - return "enum" - case TagString: - return "octetString" - case TagDateTime: - return "dateTime" - case TagResolution: - return "resolution" - case TagRange: - return "rangeOfInteger" - case TagBeginCollection: - return "collection" - case TagTextLang: - return "textWithLanguage" - case TagNameLang: - return "nameWithLanguage" - case TagEndCollection: - return "endCollection" - case TagText: - return "textWithoutLanguage" - case TagName: - return "nameWithoutLanguage" - case TagKeyword: - return "keyword" - case TagURI: - return "uri" - case TagURIScheme: - return "uriScheme" - case TagCharset: - return "charset" - case TagLanguage: - return "naturalLanguage" - case TagMimeType: - return "mimeMediaType" - case TagMemberName: - return "memberAttrName" + if tag < 0x100 { + return fmt.Sprintf("0x%2.2x", uint(tag)) } - return fmt.Sprintf("0x%2.2x", int(tag)) + return fmt.Sprintf("0x%8.8x", uint(tag)) +} + +var tagNames = [...]string{ + // Delimiter tags + TagZero: "zero", + TagOperationGroup: "operation-attributes-tag", + TagJobGroup: "job-attributes-tag", + TagEnd: "end-of-attributes-tag", + TagPrinterGroup: "printer-attributes-tag", + TagUnsupportedGroup: "unsupported-attributes-tag", + TagSubscriptionGroup: "subscription-attributes-tag", + TagEventNotificationGroup: "event-notification-attributes-tag", + TagResourceGroup: "resource-attributes-tag", + TagDocumentGroup: "document-attributes-tag", + TagSystemGroup: "system-attributes-tag", + + // Value tags + TagUnsupportedValue: "unsupported", + TagDefault: "default", + TagUnknown: "unknown", + TagNoValue: "no-value", + TagNotSettable: "not-settable", + TagDeleteAttr: "delete-attribute", + TagAdminDefine: "admin-define", + TagInteger: "integer", + TagBoolean: "boolean", + TagEnum: "enum", + TagString: "octetString", + TagDateTime: "dateTime", + TagResolution: "resolution", + TagRange: "rangeOfInteger", + TagBeginCollection: "collection", + TagTextLang: "textWithLanguage", + TagNameLang: "nameWithLanguage", + TagEndCollection: "endCollection", + TagText: "textWithoutLanguage", + TagName: "nameWithoutLanguage", + TagKeyword: "keyword", + TagURI: "uri", + TagURIScheme: "uriScheme", + TagCharset: "charset", + TagLanguage: "naturalLanguage", + TagMimeType: "mimeMediaType", + TagMemberName: "memberAttrName", } diff -Nru golang-github-openprinting-goipp-1.0.0/type.go golang-github-openprinting-goipp-1.1.0/type.go --- golang-github-openprinting-goipp-1.0.0/type.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/type.go 2023-12-05 12:34:36.000000000 +0100 @@ -15,46 +15,45 @@ // Type enumerates all possible value types type Type int +// Type values const ( - TypeInvalid Type = -1 - TypeVoid Type = iota - TypeInteger - TypeBoolean - TypeString - TypeDateTime - TypeResolution - TypeRange - TypeTextWithLang - TypeBinary - TypeCollection + TypeInvalid Type = -1 // Invalid Value type + TypeVoid Type = iota // Value is Void + TypeInteger // Value is Integer + TypeBoolean // Value is Boolean + TypeString // Value is String + TypeDateTime // Value is Time + TypeResolution // Value is Resolution + TypeRange // Value is Range + TypeTextWithLang // Value is TextWithLang + TypeBinary // Value is Binary + TypeCollection // Value is Collection ) // String converts Type to string, for debugging func (t Type) String() string { - switch t { - case TypeInvalid: + if t == TypeInvalid { return "Invalid" - case TypeVoid: - return "Void" - case TypeInteger: - return "Integer" - case TypeBoolean: - return "Boolean" - case TypeString: - return "String" - case TypeDateTime: - return "DateTime" - case TypeResolution: - return "Resolution" - case TypeRange: - return "Range" - case TypeTextWithLang: - return "TextWithLang" - case TypeBinary: - return "Binary" - case TypeCollection: - return "Collection" } - return fmt.Sprintf("Unknown type %d", int(t)) + if 0 <= t && int(t) < len(typeNames) { + if s := typeNames[t]; s != "" { + return s + } + } + + return fmt.Sprintf("0x%4.4x", uint(t)) +} + +var typeNames = [...]string{ + TypeVoid: "Void", + TypeInteger: "Integer", + TypeBoolean: "Boolean", + TypeString: "String", + TypeDateTime: "DateTime", + TypeResolution: "Resolution", + TypeRange: "Range", + TypeTextWithLang: "TextWithLang", + TypeBinary: "Binary", + TypeCollection: "Collection", } diff -Nru golang-github-openprinting-goipp-1.0.0/value.go golang-github-openprinting-goipp-1.1.0/value.go --- golang-github-openprinting-goipp-1.0.0/value.go 2020-05-17 21:53:33.000000000 +0200 +++ golang-github-openprinting-goipp-1.1.0/value.go 2023-12-05 12:34:36.000000000 +0100 @@ -17,13 +17,14 @@ "time" ) -// Values represents a slice of Attribute values with tags +// Values represents a sequence of values with tags. +// Usually Values used as a "payload" of Attribute type Values []struct { T Tag // The tag V Value // The value } -// Add value to Values +// Add Value to Values func (values *Values) Add(t Tag, v Value) { *values = append(*values, struct { T Tag @@ -31,7 +32,7 @@ }{t, v}) } -// String() converts Values to string +// String converts Values to string func (values Values) String() string { if len(values) == 1 { return values[0].V.String() @@ -50,7 +51,7 @@ return buf.String() } -// Equal checks that two Values are equal +// Equal performs deep check of equality of two Values func (values Values) Equal(values2 Values) bool { if len(values) != len(values2) { return false @@ -67,6 +68,9 @@ } // Value represents an attribute value +// +// IPP uses typed values, and type of each value is unambiguously +// defined by the attribute tag type Value interface { String() string Type() Type @@ -75,6 +79,9 @@ } // ValueEqual checks if two values are equal +// +// Equality means that types and values are equal. For structured +// values, like Collection, deep comparison is performed func ValueEqual(v1, v2 Value) bool { if v1.Type() != v2.Type() { return false @@ -94,16 +101,16 @@ return v1 == v2 } -// Void represents "no value" +// Void is the Value that represents "no value" // // Use with: TagUnsupportedValue, TagDefault, TagUnknown, // TagNotSettable, TagDeleteAttr, TagAdminDefine type Void struct{} -// String() converts Void Value to string +// String converts Void Value to string func (Void) String() string { return "" } -// Type returns type of Value +// Type returns type of Value (TypeVoid for Void) func (Void) Type() Type { return TypeVoid } // Encode Void Value into wire format @@ -116,15 +123,15 @@ return Void{}, nil } -// Integer represents an Integer Value +// Integer is the Value that represents 32-bit signed int // // Use with: TagInteger, TagEnum type Integer int32 -// String() converts Integer value to string +// String converts Integer value to string func (v Integer) String() string { return fmt.Sprintf("%d", int32(v)) } -// Type returns type of Value +// Type returns type of Value (TypeInteger for Integer) func (Integer) Type() Type { return TypeInteger } // Encode Integer Value into wire format @@ -141,15 +148,15 @@ return Integer(binary.BigEndian.Uint32(data)), nil } -// Boolean represents a boolean Value +// Boolean is the Value that contains true of false // // Use with: TagBoolean type Boolean bool -// String() converts Boolean value to string +// String converts Boolean value to string func (v Boolean) String() string { return fmt.Sprintf("%t", bool(v)) } -// Type returns type of Value +// Type returns type of Value (TypeBoolean for Boolean) func (Boolean) Type() Type { return TypeBoolean } // Encode Boolean Value into wire format @@ -169,16 +176,16 @@ return Boolean(data[0] != 0), nil } -// String represents a string Value +// String is the Value that represents string of text // // Use with: TagText, TagName, TagReservedString, TagKeyword, TagURI, // TagURIScheme, TagCharset, TagLanguage, TagMimeType, TagMemberName type String string -// String() converts String value to string +// String converts String value to string func (v String) String() string { return string(v) } -// Type returns type of Value +// Type returns type of Value (TypeString for String) func (String) Type() Type { return TypeString } // Encode String Value into wire format @@ -191,15 +198,15 @@ return String(data), nil } -// Time represents a DateTime Value +// Time is the Value that represents DataTime // // Use with: TagTime type Time struct{ time.Time } -// String() converts Time value to string +// String converts Time value to string func (v Time) String() string { return v.Time.Format(time.RFC3339) } -// Type returns type of Value +// Type returns type of Value (TypeDateTime for Time) func (Time) Type() Type { return TypeDateTime } // Encode Time Value into wire format @@ -249,32 +256,50 @@ // Decode Time Value from wire format func (Time) decode(data []byte) (Value, error) { // Check size - if len(data) != 9 && len(data) != 11 { - return nil, errors.New("value must be 9 or 11 bytes") + if len(data) != 11 { + return nil, errors.New("value must be 11 bytes") } - // Decode time zone - var l *time.Location + // Validate ranges + var err error switch { - case len(data) == 9: - l = time.UTC - case data[8] == '+', data[8] == '-': - name := fmt.Sprintf("UTC%c%d", data[8], data[9]) - if data[10] != 0 { - name += fmt.Sprintf(":%d", data[10]) - } + case data[2] < 1 || data[2] > 12: + err = fmt.Errorf("bad month %d", data[2]) + case data[3] < 1 || data[3] > 31: + err = fmt.Errorf("bad day %d", data[3]) + case data[4] > 23: + err = fmt.Errorf("bad hours %d", data[4]) + case data[5] > 59: + err = fmt.Errorf("bad minutes %d", data[5]) + case data[6] > 60: + err = fmt.Errorf("bad seconds %d", data[6]) + case data[7] > 9: + err = fmt.Errorf("bad deciseconds %d", data[7]) + case data[8] != '+' && data[8] != '-': + return nil, errors.New("bad UTC sign") + case data[9] > 11: + err = fmt.Errorf("bad UTC hours %d", data[9]) + case data[10] > 59: + err = fmt.Errorf("bad UTC minutes %d", data[10]) + } - off := 3600*int(data[9]) + 60*int(data[10]) - if data[8] == '-' { - off = -off - } + if err != nil { + return Time{}, err + } - l = time.FixedZone(name, off) + // Decode time zone + tzName := fmt.Sprintf("UTC%c%d", data[8], data[9]) + if data[10] != 0 { + tzName += fmt.Sprintf(":%d", data[10]) + } - default: - return nil, errors.New("invalid data format") + tzOff := 3600*int(data[9]) + 60*int(data[10]) + if data[8] == '-' { + tzOff = -tzOff } + tz := time.FixedZone(tzName, tzOff) + // Decode time t := time.Date( int(binary.BigEndian.Uint16(data[0:2])), // year @@ -284,13 +309,13 @@ int(data[5]), // min int(data[6]), // sec int(data[7])*100000000, // nsec - l, // time zone + tz, // time zone ) return Time{t}, nil } -// Resolution represents a resolution Value +// Resolution is the Value that represents image resolution. // // Use with: TagResolution type Resolution struct { @@ -298,12 +323,12 @@ Units Units // Resolution units } -// String() converts Resolution value to string +// String converts Resolution value to string func (v Resolution) String() string { return fmt.Sprintf("%dx%d%s", v.Xres, v.Yres, v.Units) } -// Type returns type of Value +// Type returns type of Value (TypeResolution for Resolution) func (Resolution) Type() Type { return TypeResolution } // Encode Resolution Value into wire format @@ -339,12 +364,13 @@ // Units represents resolution units type Units uint8 +// Resolution units codes const ( UnitsDpi Units = 3 // Dots per inch UnitsDpcm Units = 4 // Dots per cm ) -// String() converts Units to string +// String converts Units to string func (u Units) String() string { switch u { case UnitsDpi: @@ -356,19 +382,19 @@ } } -// Range represents a range of integers Value +// Range is the Value that represents a range of 32-bit signed integers // // Use with: TagRange type Range struct { Lower, Upper int // Lower/upper bounds } -// String() converts Range value to string +// String converts Range value to string func (v Range) String() string { return fmt.Sprintf("%d-%d", v.Lower, v.Upper) } -// Type returns type of Value +// Type returns type of Value (TypeRange for Range) func (Range) Type() Type { return TypeRange } // Encode Range Value into wire format @@ -388,7 +414,7 @@ // Decode Range Value from wire format func (Range) decode(data []byte) (Value, error) { if len(data) != 8 { - return nil, errors.New("value must be 9 bytes") + return nil, errors.New("value must be 8 bytes") } return Range{ @@ -397,19 +423,20 @@ }, nil } -// TextWithLang represents a combination of two strings: -// one is a name of natural language and second is a text -// on this language +// TextWithLang is the Value that represents a combination +// of two strings: +// * text on some natural language (i.e., "hello") +// * name of that language (i.e., "en") // // Use with: TagTextLang, TagNameLang type TextWithLang struct { Lang, Text string // Language and text } -// String() converts TextWithLang value to string +// String converts TextWithLang value to string func (v TextWithLang) String() string { return v.Text + " [" + v.Lang + "]" } -// Type returns type of Value +// Type returns type of Value (TypeTextWithLang for TextWithLang) func (TextWithLang) Type() Type { return TypeTextWithLang } // Encode TextWithLang Value into wire format @@ -449,7 +476,7 @@ // Unpack language length if len(data) < 2 { - goto ERROR + return nil, errors.New("truncated language length") } langLen = int(binary.BigEndian.Uint16(data[0:2])) @@ -457,7 +484,7 @@ // Unpack language value if len(data) < langLen { - goto ERROR + return nil, errors.New("truncated language name") } lang = string(data[:langLen]) @@ -465,7 +492,7 @@ // Unpack text length if len(data) < 2 { - goto ERROR + return nil, errors.New("truncated text length") } textLen = int(binary.BigEndian.Uint16(data[0:2])) @@ -473,7 +500,7 @@ // Unpack text value if len(data) < textLen { - goto ERROR + return nil, errors.New("truncated text string") } text = string(data[:textLen]) @@ -481,25 +508,23 @@ // We must have consumed all bytes at this point if len(data) != 0 { - goto ERROR + return nil, fmt.Errorf("extra %d bytes at the end of value", + len(data)) } // Return a value return TextWithLang{Lang: lang, Text: text}, nil - -ERROR: - return nil, errors.New("invalid data format") } -// Binary represents a raw binary Value +// Binary is the Value that represents a raw binary data type Binary []byte -// String() converts Range value to string +// String converts Binary value to string func (v Binary) String() string { return fmt.Sprintf("%x", []byte(v)) } -// Type returns type of Value +// Type returns type of Value (TypeBinary for Binary) func (Binary) Type() Type { return TypeBinary } // Encode TextWithLang Value into wire format @@ -512,22 +537,22 @@ return Binary(data), nil } -// Collection represents a collection of attributes +// Collection is the Value that represents collection of attributes // // Use with: TagBeginCollection type Collection Attributes // Add Attribute to Attributes -func (collection *Collection) Add(attr Attribute) { - *collection = append(*collection, attr) +func (v *Collection) Add(attr Attribute) { + *v = append(*v, attr) } // Equal checks that two collections are equal -func (c1 Collection) Equal(c2 Attributes) bool { - return Attributes(c1).Equal(Attributes(c2)) +func (v Collection) Equal(v2 Attributes) bool { + return Attributes(v).Equal(Attributes(v2)) } -// String() converts Collection to string +// String converts Collection to string func (v Collection) String() string { var buf bytes.Buffer buf.Write([]byte("{")) @@ -542,11 +567,11 @@ return buf.String() } -// Type returns type of Value +// Type returns type of Value (TypeCollection for Collection) func (Collection) Type() Type { return TypeCollection } // Encode Collection Value into wire format -func (v Collection) encode() ([]byte, error) { +func (Collection) encode() ([]byte, error) { // Note, TagBeginCollection attribute contains // no data, collection itself handled the different way return []byte{}, nil