├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── constants.js ├── index.js ├── package.json ├── status-codes.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5' 4 | - '4' 5 | - '0.12' 6 | - '0.10' 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Watson Steen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipp-encoder 2 | 3 | Internet Printing Protocol (IPP) encoder and decoder. 4 | 5 | This module can be used to implement either a printing client or a 6 | printer server. 7 | 8 | [![Build status](https://travis-ci.org/watson/ipp-encoder.svg?branch=master)](https://travis-ci.org/watson/ipp-encoder) 9 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 10 | [![abstract-encoding](https://img.shields.io/badge/abstract--encoding-compliant-brightgreen.svg?style=flat)](https://github.com/mafintosh/abstract-encoding) 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install ipp-encoder 16 | ``` 17 | 18 | ## Usage 19 | 20 | Printer server example: 21 | 22 | ```js 23 | var ipp = require('ipp-encoder') 24 | var C = ipp.CONSTANTS 25 | 26 | // decode binary buffer from IPP client 27 | var decoded = ipp.request.decode(buf) 28 | 29 | // ...handle request... 30 | 31 | // prepare response 32 | var response = { 33 | statusCode: C.SUCCESSFUL_OK, // set `operationId` instead if encoding a request 34 | requestId: decoded.requestId, 35 | groups: [ 36 | { tag: C.OPERATION_ATTRIBUTES_TAG, attributes: [ 37 | { tag: C.CHARSET, name: 'attributes-charset', value: 'utf-8' }, 38 | { tag: C.NATURAL_LANG, name: 'attributes-natural-language', value: 'en-us' }, 39 | { tag: C.TEXT_WITH_LANG, name: 'status-message', value: { lang: 'en-us', value: 'successful-ok' } } 40 | ] }, 41 | { tag: C.JOB_ATTRIBUTES_TAG, attributes: [ 42 | { tag: C.INTEGER, name: 'job-id', value: 147 }, 43 | { tag: C.NAME_WITH_LANG, name: 'job-name', value: { lang: 'en-us', value: 'Foobar' } } 44 | ] } 45 | ] 46 | } 47 | 48 | // encode response to binary buffer 49 | ipp.response.encode(response) // 50 | ``` 51 | 52 | ## API 53 | 54 | ### `ipp.CONSTANTS` 55 | 56 | An object containing IPP constants. See `constants.js` for the complete 57 | list. 58 | 59 | ### `ipp.STATUS_CODES` 60 | 61 | Map of IPP status codes to descriptive strings. See `status-codes.js` 62 | for the complete list. 63 | 64 | ### `ipp.request.decode(buffer[, start][, end])` 65 | 66 | Decode an IPP request buffer and returns the request object. 67 | 68 | Options: 69 | 70 | - `buffer` - The buffer containing the request 71 | - `start` - An optional start-offset from where to start parsing the 72 | request (defaults to `0`) 73 | - `end` - An optional end-offset specifying at which byte to end the 74 | decoding (defaults to `buffer.length`) 75 | 76 | Request object structure: 77 | 78 | ```js 79 | { 80 | version: { 81 | major: 1, 82 | minor: 1 83 | }, 84 | operationId: 0x02, 85 | requestId: 1, 86 | groups: [ 87 | { tag: C.OPERATION_ATTRIBUTES_TAG, attributes: [ 88 | { tag: 0x47, name: 'attributes-charset', value: ['utf-8'] }, 89 | { tag: 0x48, name: 'attributes-natural-language', value: ['en-us'] }, 90 | { tag: 0x45, name: 'printer-uri', value: ['ipp://watson.local.:3000/'] }, 91 | { tag: 0x42, name: 'job-name', value: ['foobar'] }, 92 | { tag: 0x22, name: 'ipp-attribute-fidelity', value: [true] } 93 | ] }, 94 | { tag: C.JOB_ATTRIBUTES_TAG, attributes: [ 95 | { tag: 0x21, name: 'copies', value: [20] }, 96 | { tag: 0x44, name: 'sides', value: ['two-sided-long-edge'] } 97 | ] } 98 | ] 99 | } 100 | ``` 101 | 102 | After decoding `ipp.request.decode.bytes` is set to the amount of bytes 103 | used to decode the object. 104 | 105 | Note that any data after the IPP headers are ignored. 106 | 107 | ### `ipp.request.encode(obj[, buffer][, offset])` 108 | 109 | Encode an IPP request object and returns en encoded buffer. 110 | 111 | Options: 112 | 113 | - `obj` - The object containing the request 114 | - `buffer` - An optional buffer in which to write the encoded request 115 | - `offset` - An optional offset from where to start writing the encoded 116 | data in the buffer (defaults to `0`) 117 | 118 | Response object structure: 119 | 120 | ```js 121 | { 122 | statusCode: 0x00, 123 | requestId: 1, 124 | groups: [ 125 | { tag: C.OPERATION_ATTRIBUTES_TAG, attributes: [ 126 | { tag: 0x47, name: 'attributes-charset', value: ['utf-8'] }, 127 | { tag: 0x48, name: 'attributes-natural-language', value: ['en-us'] }, 128 | { tag: 0x41, name: 'status-message', value: ['successful-ok'] } 129 | ] }, 130 | { tag: C.JOB_ATTRIBUTES_TAG, attributes: [ 131 | { tag: 0x21, name: 'job-id', value: [147] }, 132 | { tag: 0x45, name: 'job-uri', value: ['ipp://watson.local.:3000/123'] } 133 | { tag: 0x44, name: 'job-state', value: ['pending'] } 134 | ] } 135 | ] 136 | } 137 | ``` 138 | 139 | It's possible to provide a custom IPP version in the same format is seen 140 | in the request. Default IPP version is 1.1. 141 | 142 | After encoding, `ipp.request.encode.bytes` is set to the amount of bytes 143 | used to encode the object. 144 | 145 | ### `ipp.request.encodingLength(obj)` 146 | 147 | Returns the number of bytes it would take to encode the given IPP 148 | request object. 149 | 150 | ### `ipp.response.decode(buffer[, start][, end])` 151 | 152 | Same as `ipp.request.decode()`, but for IPP responses. 153 | 154 | After decoding `ipp.response.decode.bytes` is set to the amount of bytes 155 | used to decode the object. 156 | 157 | ### `ipp.response.encode(obj[, buffer][, offset])` 158 | 159 | Same as `ipp.request.encode()`, but for IPP responses. 160 | 161 | After encoding, `ipp.response.encode.bytes` is set to the amount of bytes 162 | used to encode the object. 163 | 164 | ### `ipp.response.encodingLength(obj)` 165 | 166 | Same as `ipp.request.encodingLength()`, but for IPP responses. 167 | 168 | ## License 169 | 170 | MIT 171 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | // Values 5 | FALSE: 0x00, 6 | TRUE: 0x01, 7 | 8 | // Operation Ids 9 | PRINT_JOB: 0x02, 10 | PRINT_URI: 0x03, 11 | VALIDATE_JOB: 0x04, 12 | CREATE_JOB: 0x05, 13 | SEND_DOCUMENT: 0x06, 14 | SEND_URI: 0x07, 15 | CANCEL_JOB: 0x08, 16 | GET_JOB_ATTRIBUTES: 0x09, 17 | GET_JOBS: 0x0a, 18 | GET_PRINTER_ATTRIBUTES: 0x0b, 19 | HOLD_JOB: 0x0c, 20 | RELEASE_JOB: 0x0d, 21 | RESTART_JOB: 0x0e, 22 | PAUSE_PRINTER: 0x10, 23 | RESUME_PRINTER: 0x11, 24 | PURGE_JOBS: 0x12, 25 | 26 | // Delimiter Tags 27 | OPERATION_ATTRIBUTES_TAG: 0x01, 28 | JOB_ATTRIBUTES_TAG: 0x02, 29 | END_OF_ATTRIBUTES_TAG: 0x03, 30 | PRINTER_ATTRIBUTES_TAG: 0x04, 31 | UNSUPPORTED_ATTRIBUTES_TAG: 0x05, 32 | 33 | // Value Tags (out-of-band) 34 | UNSUPPORTED: 0x10, 35 | UNKNOWN: 0x12, 36 | NO_VALUE: 0x13, 37 | 38 | // Value Tags (integer) 39 | INTEGER: 0x21, 40 | BOOLEAN: 0x22, 41 | ENUM: 0x23, 42 | 43 | // Value Tags (octet-string) 44 | OCTET_STRING: 0x30, // with unspecified format 45 | DATE_TIME: 0x31, 46 | RESOLUTION: 0x32, 47 | RANGE_OF_INTEGER: 0x33, 48 | TEXT_WITH_LANG: 0x35, 49 | NAME_WITH_LANG: 0x36, 50 | 51 | // Value Tags (character-string) 52 | TEXT_WITHOUT_LANG: 0x41, 53 | NAME_WITHOUT_LANG: 0x42, 54 | KEYWORD: 0x44, 55 | URI: 0x45, 56 | URI_SCHEME: 0x46, 57 | CHARSET: 0x47, 58 | NATURAL_LANG: 0x48, 59 | MIME_MEDIA_TYPE: 0x49, 60 | 61 | // Successful Status Codes 62 | SUCCESSFUL_OK: 0x0000, 63 | SUCCESSFUL_OK_IGNORED_OR_SUBSTITUTED_ATTRIBUTES: 0x0001, 64 | SUCCESSFUL_OK_CONFLICTING_ATTRIBUTES: 0x0002, 65 | 66 | // Client Error Status Codes 67 | CLIENT_ERROR_BAD_REQUEST: 0x0400, 68 | CLIENT_ERROR_FORBIDDEN: 0x0401, 69 | CLIENT_ERROR_NOT_AUTHENTICATED: 0x0402, 70 | CLIENT_ERROR_NOT_AUTHORIZED: 0x0403, 71 | CLIENT_ERROR_NOT_POSSIBLE: 0x0404, 72 | CLIENT_ERROR_TIMEOUT: 0x0405, 73 | CLIENT_ERROR_NOT_FOUND: 0x0406, 74 | CLIENT_ERROR_GONE: 0x0407, 75 | CLIENT_ERROR_REQUEST_ENTITY_TOO_LARGE: 0x0408, 76 | CLIENT_ERROR_REQUEST_VALUE_TOO_LONG: 0x0409, 77 | CLIENT_ERROR_DOCUMENT_FORMAT_NOT_SUPPORTED: 0x040a, 78 | CLIENT_ERROR_ATTRIBUTES_OR_VALUES_NOT_SUPPORTED: 0x040b, 79 | CLIENT_ERROR_URI_SCHEME_NOT_SUPPORTED: 0x040c, 80 | CLIENT_ERROR_CHARSET_NOT_SUPPORTED: 0x040d, 81 | CLIENT_ERROR_CONFLICTING_ATTRIBUTES: 0x040e, 82 | CLIENT_ERROR_COMPRESSION_NOT_SUPPORTED: 0x040f, 83 | CLIENT_ERROR_COMPRESSION_ERROR: 0x0410, 84 | CLIENT_ERROR_DOCUMENT_FORMAT_ERROR: 0x0411, 85 | CLIENT_ERROR_DOCUMENT_ACCESS_ERROR: 0x0412, 86 | 87 | // Server Error Status Codes 88 | SERVER_ERROR_INTERNAL_ERROR: 0x0500, 89 | SERVER_ERROR_OPERATION_NOT_SUPPORTED: 0x0501, 90 | SERVER_ERROR_SERVICE_UNAVAILABLE: 0x0502, 91 | SERVER_ERROR_VERSION_NOT_SUPPORTED: 0x0503, 92 | SERVER_ERROR_DEVICE_ERROR: 0x0504, 93 | SERVER_ERROR_TEMPORARY_ERROR: 0x0505, 94 | SERVER_ERROR_NOT_ACCEPTING_JOBS: 0x0506, 95 | SERVER_ERROR_BUSY: 0x0507, 96 | SERVER_ERROR_JOB_CANCELED: 0x0508, 97 | SERVER_ERROR_MULTIPLE_DOCUMENT_JOBS_NOT_SUPPORTED: 0x0509, 98 | 99 | // Printer states 100 | PRINTER_IDLE: 3, 101 | PRINTER_PROCESSING: 4, 102 | PRINTER_STOPPED: 5, 103 | 104 | // Job states 105 | JOB_PENDING: 3, 106 | JOB_PENDING_HELD: 4, 107 | JOB_PROCESSING: 5, 108 | JOB_PROCESSING_STOPPED: 6, 109 | JOB_CANCELED: 7, 110 | JOB_ABORTED: 8, 111 | JOB_COMPLETED: 9 112 | } 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var C = require('./constants') 4 | 5 | exports.CONSTANTS = C 6 | exports.STATUS_CODES = require('./status-codes') 7 | 8 | exports.request = { 9 | decode: function () { 10 | var obj = decode.apply(null, arguments) 11 | exports.request.decode.bytes = decode.bytes 12 | obj.operationId = obj._oprationIdOrStatusCode 13 | delete obj._oprationIdOrStatusCode 14 | return obj 15 | }, 16 | encode: encode, 17 | encodingLength: encodingLength 18 | } 19 | 20 | exports.response = { 21 | decode: function () { 22 | var obj = decode.apply(null, arguments) 23 | exports.response.decode.bytes = decode.bytes 24 | obj.statusCode = obj._oprationIdOrStatusCode 25 | delete obj._oprationIdOrStatusCode 26 | return obj 27 | }, 28 | encode: encode, 29 | encodingLength: encodingLength 30 | } 31 | 32 | function decode (buf, start, end) { 33 | if (!start) start = 0 34 | if (!end) end = buf.length 35 | var offset = start 36 | 37 | var obj = { 38 | version: {}, 39 | groups: [] 40 | } 41 | 42 | obj.version.major = buf.readInt8(offset++) 43 | obj.version.minor = buf.readInt8(offset++) 44 | obj._oprationIdOrStatusCode = buf.readInt16BE(offset) 45 | offset += 2 46 | obj.requestId = buf.readInt32BE(offset) 47 | offset += 4 48 | 49 | // attribute groups 50 | var tag = buf.readInt8(offset++) // delimiter-tag 51 | while (tag !== C.END_OF_ATTRIBUTES_TAG && offset < end) { 52 | var group = { tag: tag, attributes: [] } 53 | 54 | // attribute-with-one-value or additional-value 55 | tag = buf.readInt8(offset++) // value-tag 56 | while (tag > 0x0f) { 57 | var name = str.decode(buf, offset) 58 | offset += str.decode.bytes 59 | 60 | var val 61 | switch (tag) { 62 | case C.INTEGER: 63 | val = tint.decode(buf, offset) 64 | offset += tint.decode.bytes 65 | break 66 | case C.BOOLEAN: 67 | val = tbool.decode(buf, offset) 68 | offset += tbool.decode.bytes 69 | break 70 | case C.ENUM: 71 | val = tenum.decode(buf, offset) 72 | offset += tenum.decode.bytes 73 | break 74 | case C.DATE_TIME: 75 | val = tdatetime.decode(buf, offset) 76 | offset += tdatetime.decode.bytes 77 | break 78 | case C.TEXT_WITH_LANG: 79 | case C.NAME_WITH_LANG: 80 | val = langstr.decode(buf, offset) 81 | offset += langstr.decode.bytes 82 | break 83 | default: 84 | val = str.decode(buf, offset) 85 | offset += str.decode.bytes 86 | } 87 | 88 | if (!name) { 89 | attr.value.push(val) 90 | } else { 91 | var attr = { tag: tag, name: name, value: [val] } 92 | group.attributes.push(attr) 93 | } 94 | 95 | tag = buf.readInt8(offset++) // delimiter-tag or value-tag 96 | } 97 | 98 | obj.groups.push(group) 99 | } 100 | 101 | decode.bytes = offset - start 102 | 103 | return obj 104 | } 105 | 106 | function encode (obj, buf, offset) { 107 | if (!buf) buf = new Buffer(encodingLength(obj)) 108 | if (!offset) offset = 0 109 | var oldOffset = offset 110 | 111 | buf.writeInt8(obj.version ? obj.version.major : 1, offset++) 112 | buf.writeInt8(obj.version ? obj.version.minor : 1, offset++) 113 | 114 | buf.writeInt16BE(obj.statusCode === undefined ? obj.operationId : obj.statusCode, offset) 115 | offset += 2 116 | 117 | buf.writeInt32BE(obj.requestId, offset) 118 | offset += 4 119 | 120 | if (obj.groups) { 121 | obj.groups.forEach(function (group) { 122 | buf.writeInt8(group.tag, offset++) 123 | 124 | group.attributes.forEach(function (attr) { 125 | var value = Array.isArray(attr.value) ? attr.value : [attr.value] 126 | value.forEach(function (val, i) { 127 | buf.writeInt8(attr.tag, offset++) 128 | 129 | str.encode(i ? '' : attr.name, buf, offset) 130 | offset += str.encode.bytes 131 | 132 | switch (attr.tag) { 133 | case C.INTEGER: 134 | tint.encode(val, buf, offset) 135 | offset += tint.encode.bytes 136 | break 137 | case C.BOOLEAN: 138 | tbool.encode(val, buf, offset) 139 | offset += tbool.encode.bytes 140 | break 141 | case C.ENUM: 142 | tenum.encode(val, buf, offset) 143 | offset += tenum.encode.bytes 144 | break 145 | case C.DATE_TIME: 146 | tdatetime.encode(val, buf, offset) 147 | offset += tdatetime.encode.bytes 148 | break 149 | case C.TEXT_WITH_LANG: 150 | case C.NAME_WITH_LANG: 151 | langstr.encode(val, buf, offset) 152 | offset += langstr.encode.bytes 153 | break 154 | default: 155 | str.encode(val, buf, offset) 156 | offset += str.encode.bytes 157 | } 158 | }) 159 | }) 160 | }) 161 | } 162 | 163 | buf.writeInt8(C.END_OF_ATTRIBUTES_TAG, offset++) 164 | 165 | if (obj.data) offset += obj.data.copy(buf, offset) 166 | 167 | encode.bytes = offset - oldOffset 168 | 169 | return buf 170 | } 171 | 172 | function encodingLength (obj) { 173 | var len = 8 // version-number + status-code + request-id 174 | 175 | if (obj.groups) { 176 | len += obj.groups.reduce(function (len, group) { 177 | len += 1 // begin-attribute-group-tag 178 | len += group.attributes.reduce(function (len, attr) { 179 | var value = Array.isArray(attr.value) ? attr.value : [attr.value] 180 | len += value.reduce(function (len, val) { 181 | len += 1 // value-tag 182 | len += str.encodingLength(len === 1 ? attr.name : '') 183 | 184 | switch (attr.tag) { 185 | case C.INTEGER: return len + tint.encodingLength(val) 186 | case C.BOOLEAN: return len + tbool.encodingLength(val) 187 | case C.ENUM: return len + tenum.encodingLength(val) 188 | case C.DATE_TIME: return len + tdatetime.encodingLength(val) 189 | case C.TEXT_WITH_LANG: 190 | case C.NAME_WITH_LANG: return len + langstr.encodingLength(val) 191 | default: return len + str.encodingLength(val) 192 | } 193 | }, 0) 194 | 195 | return len 196 | }, 0) 197 | return len 198 | }, 0) 199 | } 200 | 201 | len++ // end-of-attributes-tag 202 | 203 | if (obj.data) len += obj.data.length 204 | 205 | return len 206 | } 207 | 208 | var tint = {} 209 | 210 | tint.decode = function (buf, offset) { 211 | var i = buf.readInt32BE(offset + 2) 212 | tint.decode.bytes = 6 213 | return i 214 | } 215 | 216 | tint.encode = function (i, buf, offset) { 217 | buf.writeInt16BE(4, offset) 218 | buf.writeInt32BE(i, offset + 2) 219 | tint.encode.bytes = 6 220 | return buf 221 | } 222 | 223 | tint.encodingLength = function (s) { 224 | return 6 225 | } 226 | 227 | var tenum = {} 228 | 229 | tenum.decode = function (buf, offset) { 230 | var i = buf.readInt32BE(offset + 2) 231 | tenum.decode.bytes = 6 232 | return i 233 | } 234 | 235 | tenum.encode = function (i, buf, offset) { 236 | buf.writeInt16BE(4, offset) 237 | buf.writeInt32BE(i, offset + 2) 238 | tenum.encode.bytes = 6 239 | return buf 240 | } 241 | 242 | tenum.encodingLength = function (s) { 243 | return 6 244 | } 245 | 246 | var tbool = {} 247 | 248 | tbool.decode = function (buf, offset) { 249 | var b = buf.readInt8(offset + 2) === C.TRUE 250 | tbool.decode.bytes = 3 251 | return b 252 | } 253 | 254 | tbool.encode = function (b, buf, offset) { 255 | buf.writeInt16BE(1, offset) 256 | buf.writeInt8(b ? C.TRUE : C.FALSE, offset + 2) 257 | tbool.encode.bytes = 3 258 | return buf 259 | } 260 | 261 | tbool.encodingLength = function (s) { 262 | return 3 263 | } 264 | 265 | var langstr = {} 266 | 267 | langstr.decode = function (buf, offset) { 268 | var oldOffset = offset 269 | offset += 2 270 | var lang = str.decode(buf, offset) 271 | offset += str.decode.bytes 272 | var val = str.decode(buf, offset) 273 | offset += str.decode.bytes 274 | langstr.decode.bytes = offset - oldOffset 275 | return { lang: lang, value: val } 276 | } 277 | 278 | langstr.encode = function (obj, buf, offset) { 279 | str.encode(obj.lang, buf, offset + 2) 280 | var len = str.encode.bytes 281 | str.encode(obj.value, buf, offset + 2 + len) 282 | len += str.encode.bytes 283 | buf.writeInt16BE(len, offset) 284 | langstr.encode.bytes = len + 2 285 | return buf 286 | } 287 | 288 | langstr.encodingLength = function (obj) { 289 | return Buffer.byteLength(obj.lang) + Buffer.byteLength(obj.value) + 6 290 | } 291 | 292 | var str = {} 293 | 294 | str.decode = function (buf, offset) { 295 | var len = buf.readInt16BE(offset) 296 | var s = buf.toString('utf-8', offset + 2, offset + 2 + len) 297 | str.decode.bytes = len + 2 298 | return s 299 | } 300 | 301 | str.encode = function (s, buf, offset) { 302 | var len = buf.write(s, offset + 2) 303 | buf.writeInt16BE(len, offset) 304 | str.encode.bytes = len + 2 305 | return buf 306 | } 307 | 308 | str.encodingLength = function (s) { 309 | return Buffer.byteLength(s) + 2 310 | } 311 | 312 | var tdatetime = {} 313 | 314 | tdatetime.decode = function (buf, offset) { 315 | var drift = (buf.readInt8(offset + 11) * 60) + buf.readInt8(offset + 12) 316 | if (buf.slice(offset + 10, offset + 11) === '+') drift = drift * -1 317 | 318 | var d = new Date(Date.UTC( 319 | buf.readInt16BE(offset + 2), 320 | buf.readInt8(offset + 4) - 1, 321 | buf.readInt8(offset + 5), 322 | buf.readInt8(offset + 6), 323 | buf.readInt8(offset + 7) + drift, 324 | buf.readInt8(offset + 8), 325 | buf.readInt8(offset + 9) * 100 326 | )) 327 | 328 | tdatetime.decode.bytes = 13 329 | 330 | return d 331 | } 332 | 333 | tdatetime.encode = function (d, buf, offset) { 334 | buf.writeInt16BE(11, offset) 335 | buf.writeInt16BE(d.getFullYear(), offset + 2) 336 | buf.writeInt8(d.getMonth() + 1, offset + 4) 337 | buf.writeInt8(d.getDate(), offset + 5) 338 | buf.writeInt8(d.getHours(), offset + 6) 339 | buf.writeInt8(d.getMinutes(), offset + 7) 340 | buf.writeInt8(d.getSeconds(), offset + 8) 341 | buf.writeInt8(Math.floor(d.getMilliseconds() / 100), offset + 9) 342 | buf.write(d.getTimezoneOffset() > 0 ? '-' : '+', offset + 10) 343 | buf.writeInt8(d.getTimezoneOffset() / 60, offset + 11) 344 | buf.writeInt8(d.getTimezoneOffset() % 60, offset + 12) 345 | 346 | tdatetime.encode.bytes = 13 347 | 348 | return buf 349 | } 350 | 351 | tdatetime.encodingLength = function (s) { 352 | return 13 353 | } 354 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipp-encoder", 3 | "version": "5.0.0", 4 | "description": "Internet Printing Protocol (IPP) encoder and decoder", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "tape": "^4.2.2", 9 | "standard": "^5.3.1" 10 | }, 11 | "scripts": { 12 | "test": "standard && tape test.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/watson/ipp-encoder.git" 17 | }, 18 | "keywords": [ 19 | "ipp", 20 | "internet", 21 | "printing", 22 | "protocol", 23 | "encode", 24 | "encoder", 25 | "decode", 26 | "decoder", 27 | "print", 28 | "printer", 29 | "parse", 30 | "parser" 31 | ], 32 | "author": "Thomas Watson Steen (https://twitter.com/wa7son)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/watson/ipp-encoder/issues" 36 | }, 37 | "homepage": "https://github.com/watson/ipp-encoder", 38 | "coordinates": [ 39 | 59.92006379999999, 40 | 10.7402093 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /status-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | // Successful Status Codes 5 | 0x0000: 'successful-ok', 6 | 0x0001: 'successful-ok-ignored-or-substituted-attributes', 7 | 0x0002: 'successful-ok-conflicting-attributes', 8 | 9 | // Client Error Status Codes 10 | 0x0400: 'client-error-bad-request', 11 | 0x0401: 'client-error-forbidden', 12 | 0x0402: 'client-error-not-authenticated', 13 | 0x0403: 'client-error-not-authorized', 14 | 0x0404: 'client-error-not-possible', 15 | 0x0405: 'client-error-timeout', 16 | 0x0406: 'client-error-not-found', 17 | 0x0407: 'client-error-gone', 18 | 0x0408: 'client-error-request-entity-too-large', 19 | 0x0409: 'client-error-request-value-too-long', 20 | 0x040a: 'client-error-document-format-not-supported', 21 | 0x040b: 'client-error-attributes-or-values-not-supported', 22 | 0x040c: 'client-error-uri-scheme-not-supported', 23 | 0x040d: 'client-error-charset-not-supported', 24 | 0x040e: 'client-error-conflicting-attributes', 25 | 0x040f: 'client-error-compression-not-supported', 26 | 0x0410: 'client-error-compression-error', 27 | 0x0411: 'client-error-document-format-error', 28 | 0x0412: 'client-error-document-access-error', 29 | 30 | // Server Error Status Codes 31 | 0x0500: 'server-error-internal-error', 32 | 0x0501: 'server-error-operation-not-supported', 33 | 0x0502: 'server-error-service-unavailable', 34 | 0x0503: 'server-error-version-not-supported', 35 | 0x0504: 'server-error-device-error', 36 | 0x0505: 'server-error-temporary-error', 37 | 0x0506: 'server-error-not-accepting-jobs', 38 | 0x0507: 'server-error-busy', 39 | 0x0508: 'server-error-job-canceled', 40 | 0x0509: 'server-error-multiple-document-jobs-not-supported' 41 | } 42 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var C = require('./constants') 5 | var ipp = require('./') 6 | 7 | test('encodingLength', function (t) { 8 | var types = ['request', 'response'] 9 | 10 | types.forEach(function (type) { 11 | t.test(type + ' minimal', function (t) { 12 | var len = ipp[type].encodingLength({}) 13 | t.deepEqual(len, 9) 14 | t.end() 15 | }) 16 | 17 | t.test(type + ' groups', function (t) { 18 | var date = new Date(2015, 11, 1, 1, 23, 45, 678) 19 | var obj = { // version + statusCode + operationId/requestId: +8 20 | groups: [ 21 | { tag: 0, attributes: [ // +1 (9) 22 | { tag: C.KEYWORD, name: 'string', value: 'foo' }, // +1+2+6+2+3=14 (23) 23 | { tag: C.KEYWORD, name: 'array', value: ['foo', 'bar'] }, // +1+2+5+2+3+1+2+0+2+3=21 (44) 24 | { tag: C.BOOLEAN, name: 'bool', value: true }, // +1+2+4+2+1=10 (54) 25 | { tag: C.ENUM, name: 'enum', value: 1 } // +1+2+4+2+4=13 (67) 26 | ] }, 27 | { tag: 1, attributes: [ // +1 (68) 28 | { tag: C.KEYWORD, name: 'string', value: ['foo'] }, // +1+2+6+2+3=14 (82) 29 | { tag: C.TEXT_WITH_LANG, name: 'text-with-language', value: { lang: 'fr-CA', value: 'fou' } }, // +1+2+18+2+2+5+2+3=35 (117) 30 | { tag: C.DATE_TIME, name: 'date-time', value: date } // +1+2+9+2+11=25 (142) 31 | ] } 32 | ] 33 | } // end tag: +1 (143) 34 | var len = ipp[type].encodingLength(obj) 35 | t.deepEqual(len, 143) 36 | t.end() 37 | }) 38 | }) 39 | }) 40 | 41 | test('encode', function (t) { 42 | t.test('request', function (t) { 43 | t.test('minimal', function (t) { 44 | var obj = { 45 | operationId: C.PRINT_JOB, 46 | requestId: 42 47 | } 48 | var encoded = ipp.request.encode(obj) 49 | var expected = new Buffer('010100020000002a03', 'hex') 50 | t.deepEqual(encoded, expected) 51 | t.end() 52 | }) 53 | }) 54 | 55 | t.test('response', function (t) { 56 | t.test('minimal', function (t) { 57 | var obj = { 58 | statusCode: C.SERVER_ERROR_VERSION_NOT_SUPPORTED, 59 | requestId: 42 60 | } 61 | var encoded = ipp.response.encode(obj) 62 | var expected = new Buffer('010105030000002a03', 'hex') 63 | t.deepEqual(encoded, expected) 64 | t.end() 65 | }) 66 | 67 | t.test('custom version', function (t) { 68 | var obj = { 69 | version: { major: 2, minor: 0 }, 70 | statusCode: C.SUCCESSFUL_OK, 71 | requestId: 42 72 | } 73 | var encoded = ipp.response.encode(obj) 74 | var expected = new Buffer('020000000000002a03', 'hex') 75 | t.deepEqual(encoded, expected) 76 | t.end() 77 | }) 78 | 79 | t.test('groups', function (t) { 80 | var date = new Date(2015, 11, 1, 1, 23, 45, 678) 81 | var sign = date.getTimezoneOffset() > 0 ? '2d' : '2b' 82 | var zone = new Buffer(2) 83 | zone.writeInt8(date.getTimezoneOffset() / 60, 0) 84 | zone.writeInt8(date.getTimezoneOffset() % 60, 1) 85 | var dateHex = '07df0c0101172d06' + sign + zone.toString('hex') 86 | 87 | var obj = { 88 | statusCode: C.SUCCESSFUL_OK, 89 | requestId: 42, 90 | groups: [ 91 | { tag: C.OPERATION_ATTRIBUTES_TAG, attributes: [ 92 | { tag: C.KEYWORD, name: 'string', value: 'foo' }, 93 | { tag: C.KEYWORD, name: 'array', value: ['foo', 'bar'] }, 94 | { tag: C.BOOLEAN, name: 'bool', value: true }, 95 | { tag: C.ENUM, name: 'enum', value: 42 } 96 | ] }, 97 | { tag: C.JOB_ATTRIBUTES_TAG, attributes: [ 98 | { tag: C.KEYWORD, name: 'string', value: ['foo'] }, 99 | { tag: C.NAME_WITH_LANG, name: 'name-with-language', value: { lang: 'fr-CA', value: 'fou' } }, 100 | { tag: C.DATE_TIME, name: 'date-time', value: date } 101 | ] } 102 | ] 103 | } 104 | var encoded = ipp.response.encode(obj) 105 | var expected = new Buffer( 106 | '0101' + // version 107 | '0000' + // statusCode 108 | '0000002a' + // requestId 109 | '01' + // delimiter tag 110 | '44' + // value tag 111 | '0006' + // name length 112 | '737472696e67' + // name 113 | '0003' + // value length 114 | '666f6f' + // value 115 | '44' + // value tag 116 | '0005' + // name length 117 | '6172726179' + // name 118 | '0003' + // value length 119 | '666f6f' + // value 120 | '44' + // value tag 121 | '0000' + // name length 122 | '' + // name 123 | '0003' + // value length 124 | '626172' + // value 125 | '22' + // value tag 126 | '0004' + // name length 127 | '626f6f6c' + // name 128 | '0001' + // value length 129 | '01' + // value 130 | '23' + // value tag 131 | '0004' + // name length 132 | '656e756d' + // name 133 | '0004' + // value length 134 | '0000002a' + // value 135 | '02' + // delimiter tag 136 | '44' + // value tag 137 | '0006' + // name length 138 | '737472696e67' + // name 139 | '0003' + // value length 140 | '666f6f' + // value 141 | '36' + // value tag 142 | '0012' + // name length 143 | '6e616d652d776974682d6c616e6775616765' + // name 144 | '000c' + // value length 145 | '0005' + // sub-value length 146 | '66722d4341' + // sub-value 147 | '0003' + // sub-value length 148 | '666f75' + // name 149 | '31' + // value tag 150 | '0009' + // name length 151 | '646174652d74696d65' + // name 152 | '000b' + // value length 153 | dateHex + // value 154 | '03', // end of attributes tag 155 | 'hex') 156 | t.deepEqual(encoded, expected) 157 | t.end() 158 | }) 159 | }) 160 | }) 161 | 162 | test('decode', function (t) { 163 | t.test('request', function (t) { 164 | t.test('minimal', function (t) { 165 | var data = new Buffer('0101000a0000002a03', 'hex') 166 | var expected = { 167 | version: { major: 1, minor: 1 }, 168 | operationId: 10, 169 | requestId: 42, 170 | groups: [] 171 | } 172 | var decoded = ipp.request.decode(data) 173 | t.deepEqual(decoded, expected) 174 | t.end() 175 | }) 176 | 177 | t.test('truncated', function (t) { 178 | var data = new Buffer('0101000a0000002a', 'hex') 179 | t.throws(function () { 180 | ipp.request.decode(data) 181 | }) 182 | t.end() 183 | }) 184 | }) 185 | }) 186 | 187 | test('encode -> decode', function (t) { 188 | var encodeDate = new Date(2015, 11, 1, 1, 23, 45, 678) 189 | var decodeDate = new Date(2015, 11, 1, 1, 23, 45, 600) 190 | var obj = { 191 | version: { major: 1, minor: 0 }, 192 | statusCode: C.SUCCESSFUL_OK, 193 | requestId: 42, 194 | groups: [ 195 | { tag: C.OPERATION_ATTRIBUTES_TAG, attributes: [ 196 | { tag: C.KEYWORD, name: 'string', value: ['foo'] }, 197 | { tag: C.KEYWORD, name: 'array', value: ['foo', 'bar'] }, 198 | { tag: C.BOOLEAN, name: 'bool', value: [true] }, 199 | { tag: C.ENUM, name: 'enum', value: [42] } 200 | ] }, 201 | { tag: C.JOB_ATTRIBUTES_TAG, attributes: [ 202 | { tag: C.KEYWORD, name: 'string', value: ['foo'] }, 203 | { tag: C.DATE_TIME, name: 'date-time', value: [encodeDate] } 204 | ] } 205 | ] 206 | } 207 | var encoded = Buffer.concat([ipp.response.encode(obj), new Buffer('foo')]) 208 | obj.groups[1].attributes[1].value[0] = decodeDate 209 | var decoded = ipp.response.decode(encoded) 210 | t.deepEqual(decoded, obj) 211 | t.end() 212 | }) 213 | --------------------------------------------------------------------------------