├── .gitignore ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── checkcodes.c ├── dmap_parser.c ├── dmap_parser.h ├── dmapprint.c └── test.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | dmapprint 3 | checkapp 4 | testapp 5 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 1.2.1 (2014-06-22) 2 | 3 | * Fixed a potential debug assertion on Windows 4 | 5 | 1.2.0 (2014-02-23) 6 | 7 | * Added mappings for some iTunes Radio codes 8 | * Added support for non-dictionary list codes such as daap.browseartistlisting 9 | * Added --raw-codes option to dmapprint 10 | * When displaying the output of /content-codes dmapprint now includes a string representation of the code's type 11 | * Fixed handling of unsigned 8-bit values 12 | 13 | 1.1.0 (2013-10-05) 14 | 15 | * Added `dmap_version()` and `dmap_version_string()` 16 | * Added --version option to dmapprint 17 | * Added mappings for Digital Photo Access Protocol content codes 18 | * Added mappings for chapter-data and media-kind-listing content codes 19 | * Fixed NULL input parameter handling 20 | 21 | 1.0.0 (2013-03-25) 22 | 23 | * Initial release 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Matt Stevens 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS += -Wall -Wextra -Werror -Os 2 | APPCFLAGS = $(CFLAGS) -Wno-unused-parameter 3 | 4 | all: dmapprint test 5 | 6 | test: testapp 7 | @ ./testapp 8 | 9 | checkcodes: checkapp 10 | @echo "iTunes: " 11 | curl --silent --header "Viewer-Only-Client: 1" http://127.0.0.1:3689/content-codes | ./checkapp | LC_ALL=C sort 12 | @echo "\niPhoto (note: remaps containers): " 13 | curl --silent --header "Viewer-Only-Client: 1" http://127.0.0.1:8770/content-codes | ./checkapp | LC_ALL=C sort 14 | 15 | dmap_parser.o: dmap_parser.c dmap_parser.h Makefile 16 | $(CC) $(CFLAGS) -c dmap_parser.c -o $@ 17 | 18 | testapp: dmap_parser.o test.c 19 | $(CC) $(APPCFLAGS) -o testapp dmap_parser.o test.c 20 | 21 | checkapp: dmap_parser.o checkcodes.c 22 | $(CC) $(APPCFLAGS) -o checkapp dmap_parser.o checkcodes.c 23 | 24 | dmapprint: dmap_parser.o dmapprint.c 25 | $(CC) $(APPCFLAGS) -o dmapprint dmap_parser.o dmapprint.c 26 | 27 | clean: 28 | rm -f *.o checkapp testapp dmapprint 29 | 30 | .PHONY: checkcodes clean test 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DMAP Parser 2 | =========== 3 | 4 | A parser in C for the Digital Media Access Protocol and its derivatives as used by iTunes and iPhoto. This includes support for the following protocols: 5 | 6 | * Digital Media Access Protocol (DMAP) 7 | * Digital Audio Access Protocol (DAAP) 8 | * Digital Photo Access Protocol (DPAP) 9 | * Digital Media Control Protocol (DMCP) 10 | * Digital Audio Control Protocol (DACP) 11 | 12 | Usage 13 | ----- 14 | 15 | Populate a `dmap_settings` structure with the callbacks you're interested in and call `dmap_parse`. The parser maintains no state and expects to receive a complete message. Each callback returns both the four character code included in the message and a more human readable name for fields the parser is aware of. 16 | 17 | Forward Compatibility 18 | --------------------- 19 | 20 | The DMAP protocol does not encode any type information, requiring clients to know what type is used for each field ID and to skip unknown fields. However, through examination of the field data the parser is able to make a reasonable guess at the type. This is useful when examining new fields or messages. 21 | 22 | dmapprint 23 | --------- 24 | 25 | The dmapprint utility accepts DMAP input from stdin or a file and outputs a human readable representation of the message. For example: 26 | 27 | > curl --silent --header "Viewer-Only-Client: 1" http://127.0.0.1:3689/content-codes | dmapprint 28 | dmap.contentcodesresponse: 29 | dmap.status: 200 30 | dmap.dictionary: 31 | dmap.contentcodesnumber: miid 32 | dmap.contentcodesname: dmap.itemid 33 | dmap.contentcodestype: 5 34 | dmap.dictionary: 35 | dmap.contentcodesnumber: minm 36 | dmap.contentcodesname: dmap.itemname 37 | dmap.contentcodestype: 9 38 | ... 39 | 40 | This utility can be built by running `make dmapprint`. 41 | 42 | Content Code Sources 43 | -------------------- 44 | 45 | 1. The /content-codes endpoints exposed by iTunes and iPhoto (see the dmapprint example above). The makefile includes a `checkcodes` target that parses the output of these endpoints and checks for new or updated codes. 46 | 47 | 2. Examination of network traffic between iTunes libraries and between iTunes and the Remote app. The /databases and /ctrl-int/1/getproperty endpoints are used to request specific properties by name and the results are returned using their fourchar equivalents. These mappings can be confirmed by requesting specific properties in isolation. 48 | 49 | 3. Output of `strings` on the iTunes binary. Since content code names use consistent prefixing this provides a good idea of the set of names that iTunes contains mappings for. These names can be mapped by querying them through the databases or getproperty endpoints. In a few cases these names are manually mapped where the name and fourchar follow a pattern in confirmed codes and the context in which they are used provides a high level of confidence in the mapping. 50 | -------------------------------------------------------------------------------- /checkcodes.c: -------------------------------------------------------------------------------- 1 | #include "dmap_parser.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* Parses a dmap.contentcodesresponse checking for new content codes */ 12 | 13 | static int in_response = 0; 14 | static int in_type = 0; 15 | 16 | struct contentcode { 17 | char code[5]; 18 | char name[256]; 19 | uint32_t type; 20 | }; 21 | 22 | static struct contentcode current_type = {}; 23 | 24 | enum CODE_TYPE { 25 | UINT8 = 1, 26 | INT8 = 2, 27 | UINT16 = 3, 28 | INT16 = 4, 29 | UINT32 = 5, 30 | INT32 = 6, 31 | UINT64 = 7, 32 | INT64 = 8, 33 | STRING = 9, 34 | DATE = 10, 35 | VERSION = 11, 36 | LIST = 12 37 | }; 38 | 39 | static const char *dmap_type_for_type(uint32_t type) { 40 | switch ((enum CODE_TYPE)type) { 41 | case UINT8: 42 | case UINT16: 43 | case UINT32: 44 | case UINT64: 45 | return "DMAP_UINT,"; 46 | 47 | case INT8: 48 | case INT16: 49 | case INT32: 50 | case INT64: 51 | return "DMAP_INT, "; 52 | 53 | case STRING: 54 | return "DMAP_STR, "; 55 | 56 | case DATE: 57 | return "DMAP_DATE,"; 58 | 59 | case VERSION: 60 | return "DMAP_VERS,"; 61 | 62 | case LIST: 63 | return "DMAP_DICT,"; 64 | } 65 | 66 | return "UNKNOWN,"; 67 | } 68 | 69 | static void on_dict_start(void *ctx, const char *code, const char *name) { 70 | if (in_response) { 71 | in_type = 1; 72 | } else { 73 | if (strcmp(code, "mccr") == 0) { 74 | in_response = 1; 75 | } 76 | } 77 | } 78 | 79 | static void on_dict_end(void *ctx, const char *code, const char *name) { 80 | if (in_type) { 81 | in_type = 0; 82 | if (current_type.code[0] && current_type.name[0]) { 83 | const char *state = NULL; 84 | const char *current_name = dmap_name_from_code(current_type.code); 85 | if (!current_name) { 86 | state = "NEW "; 87 | } else if (strcmp(current_type.name, current_name) != 0) { 88 | state = "UPDATED"; 89 | } 90 | 91 | if (state) { 92 | printf("%s { \"%s\", %s 0, \"%s\" },\n", state, current_type.code, dmap_type_for_type(current_type.type), current_type.name); 93 | } 94 | } 95 | memset(¤t_type, 0, sizeof(current_type)); 96 | } else if (in_response) { 97 | in_response = 0; 98 | } 99 | } 100 | 101 | static void on_uint32(void *ctx, const char *code, const char *name, uint32_t value) { 102 | if (strcmp(code, "mcnm") == 0) { 103 | current_type.code[0] = (value >> 24) & 0xff; 104 | current_type.code[1] = (value >> 16) & 0xff; 105 | current_type.code[2] = (value >> 8) & 0xff; 106 | current_type.code[3] = value & 0xff; 107 | return; 108 | } else if (strcmp(code, "mcty") == 0) { 109 | current_type.type = value; 110 | } 111 | } 112 | 113 | static void on_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { 114 | if (strcmp(code, "mcna") == 0) { 115 | strncpy(current_type.name, buf, len); 116 | } 117 | } 118 | 119 | int main(int argc, char *argv[]) { 120 | dmap_settings settings = { 121 | .on_dict_start = on_dict_start, 122 | .on_dict_end = on_dict_end, 123 | .on_uint32 = on_uint32, 124 | .on_string = on_string, 125 | .ctx = 0 126 | }; 127 | 128 | char *buf = NULL; 129 | size_t size = 0; 130 | ssize_t result = 0; 131 | 132 | size_t bufIncrement = 60 * 1024; 133 | 134 | do { 135 | size += result; 136 | buf = realloc(buf, size + bufIncrement); 137 | result = read(fileno(stdin), &buf[size], bufIncrement); 138 | } while (result > 0); 139 | 140 | dmap_parse(&settings, buf, size); 141 | 142 | free(buf); 143 | 144 | return 0; 145 | } 146 | -------------------------------------------------------------------------------- /dmap_parser.c: -------------------------------------------------------------------------------- 1 | #include "dmap_parser.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define DMAP_STRINGIFY_(x) #x 8 | #define DMAP_STRINGIFY(x) DMAP_STRINGIFY_(x) 9 | 10 | typedef enum { 11 | DMAP_UNKNOWN, 12 | DMAP_UINT, 13 | DMAP_INT, 14 | DMAP_STR, 15 | DMAP_DATA, 16 | DMAP_DATE, 17 | DMAP_VERS, 18 | DMAP_DICT, 19 | DMAP_ITEM 20 | } DMAP_TYPE; 21 | 22 | typedef struct { 23 | /** 24 | * The four-character code used in the encoded message. 25 | */ 26 | const char *code; 27 | 28 | /** 29 | * The type of data associated with the content code. 30 | */ 31 | DMAP_TYPE type; 32 | 33 | /** 34 | * For listings, the type of their listing item children. 35 | * 36 | * Listing items (mlit) can be of any type, and as with other content codes 37 | * their type information is not encoded in the message. Parsers must 38 | * determine the type of the listing items based on their parent context. 39 | */ 40 | DMAP_TYPE list_item_type; 41 | 42 | /** 43 | * A human-readable name for the content code. 44 | */ 45 | const char *name; 46 | } dmap_field; 47 | 48 | static const dmap_field dmap_fields[] = { 49 | { "abal", DMAP_DICT, DMAP_STR, "daap.browsealbumlisting" }, 50 | { "abar", DMAP_DICT, DMAP_STR, "daap.browseartistlisting" }, 51 | { "abcp", DMAP_DICT, DMAP_STR, "daap.browsecomposerlisting" }, 52 | { "abgn", DMAP_DICT, DMAP_STR, "daap.browsegenrelisting" }, 53 | { "abpl", DMAP_UINT, 0, "daap.baseplaylist" }, 54 | { "abro", DMAP_DICT, 0, "daap.databasebrowse" }, 55 | { "adbs", DMAP_DICT, 0, "daap.databasesongs" }, 56 | { "aeAD", DMAP_DICT, 0, "com.apple.itunes.adam-ids-array" }, 57 | { "aeAI", DMAP_UINT, 0, "com.apple.itunes.itms-artistid" }, 58 | { "aeCD", DMAP_DATA, 0, "com.apple.itunes.flat-chapter-data" }, 59 | { "aeCF", DMAP_UINT, 0, "com.apple.itunes.cloud-flavor-id" }, 60 | { "aeCI", DMAP_UINT, 0, "com.apple.itunes.itms-composerid" }, 61 | { "aeCK", DMAP_UINT, 0, "com.apple.itunes.cloud-library-kind" }, 62 | { "aeCM", DMAP_UINT, 0, "com.apple.itunes.cloud-match-type" }, 63 | { "aeCR", DMAP_STR, 0, "com.apple.itunes.content-rating" } , 64 | { "aeCS", DMAP_UINT, 0, "com.apple.itunes.artworkchecksum" }, 65 | { "aeCU", DMAP_UINT, 0, "com.apple.itunes.cloud-user-id" }, 66 | { "aeCd", DMAP_UINT, 0, "com.apple.itunes.cloud-id" }, 67 | { "aeDE", DMAP_STR, 0, "com.apple.itunes.longest-content-description" }, 68 | { "aeDL", DMAP_UINT, 0, "com.apple.itunes.drm-downloader-user-id" }, 69 | { "aeDP", DMAP_UINT, 0, "com.apple.itunes.drm-platform-id" }, 70 | { "aeDR", DMAP_UINT, 0, "com.apple.itunes.drm-user-id" }, 71 | { "aeDV", DMAP_UINT, 0, "com.apple.itunes.drm-versions" }, 72 | { "aeEN", DMAP_STR, 0, "com.apple.itunes.episode-num-str" }, 73 | { "aeES", DMAP_UINT, 0, "com.apple.itunes.episode-sort" }, 74 | { "aeFA", DMAP_UINT, 0, "com.apple.itunes.drm-family-id" }, 75 | { "aeGD", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-dr" } , 76 | { "aeGE", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-del" }, 77 | { "aeGH", DMAP_UINT, 0, "com.apple.itunes.gapless-heur" }, 78 | { "aeGI", DMAP_UINT, 0, "com.apple.itunes.itms-genreid" }, 79 | { "aeGR", DMAP_UINT, 0, "com.apple.itunes.gapless-resy" }, 80 | { "aeGU", DMAP_UINT, 0, "com.apple.itunes.gapless-dur" }, 81 | { "aeGs", DMAP_UINT, 0, "com.apple.itunes.can-be-genius-seed" }, 82 | { "aeHC", DMAP_UINT, 0, "com.apple.itunes.has-chapter-data" }, 83 | { "aeHD", DMAP_UINT, 0, "com.apple.itunes.is-hd-video" }, 84 | { "aeHV", DMAP_UINT, 0, "com.apple.itunes.has-video" }, 85 | { "aeK1", DMAP_UINT, 0, "com.apple.itunes.drm-key1-id" }, 86 | { "aeK2", DMAP_UINT, 0, "com.apple.itunes.drm-key2-id" }, 87 | { "aeMC", DMAP_UINT, 0, "com.apple.itunes.playlist-contains-media-type-count" }, 88 | { "aeMK", DMAP_UINT, 0, "com.apple.itunes.mediakind" }, 89 | { "aeMX", DMAP_STR, 0, "com.apple.itunes.movie-info-xml" }, 90 | { "aeMk", DMAP_UINT, 0, "com.apple.itunes.extended-media-kind" }, 91 | { "aeND", DMAP_UINT, 0, "com.apple.itunes.non-drm-user-id" }, 92 | { "aeNN", DMAP_STR, 0, "com.apple.itunes.network-name" }, 93 | { "aeNV", DMAP_UINT, 0, "com.apple.itunes.norm-volume" }, 94 | { "aePC", DMAP_UINT, 0, "com.apple.itunes.is-podcast" }, 95 | { "aePI", DMAP_UINT, 0, "com.apple.itunes.itms-playlistid" }, 96 | { "aePP", DMAP_UINT, 0, "com.apple.itunes.is-podcast-playlist" }, 97 | { "aePS", DMAP_UINT, 0, "com.apple.itunes.special-playlist" }, 98 | { "aeRD", DMAP_UINT, 0, "com.apple.itunes.rental-duration" }, 99 | { "aeRP", DMAP_UINT, 0, "com.apple.itunes.rental-pb-start" }, 100 | { "aeRS", DMAP_UINT, 0, "com.apple.itunes.rental-start" }, 101 | { "aeRU", DMAP_UINT, 0, "com.apple.itunes.rental-pb-duration" }, 102 | { "aeRf", DMAP_UINT, 0, "com.apple.itunes.is-featured" }, 103 | { "aeSE", DMAP_UINT, 0, "com.apple.itunes.store-pers-id" }, 104 | { "aeSF", DMAP_UINT, 0, "com.apple.itunes.itms-storefrontid" }, 105 | { "aeSG", DMAP_UINT, 0, "com.apple.itunes.saved-genius" }, 106 | { "aeSI", DMAP_UINT, 0, "com.apple.itunes.itms-songid" }, 107 | { "aeSN", DMAP_STR, 0, "com.apple.itunes.series-name" }, 108 | { "aeSP", DMAP_UINT, 0, "com.apple.itunes.smart-playlist" }, 109 | { "aeSU", DMAP_UINT, 0, "com.apple.itunes.season-num" }, 110 | { "aeSV", DMAP_VERS, 0, "com.apple.itunes.music-sharing-version" }, 111 | { "aeXD", DMAP_STR, 0, "com.apple.itunes.xid" }, 112 | { "aecp", DMAP_STR, 0, "com.apple.itunes.collection-description" }, 113 | { "aels", DMAP_UINT, 0, "com.apple.itunes.liked-state" }, 114 | { "aemi", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing-item" }, 115 | { "aeml", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing" }, 116 | { "agac", DMAP_UINT, 0, "daap.groupalbumcount" }, 117 | { "agma", DMAP_UINT, 0, "daap.groupmatchedqueryalbumcount" }, 118 | { "agmi", DMAP_UINT, 0, "daap.groupmatchedqueryitemcount" }, 119 | { "agrp", DMAP_STR, 0, "daap.songgrouping" }, 120 | { "ajAE", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-type" }, 121 | { "ajAS", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-sort-order" }, 122 | { "ajAT", DMAP_UINT, 0, "com.apple.itunes.store.ams-show-type" }, 123 | { "ajAV", DMAP_UINT, 0, "com.apple.itunes.store.is-ams-video" }, 124 | { "ajal", DMAP_UINT, 0, "com.apple.itunes.store.album-liked-state" }, 125 | { "ajcA", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" }, 126 | { "ajca", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" }, 127 | { "ajuw", DMAP_UINT, 0, "com.apple.itunes.store.use-work-name-as-display-name" }, 128 | { "amvc", DMAP_UINT, 0, "daap.songmovementcount" }, 129 | { "amvm", DMAP_STR, 0, "daap.songmovementname" }, 130 | { "amvn", DMAP_UINT, 0, "daap.songmovementnumber" }, 131 | { "aply", DMAP_DICT, 0, "daap.databaseplaylists" }, 132 | { "aprm", DMAP_UINT, 0, "daap.playlistrepeatmode" }, 133 | { "apro", DMAP_VERS, 0, "daap.protocolversion" }, 134 | { "apsm", DMAP_UINT, 0, "daap.playlistshufflemode" }, 135 | { "apso", DMAP_DICT, 0, "daap.playlistsongs" }, 136 | { "arif", DMAP_DICT, 0, "daap.resolveinfo" }, 137 | { "arsv", DMAP_DICT, 0, "daap.resolve" }, 138 | { "asaa", DMAP_STR, 0, "daap.songalbumartist" }, 139 | { "asac", DMAP_UINT, 0, "daap.songartworkcount" }, 140 | { "asai", DMAP_UINT, 0, "daap.songalbumid" }, 141 | { "asal", DMAP_STR, 0, "daap.songalbum" }, 142 | { "asar", DMAP_STR, 0, "daap.songartist" }, 143 | { "asas", DMAP_UINT, 0, "daap.songalbumuserratingstatus" }, 144 | { "asbk", DMAP_UINT, 0, "daap.bookmarkable" }, 145 | { "asbo", DMAP_UINT, 0, "daap.songbookmark" }, 146 | { "asbr", DMAP_UINT, 0, "daap.songbitrate" }, 147 | { "asbt", DMAP_UINT, 0, "daap.songbeatsperminute" }, 148 | { "ascd", DMAP_UINT, 0, "daap.songcodectype" }, 149 | { "ascm", DMAP_STR, 0, "daap.songcomment" }, 150 | { "ascn", DMAP_STR, 0, "daap.songcontentdescription" }, 151 | { "asco", DMAP_UINT, 0, "daap.songcompilation" }, 152 | { "ascp", DMAP_STR, 0, "daap.songcomposer" }, 153 | { "ascr", DMAP_UINT, 0, "daap.songcontentrating" }, 154 | { "ascs", DMAP_UINT, 0, "daap.songcodecsubtype" }, 155 | { "asct", DMAP_STR, 0, "daap.songcategory" }, 156 | { "asda", DMAP_DATE, 0, "daap.songdateadded" }, 157 | { "asdb", DMAP_UINT, 0, "daap.songdisabled" }, 158 | { "asdc", DMAP_UINT, 0, "daap.songdisccount" }, 159 | { "asdk", DMAP_UINT, 0, "daap.songdatakind" }, 160 | { "asdm", DMAP_DATE, 0, "daap.songdatemodified" }, 161 | { "asdn", DMAP_UINT, 0, "daap.songdiscnumber" }, 162 | { "asdp", DMAP_DATE, 0, "daap.songdatepurchased" }, 163 | { "asdr", DMAP_DATE, 0, "daap.songdatereleased" }, 164 | { "asdt", DMAP_STR, 0, "daap.songdescription" }, 165 | { "ased", DMAP_UINT, 0, "daap.songextradata" }, 166 | { "aseq", DMAP_STR, 0, "daap.songeqpreset" }, 167 | { "ases", DMAP_UINT, 0, "daap.songexcludefromshuffle" }, 168 | { "asfm", DMAP_STR, 0, "daap.songformat" }, 169 | { "asgn", DMAP_STR, 0, "daap.songgenre" }, 170 | { "asgp", DMAP_UINT, 0, "daap.songgapless" }, 171 | { "asgr", DMAP_UINT, 0, "daap.supportsgroups" }, 172 | { "ashp", DMAP_UINT, 0, "daap.songhasbeenplayed" }, 173 | { "askd", DMAP_DATE, 0, "daap.songlastskipdate" }, 174 | { "askp", DMAP_UINT, 0, "daap.songuserskipcount" }, 175 | { "asky", DMAP_STR, 0, "daap.songkeywords" }, 176 | { "aslc", DMAP_STR, 0, "daap.songlongcontentdescription" }, 177 | { "aslr", DMAP_UINT, 0, "daap.songalbumuserrating" }, 178 | { "asls", DMAP_UINT, 0, "daap.songlongsize" }, 179 | { "aspc", DMAP_UINT, 0, "daap.songuserplaycount" }, 180 | { "aspl", DMAP_DATE, 0, "daap.songdateplayed" }, 181 | { "aspu", DMAP_STR, 0, "daap.songpodcasturl" }, 182 | { "asri", DMAP_UINT, 0, "daap.songartistid" }, 183 | { "asrs", DMAP_UINT, 0, "daap.songuserratingstatus" }, 184 | { "asrv", DMAP_INT, 0, "daap.songrelativevolume" }, 185 | { "assa", DMAP_STR, 0, "daap.sortartist" }, 186 | { "assc", DMAP_STR, 0, "daap.sortcomposer" }, 187 | { "assl", DMAP_STR, 0, "daap.sortalbumartist" }, 188 | { "assn", DMAP_STR, 0, "daap.sortname" }, 189 | { "assp", DMAP_UINT, 0, "daap.songstoptime" }, 190 | { "assr", DMAP_UINT, 0, "daap.songsamplerate" }, 191 | { "asss", DMAP_STR, 0, "daap.sortseriesname" }, 192 | { "asst", DMAP_UINT, 0, "daap.songstarttime" }, 193 | { "assu", DMAP_STR, 0, "daap.sortalbum" }, 194 | { "assz", DMAP_UINT, 0, "daap.songsize" }, 195 | { "astc", DMAP_UINT, 0, "daap.songtrackcount" }, 196 | { "astm", DMAP_UINT, 0, "daap.songtime" }, 197 | { "astn", DMAP_UINT, 0, "daap.songtracknumber" }, 198 | { "asul", DMAP_STR, 0, "daap.songdataurl" }, 199 | { "asur", DMAP_UINT, 0, "daap.songuserrating" }, 200 | { "asvc", DMAP_UINT, 0, "daap.songprimaryvideocodec" }, 201 | { "asyr", DMAP_UINT, 0, "daap.songyear" }, 202 | { "ated", DMAP_UINT, 0, "daap.supportsextradata" }, 203 | { "avdb", DMAP_DICT, 0, "daap.serverdatabases" }, 204 | { "awrk", DMAP_STR, 0, "daap.songwork" }, 205 | { "caar", DMAP_UINT, 0, "dacp.availablerepeatstates" }, 206 | { "caas", DMAP_UINT, 0, "dacp.availableshufflestates" }, 207 | { "caci", DMAP_DICT, 0, "caci" }, 208 | { "cafe", DMAP_UINT, 0, "dacp.fullscreenenabled" }, 209 | { "cafs", DMAP_UINT, 0, "dacp.fullscreen" }, 210 | { "caia", DMAP_UINT, 0, "dacp.isactive" }, 211 | { "cana", DMAP_STR, 0, "dacp.nowplayingartist" }, 212 | { "cang", DMAP_STR, 0, "dacp.nowplayinggenre" }, 213 | { "canl", DMAP_STR, 0, "dacp.nowplayingalbum" }, 214 | { "cann", DMAP_STR, 0, "dacp.nowplayingname" }, 215 | { "canp", DMAP_UINT, 0, "dacp.nowplayingids" }, 216 | { "cant", DMAP_UINT, 0, "dacp.nowplayingtime" }, 217 | { "capr", DMAP_VERS, 0, "dacp.protocolversion" }, 218 | { "caps", DMAP_UINT, 0, "dacp.playerstate" }, 219 | { "carp", DMAP_UINT, 0, "dacp.repeatstate" }, 220 | { "cash", DMAP_UINT, 0, "dacp.shufflestate" }, 221 | { "casp", DMAP_DICT, 0, "dacp.speakers" }, 222 | { "cast", DMAP_UINT, 0, "dacp.songtime" }, 223 | { "cavc", DMAP_UINT, 0, "dacp.volumecontrollable" }, 224 | { "cave", DMAP_UINT, 0, "dacp.visualizerenabled" }, 225 | { "cavs", DMAP_UINT, 0, "dacp.visualizer" }, 226 | { "ceJC", DMAP_UINT, 0, "com.apple.itunes.jukebox-client-vote" }, 227 | { "ceJI", DMAP_UINT, 0, "com.apple.itunes.jukebox-current" }, 228 | { "ceJS", DMAP_UINT, 0, "com.apple.itunes.jukebox-score" }, 229 | { "ceJV", DMAP_UINT, 0, "com.apple.itunes.jukebox-vote" }, 230 | { "ceQR", DMAP_DICT, 0, "com.apple.itunes.playqueue-contents-response" }, 231 | { "ceQa", DMAP_STR, 0, "com.apple.itunes.playqueue-album" }, 232 | { "ceQg", DMAP_STR, 0, "com.apple.itunes.playqueue-genre" }, 233 | { "ceQn", DMAP_STR, 0, "com.apple.itunes.playqueue-name" }, 234 | { "ceQr", DMAP_STR, 0, "com.apple.itunes.playqueue-artist" }, 235 | { "cmgt", DMAP_DICT, 0, "dmcp.getpropertyresponse" }, 236 | { "cmmk", DMAP_UINT, 0, "dmcp.mediakind" }, 237 | { "cmpr", DMAP_VERS, 0, "dmcp.protocolversion" }, 238 | { "cmsr", DMAP_UINT, 0, "dmcp.serverrevision" }, 239 | { "cmst", DMAP_DICT, 0, "dmcp.playstatus" }, 240 | { "cmvo", DMAP_UINT, 0, "dmcp.volume" }, 241 | { "f\215ch", DMAP_UINT, 0, "dmap.haschildcontainers" }, 242 | { "ipsa", DMAP_DICT, 0, "dpap.iphotoslideshowadvancedoptions" }, 243 | { "ipsl", DMAP_DICT, 0, "dpap.iphotoslideshowoptions" }, 244 | { "mbcl", DMAP_DICT, 0, "dmap.bag" }, 245 | { "mccr", DMAP_DICT, 0, "dmap.contentcodesresponse" }, 246 | { "mcna", DMAP_STR, 0, "dmap.contentcodesname" }, 247 | { "mcnm", DMAP_UINT, 0, "dmap.contentcodesnumber" }, 248 | { "mcon", DMAP_DICT, 0, "dmap.container" }, 249 | { "mctc", DMAP_UINT, 0, "dmap.containercount" }, 250 | { "mcti", DMAP_UINT, 0, "dmap.containeritemid" }, 251 | { "mcty", DMAP_UINT, 0, "dmap.contentcodestype" }, 252 | { "mdbk", DMAP_UINT, 0, "dmap.databasekind" }, 253 | { "mdcl", DMAP_DICT, 0, "dmap.dictionary" }, 254 | { "mdst", DMAP_UINT, 0, "dmap.downloadstatus" }, 255 | { "meds", DMAP_UINT, 0, "dmap.editcommandssupported" }, 256 | { "meia", DMAP_UINT, 0, "dmap.itemdateadded" }, 257 | { "meip", DMAP_UINT, 0, "dmap.itemdateplayed" }, 258 | { "mext", DMAP_UINT, 0, "dmap.objectextradata" }, 259 | { "miid", DMAP_UINT, 0, "dmap.itemid" }, 260 | { "mikd", DMAP_UINT, 0, "dmap.itemkind" }, 261 | { "mimc", DMAP_UINT, 0, "dmap.itemcount" }, 262 | { "minm", DMAP_STR, 0, "dmap.itemname" }, 263 | { "mlcl", DMAP_DICT, DMAP_DICT, "dmap.listing" }, 264 | { "mlid", DMAP_UINT, 0, "dmap.sessionid" }, 265 | { "mlit", DMAP_ITEM, 0, "dmap.listingitem" }, 266 | { "mlog", DMAP_DICT, 0, "dmap.loginresponse" }, 267 | { "mpco", DMAP_UINT, 0, "dmap.parentcontainerid" }, 268 | { "mper", DMAP_UINT, 0, "dmap.persistentid" }, 269 | { "mpro", DMAP_VERS, 0, "dmap.protocolversion" }, 270 | { "mrco", DMAP_UINT, 0, "dmap.returnedcount" }, 271 | { "mrpr", DMAP_UINT, 0, "dmap.remotepersistentid" }, 272 | { "msal", DMAP_UINT, 0, "dmap.supportsautologout" }, 273 | { "msas", DMAP_UINT, 0, "dmap.authenticationschemes" }, 274 | { "msau", DMAP_UINT, 0, "dmap.authenticationmethod" }, 275 | { "msbr", DMAP_UINT, 0, "dmap.supportsbrowse" }, 276 | { "msdc", DMAP_UINT, 0, "dmap.databasescount" }, 277 | { "msex", DMAP_UINT, 0, "dmap.supportsextensions" }, 278 | { "msix", DMAP_UINT, 0, "dmap.supportsindex" }, 279 | { "mslr", DMAP_UINT, 0, "dmap.loginrequired" }, 280 | { "msma", DMAP_UINT, 0, "dmap.machineaddress" }, 281 | { "msml", DMAP_DICT, 0, "msml" }, 282 | { "mspi", DMAP_UINT, 0, "dmap.supportspersistentids" }, 283 | { "msqy", DMAP_UINT, 0, "dmap.supportsquery" }, 284 | { "msrs", DMAP_UINT, 0, "dmap.supportsresolve" }, 285 | { "msrv", DMAP_DICT, 0, "dmap.serverinforesponse" }, 286 | { "mstc", DMAP_DATE, 0, "dmap.utctime" }, 287 | { "mstm", DMAP_UINT, 0, "dmap.timeoutinterval" }, 288 | { "msto", DMAP_INT, 0, "dmap.utcoffset" }, 289 | { "msts", DMAP_STR, 0, "dmap.statusstring" }, 290 | { "mstt", DMAP_UINT, 0, "dmap.status" }, 291 | { "msup", DMAP_UINT, 0, "dmap.supportsupdate" }, 292 | { "mtco", DMAP_UINT, 0, "dmap.specifiedtotalcount" }, 293 | { "mudl", DMAP_DICT, 0, "dmap.deletedidlisting" }, 294 | { "mupd", DMAP_DICT, 0, "dmap.updateresponse" }, 295 | { "musr", DMAP_UINT, 0, "dmap.serverrevision" }, 296 | { "muty", DMAP_UINT, 0, "dmap.updatetype" }, 297 | { "pasp", DMAP_STR, 0, "dpap.aspectratio" }, 298 | { "pcmt", DMAP_STR, 0, "dpap.imagecomments" }, 299 | { "peak", DMAP_UINT, 0, "com.apple.itunes.photos.album-kind" }, 300 | { "peed", DMAP_DATE, 0, "com.apple.itunes.photos.exposure-date" }, 301 | { "pefc", DMAP_DICT, 0, "com.apple.itunes.photos.faces" }, 302 | { "peki", DMAP_UINT, 0, "com.apple.itunes.photos.key-image-id" }, 303 | { "pekm", DMAP_DICT, 0, "com.apple.itunes.photos.key-image" }, 304 | { "pemd", DMAP_DATE, 0, "com.apple.itunes.photos.modification-date" }, 305 | { "pfai", DMAP_DICT, 0, "dpap.failureids" }, 306 | { "pfdt", DMAP_DICT, 0, "dpap.filedata" }, 307 | { "pfmt", DMAP_STR, 0, "dpap.imageformat" }, 308 | { "phgt", DMAP_UINT, 0, "dpap.imagepixelheight" }, 309 | { "picd", DMAP_DATE, 0, "dpap.creationdate" }, 310 | { "pifs", DMAP_UINT, 0, "dpap.imagefilesize" }, 311 | { "pimf", DMAP_STR, 0, "dpap.imagefilename" }, 312 | { "plsz", DMAP_UINT, 0, "dpap.imagelargefilesize" }, 313 | { "ppro", DMAP_VERS, 0, "dpap.protocolversion" }, 314 | { "prat", DMAP_UINT, 0, "dpap.imagerating" }, 315 | { "pret", DMAP_DICT, 0, "dpap.retryids" }, 316 | { "pwth", DMAP_UINT, 0, "dpap.imagepixelwidth" } 317 | }; 318 | static const size_t dmap_field_count = sizeof(dmap_fields) / sizeof(dmap_field); 319 | 320 | typedef int (*sort_func) (const void *, const void *); 321 | 322 | int dmap_version(void) { 323 | return DMAP_VERSION; 324 | } 325 | 326 | const char *dmap_version_string(void) { 327 | return DMAP_STRINGIFY(DMAP_VERSION_MAJOR) "." 328 | DMAP_STRINGIFY(DMAP_VERSION_MINOR) "." 329 | DMAP_STRINGIFY(DMAP_VERSION_PATCH); 330 | } 331 | 332 | static int dmap_field_sort(const dmap_field *a, const dmap_field *b) { 333 | return memcmp(a->code, b->code, 4); 334 | } 335 | 336 | static const dmap_field *dmap_field_from_code(const char *code) { 337 | dmap_field key; 338 | key.code = code; 339 | return bsearch(&key, dmap_fields, dmap_field_count, sizeof(dmap_field), (sort_func)dmap_field_sort); 340 | } 341 | 342 | const char *dmap_name_from_code(const char *code) { 343 | const dmap_field *field; 344 | if (!code) 345 | return NULL; 346 | 347 | field = dmap_field_from_code(code); 348 | return field ? field->name : NULL; 349 | } 350 | 351 | static uint16_t dmap_read_u16(const char *buf) { 352 | return (uint16_t)(((buf[0] & 0xff) << 8) | (buf[1] & 0xff)); 353 | } 354 | 355 | static int16_t dmap_read_i16(const char *buf) { 356 | return (int16_t)dmap_read_u16(buf); 357 | } 358 | 359 | static uint32_t dmap_read_u32(const char *buf) { 360 | return ((uint32_t)(buf[0] & 0xff) << 24) | 361 | ((uint32_t)(buf[1] & 0xff) << 16) | 362 | ((uint32_t)(buf[2] & 0xff) << 8) | 363 | ((uint32_t)(buf[3] & 0xff)); 364 | } 365 | 366 | static int32_t dmap_read_i32(const char *buf) { 367 | return (int32_t)dmap_read_u32(buf); 368 | } 369 | 370 | static uint64_t dmap_read_u64(const char *buf) { 371 | return ((uint64_t)(buf[0] & 0xff) << 56) | 372 | ((uint64_t)(buf[1] & 0xff) << 48) | 373 | ((uint64_t)(buf[2] & 0xff) << 40) | 374 | ((uint64_t)(buf[3] & 0xff) << 32) | 375 | ((uint64_t)(buf[4] & 0xff) << 24) | 376 | ((uint64_t)(buf[5] & 0xff) << 16) | 377 | ((uint64_t)(buf[6] & 0xff) << 8) | 378 | ((uint64_t)(buf[7] & 0xff)); 379 | } 380 | 381 | static int64_t dmap_read_i64(const char *buf) { 382 | return (int64_t)dmap_read_u64(buf); 383 | } 384 | 385 | static int dmap_parse_internal(const dmap_settings *settings, const char *buf, size_t len, const dmap_field *parent) { 386 | const dmap_field *field; 387 | DMAP_TYPE field_type; 388 | size_t field_len; 389 | const char *field_name; 390 | const char *p = buf; 391 | const char *end = buf + len; 392 | char code[5] = {0}; 393 | 394 | if (!settings || !buf) 395 | return -1; 396 | 397 | while (end - p >= 8) { 398 | memcpy(code, p, 4); 399 | field = dmap_field_from_code(code); 400 | p += 4; 401 | 402 | field_len = dmap_read_u32(p); 403 | p += 4; 404 | 405 | if (p + field_len > end) 406 | return -1; 407 | 408 | if (field) { 409 | field_type = field->type; 410 | field_name = field->name; 411 | 412 | if (field_type == DMAP_ITEM) { 413 | if (parent != NULL && parent->list_item_type) { 414 | field_type = parent->list_item_type; 415 | } else { 416 | field_type = DMAP_DICT; 417 | } 418 | } 419 | } else { 420 | /* Make a best guess of the type */ 421 | field_type = DMAP_UNKNOWN; 422 | field_name = code; 423 | 424 | if (field_len >= 8) { 425 | /* Look for a four char code followed by a length within the current field */ 426 | if (isalpha(p[0] & 0xff) && 427 | isalpha(p[1] & 0xff) && 428 | isalpha(p[2] & 0xff) && 429 | isalpha(p[3] & 0xff)) { 430 | if (dmap_read_u32(p + 4) < field_len) 431 | field_type = DMAP_DICT; 432 | } 433 | } 434 | 435 | if (field_type == DMAP_UNKNOWN) { 436 | size_t i; 437 | int is_string = 1; 438 | for (i=0; i < field_len; i++) { 439 | if (!isprint(p[i] & 0xff)) { 440 | is_string = 0; 441 | break; 442 | } 443 | } 444 | 445 | field_type = is_string ? DMAP_STR : DMAP_UINT; 446 | } 447 | } 448 | 449 | switch (field_type) { 450 | case DMAP_UINT: 451 | /* Determine the integer's type based on its size */ 452 | switch (field_len) { 453 | case 1: 454 | if (settings->on_uint32) 455 | settings->on_uint32(settings->ctx, code, field_name, (unsigned char)*p); 456 | break; 457 | case 2: 458 | if (settings->on_uint32) 459 | settings->on_uint32(settings->ctx, code, field_name, dmap_read_u16(p)); 460 | break; 461 | case 4: 462 | if (settings->on_uint32) 463 | settings->on_uint32(settings->ctx, code, field_name, dmap_read_u32(p)); 464 | break; 465 | case 8: 466 | if (settings->on_uint64) 467 | settings->on_uint64(settings->ctx, code, field_name, dmap_read_u64(p)); 468 | break; 469 | default: 470 | if (settings->on_data) 471 | settings->on_data(settings->ctx, code, field_name, p, field_len); 472 | break; 473 | } 474 | break; 475 | case DMAP_INT: 476 | switch (field_len) { 477 | case 1: 478 | if (settings->on_int32) 479 | settings->on_int32(settings->ctx, code, field_name, *p); 480 | break; 481 | case 2: 482 | if (settings->on_int32) 483 | settings->on_int32(settings->ctx, code, field_name, dmap_read_i16(p)); 484 | break; 485 | case 4: 486 | if (settings->on_int32) 487 | settings->on_int32(settings->ctx, code, field_name, dmap_read_i32(p)); 488 | break; 489 | case 8: 490 | if (settings->on_int64) 491 | settings->on_int64(settings->ctx, code, field_name, dmap_read_i64(p)); 492 | break; 493 | default: 494 | if (settings->on_data) 495 | settings->on_data(settings->ctx, code, field_name, p, field_len); 496 | break; 497 | } 498 | break; 499 | case DMAP_STR: 500 | if (settings->on_string) 501 | settings->on_string(settings->ctx, code, field_name, p, field_len); 502 | break; 503 | case DMAP_DATA: 504 | if (settings->on_data) 505 | settings->on_data(settings->ctx, code, field_name, p, field_len); 506 | break; 507 | case DMAP_DATE: 508 | /* Seconds since epoch */ 509 | if (settings->on_date) 510 | settings->on_date(settings->ctx, code, field_name, dmap_read_u32(p)); 511 | break; 512 | case DMAP_VERS: 513 | if (settings->on_string && field_len >= 4) { 514 | char version[20]; 515 | sprintf(version, "%u.%u", dmap_read_u16(p), dmap_read_u16(p+2)); 516 | settings->on_string(settings->ctx, code, field_name, version, strlen(version)); 517 | } 518 | break; 519 | case DMAP_DICT: 520 | if (settings->on_dict_start) 521 | settings->on_dict_start(settings->ctx, code, field_name); 522 | if (dmap_parse_internal(settings, p, field_len, field) != 0) 523 | return -1; 524 | if (settings->on_dict_end) 525 | settings->on_dict_end(settings->ctx, code, field_name); 526 | break; 527 | case DMAP_ITEM: 528 | /* Unreachable: listing item types are always mapped to another type */ 529 | abort(); 530 | case DMAP_UNKNOWN: 531 | break; 532 | } 533 | 534 | p += field_len; 535 | } 536 | 537 | if (p != end) 538 | return -1; 539 | 540 | return 0; 541 | } 542 | 543 | int dmap_parse(const dmap_settings *settings, const char *buf, size_t len) { 544 | return dmap_parse_internal(settings, buf, len, NULL); 545 | } 546 | -------------------------------------------------------------------------------- /dmap_parser.h: -------------------------------------------------------------------------------- 1 | #ifndef dmap_parser_h 2 | #define dmap_parser_h 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #include 8 | #include 9 | 10 | #define DMAP_VERSION_MAJOR 1 11 | #define DMAP_VERSION_MINOR 2 12 | #define DMAP_VERSION_PATCH 1 13 | 14 | #define DMAP_VERSION (DMAP_VERSION_MAJOR * 1000000 + \ 15 | DMAP_VERSION_MINOR * 1000 + \ 16 | DMAP_VERSION_PATCH) 17 | 18 | /* 19 | * Callbacks invoked during parsing. 20 | * 21 | * @param ctx The context pointer specified in the dmap_settings structure. 22 | * @param code The content code from the message. 23 | * @param name The name associated with the content code, if known. If there is 24 | * no known name this parameter contains the same value as the code 25 | * parameter. 26 | */ 27 | typedef void (*dmap_dict_cb) (void *ctx, const char *code, const char *name); 28 | typedef void (*dmap_int32_cb) (void *ctx, const char *code, const char *name, int32_t value); 29 | typedef void (*dmap_int64_cb) (void *ctx, const char *code, const char *name, int64_t value); 30 | typedef void (*dmap_uint32_cb) (void *ctx, const char *code, const char *name, uint32_t value); 31 | typedef void (*dmap_uint64_cb) (void *ctx, const char *code, const char *name, uint64_t value); 32 | typedef void (*dmap_data_cb) (void *ctx, const char *code, const char *name, const char *buf, size_t len); 33 | 34 | typedef struct { 35 | /* Callbacks to indicate the start and end of dictionary fields. */ 36 | dmap_dict_cb on_dict_start; 37 | dmap_dict_cb on_dict_end; 38 | 39 | /* Callbacks for field data. */ 40 | dmap_int32_cb on_int32; 41 | dmap_int64_cb on_int64; 42 | dmap_uint32_cb on_uint32; 43 | dmap_uint64_cb on_uint64; 44 | dmap_uint32_cb on_date; 45 | dmap_data_cb on_string; 46 | dmap_data_cb on_data; 47 | 48 | /** A context pointer passed to each callback function. */ 49 | void *ctx; 50 | } dmap_settings; 51 | 52 | /** 53 | * Returns the library version number. 54 | * 55 | * The version number format is (major * 1000000) + (minor * 1000) + patch. 56 | * For example, the value for version 1.2.3 is 1002003. 57 | */ 58 | int dmap_version(void); 59 | 60 | /** 61 | * Returns the library version as a string. 62 | */ 63 | const char *dmap_version_string(void); 64 | 65 | /** 66 | * Returns the name associated with the provided content code, or NULL if there 67 | * is no known name. 68 | * 69 | * For example, if given the code "minm" this function returns "dmap.itemname". 70 | */ 71 | const char *dmap_name_from_code(const char *code); 72 | 73 | /** 74 | * Parses a DMAP message buffer using the provided settings. 75 | * 76 | * @param settings A dmap_settings structure populated with the callbacks to 77 | * invoke during parsing. 78 | * @param buf Pointer to a DMAP message buffer. The buffer must contain a 79 | * complete message. 80 | * @param len The length of the DMAP message buffer. 81 | * 82 | * @return 0 if parsing was successful, or -1 if an error occurred. 83 | */ 84 | int dmap_parse(const dmap_settings *settings, const char *buf, size_t len); 85 | 86 | #ifdef __cplusplus 87 | } 88 | #endif 89 | #endif 90 | -------------------------------------------------------------------------------- /dmapprint.c: -------------------------------------------------------------------------------- 1 | #include "dmap_parser.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | typedef enum { 14 | UINT8 = 1, 15 | INT8 = 2, 16 | UINT16 = 3, 17 | INT16 = 4, 18 | UINT32 = 5, 19 | INT32 = 6, 20 | UINT64 = 7, 21 | INT64 = 8, 22 | STRING = 9, 23 | DATE = 10, 24 | VERSION = 11, 25 | LIST = 12 26 | } CONTENT_CODE_TYPE; 27 | 28 | static char prefix[64] = {0}; 29 | static const char hexchars[] = "0123456789abcdef"; 30 | static int rawcodes = 0; 31 | 32 | static void indent() { 33 | strcat(prefix, " "); 34 | } 35 | 36 | static void outdent() { 37 | size_t len = strlen(prefix); 38 | if (len >= 2) { 39 | prefix[len - 2] = '\0'; 40 | } 41 | } 42 | 43 | #if defined(__GNUC__) 44 | __attribute__((format(printf, 1, 2))) 45 | #endif 46 | static void append(const char *line, ...) { 47 | va_list args; 48 | va_start(args, line); 49 | 50 | printf("%s", prefix); 51 | vprintf(line, args); 52 | printf("\n"); 53 | 54 | va_end(args); 55 | } 56 | 57 | static inline const char *display_name(const char *code, const char *name) { 58 | return rawcodes ? code : name; 59 | } 60 | 61 | static void on_dict_start(void *ctx, const char *code, const char *name) { 62 | append("%s:", display_name(code, name)); 63 | indent(); 64 | } 65 | 66 | static void on_dict_end(void *ctx, const char *code, const char *name) { 67 | outdent(); 68 | } 69 | 70 | static void on_int32(void *ctx, const char *code, const char *name, int32_t value) { 71 | append("%s: %d", display_name(code, name), value); 72 | } 73 | 74 | static void on_int64(void *ctx, const char *code, const char *name, int64_t value) { 75 | append("%s: %lld", display_name(code, name), value); 76 | } 77 | 78 | static void on_uint32(void *ctx, const char *code, const char *name, uint32_t value) { 79 | if (strcmp(code, "mcnm") == 0) { 80 | char buf[5] = { 81 | (char)((value >> 24) & 0xff), 82 | (char)((value >> 16) & 0xff), 83 | (char)((value >> 8) & 0xff), 84 | (char)(value & 0xff), 85 | 0 86 | }; 87 | append("%s: %s", display_name(code, name), buf); 88 | return; 89 | } else if (strcmp(code, "mcty") == 0) { 90 | const char *description = NULL; 91 | switch ((CONTENT_CODE_TYPE)value) { 92 | case UINT8: 93 | description = "unsigned 8-bit integer"; 94 | break; 95 | case INT8: 96 | description = "8-bit integer"; 97 | break; 98 | case UINT16: 99 | description = "unsigned 16-bit integer"; 100 | break; 101 | case INT16: 102 | description = "16-bit integer"; 103 | break; 104 | case UINT32: 105 | description = "unsigned 32-bit integer"; 106 | break; 107 | case INT32: 108 | description = "32-bit integer"; 109 | break; 110 | case UINT64: 111 | description = "unsigned 64-bit integer"; 112 | break; 113 | case INT64: 114 | description = "64-bit integer"; 115 | break; 116 | case STRING: 117 | description = "string"; 118 | break; 119 | case DATE: 120 | description = "date"; 121 | break; 122 | case VERSION: 123 | description = "version"; 124 | break; 125 | case LIST: 126 | description = "list"; 127 | break; 128 | } 129 | 130 | if (description != NULL) { 131 | append("%s: %s (%u)", display_name(code, name), description, value); 132 | return; 133 | } 134 | } 135 | 136 | append("%s: %u", display_name(code, name), value); 137 | } 138 | 139 | static void on_uint64(void *ctx, const char *code, const char *name, uint64_t value) { 140 | append("%s: %llu", display_name(code, name), value); 141 | } 142 | 143 | static void on_date(void *ctx, const char *code, const char *name, uint32_t value) { 144 | char buf[32]; 145 | time_t timeval = value; 146 | struct tm *timestruct = gmtime(&timeval); 147 | strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S +0000", timestruct); 148 | append("%s: %s", display_name(code, name), buf); 149 | } 150 | 151 | static void on_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { 152 | char *str = (char *)malloc(len + 1); 153 | strncpy(str, buf, len); 154 | str[len] = '\0'; 155 | append("%s: %s", display_name(code, name), str); 156 | free(str); 157 | } 158 | 159 | static void on_data(void *ctx, const char *code, const char *name, const char *buf, size_t len) { 160 | size_t i; 161 | printf("%s%s: ", prefix, display_name(code, name)); 162 | for (i = 0; i < len; i++) { 163 | putc(hexchars[(unsigned char)buf[i] >> 4], stdout); 164 | putc(hexchars[(unsigned char)buf[i] & 0x0f], stdout); 165 | putc(' ', stdout); 166 | } 167 | printf("\n"); 168 | } 169 | 170 | static void print_usage(void) { 171 | printf("" 172 | "Usage: dmapprint [options] [file]\n" 173 | "\n" 174 | "Reads Digital Media Access Protocol (DMAP) input from a file or standard input\n" 175 | "and prints it in a human-readable format.\n" 176 | "\n" 177 | "Options:\n" 178 | " -h, --help Show this help message and exit\n" 179 | " -r, --raw-codes Don't map content codes to human-readable names\n" 180 | " --version Show the version and exit \n"); 181 | } 182 | 183 | int main(int argc, char *argv[]) { 184 | dmap_settings settings = { 185 | .on_dict_start = on_dict_start, 186 | .on_dict_end = on_dict_end, 187 | .on_int32 = on_int32, 188 | .on_int64 = on_int64, 189 | .on_uint32 = on_uint32, 190 | .on_uint64 = on_uint64, 191 | .on_date = on_date, 192 | .on_string = on_string, 193 | .on_data = on_data, 194 | .ctx = 0 195 | }; 196 | 197 | char *buf = NULL; 198 | size_t size = 0; 199 | ssize_t result = 0; 200 | int optchar; 201 | 202 | static struct option longopts[] = { 203 | { "help", no_argument, NULL, 'h' }, 204 | { "raw-codes", no_argument, NULL, 'r' }, 205 | { "version", no_argument, NULL, 'v' }, 206 | { NULL, 0, NULL, 0 } 207 | }; 208 | 209 | while ((optchar = getopt_long(argc, argv, "hr", longopts, NULL)) != -1) { 210 | switch (optchar) { 211 | case 'h': 212 | print_usage(); 213 | return 0; 214 | case 'r': 215 | rawcodes = 1; 216 | break; 217 | case 'v': 218 | printf("dmapprint %s\n", dmap_version_string()); 219 | return 0; 220 | case 0: 221 | break; 222 | case '?': 223 | return 1; 224 | default: 225 | fprintf(stderr, "unhandled option -%c\n", optchar); 226 | return 1; 227 | } 228 | } 229 | argc -= optind; 230 | argv += optind; 231 | 232 | if (argc >= 1) { 233 | const char *path = argv[0]; 234 | 235 | struct stat s; 236 | if (stat(path, &s) != 0 || s.st_size < 0) { 237 | return 1; 238 | } 239 | 240 | size = (size_t)s.st_size; 241 | buf = malloc(size); 242 | 243 | int fd = open(path, O_RDONLY); 244 | if (fd == -1) 245 | return 1; 246 | 247 | do { 248 | result = read(fd, buf, size); 249 | } while (result == -1 && errno == EINTR); 250 | 251 | close(fd); 252 | 253 | if (result == -1) 254 | return 1; 255 | } else { 256 | size_t bufIncrement = 60 * 1024; 257 | 258 | do { 259 | size += (size_t)result; 260 | buf = realloc(buf, size + bufIncrement); 261 | result = read(fileno(stdin), &buf[size], bufIncrement); 262 | } while (result > 0); 263 | } 264 | 265 | dmap_parse(&settings, buf, size); 266 | 267 | free(buf); 268 | 269 | return 0; 270 | } 271 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | #include "dmap_parser.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static char output[2048] = {0}; 11 | static size_t outpos = 0; 12 | static char prefix[64] = {0}; 13 | static const char hexchars[] = "0123456789abcdef"; 14 | 15 | typedef struct { 16 | const char *name; 17 | const uint8_t msg[1024]; 18 | size_t msglen; 19 | const char *expected; 20 | } test; 21 | 22 | static const test tests[] = { 23 | 24 | /* Basic types */ 25 | { 26 | "8-bit unsigned integer", 27 | { 0x61, 0x65, 0x50, 0x53, 0x00, 0x00, 0x00, 0x01, 0x80 }, 28 | 9, 29 | "com.apple.itunes.special-playlist: 128\n" 30 | }, 31 | { 32 | "8-bit signed integer", 33 | { 0x61, 0x73, 0x72, 0x76, 0x00, 0x00, 0x00, 0x01, 0xce }, 34 | 9, 35 | "daap.songrelativevolume: -50\n" 36 | }, 37 | { 38 | "16-bit unsigned integer", 39 | { 0x61, 0x73, 0x74, 0x6d, 0x00, 0x00, 0x00, 0x02, 0x80, 0x00 }, 40 | 10, 41 | "daap.songtime: 32768\n" 42 | }, 43 | { 44 | "16-bit signed integer", 45 | { 0x6d, 0x73, 0x74, 0x6f, 0x00, 0x00, 0x00, 0x02, 0xf8, 0x30 }, 46 | 10, 47 | "dmap.utcoffset: -2000\n" 48 | }, 49 | { 50 | "32-bit unsigned integer", 51 | { 0x6d, 0x69, 0x69, 0x64, 0x00, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x00 }, 52 | 12, 53 | "dmap.itemid: 2147483648\n" 54 | }, 55 | { 56 | "32-bit signed integer", 57 | { 0x6d, 0x73, 0x74, 0x6f, 0x00, 0x00, 0x00, 0x04, 0xf6, 0xdc, 0xda, 0x05 }, 58 | 12, 59 | "dmap.utcoffset: -153298427\n" 60 | }, 61 | { 62 | "64-bit unsigned integer", 63 | { 64 | 0x6d, 0x70, 0x65, 0x72, 0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x00, 65 | 0x00, 0x00, 0x00, 0x00 66 | }, 67 | 16, 68 | "dmap.persistentid: 9223372036854775808\n" 69 | }, 70 | { 71 | "64-bit signed integer", 72 | { 73 | 0x6d, 0x73, 0x74, 0x6f, 0x00, 0x00, 0x00, 0x08, 0xa3, 0xfa, 0xc4, 0x95, 74 | 0x28, 0x1e, 0xed, 0xdd 75 | }, 76 | 16, 77 | "dmap.utcoffset: -6630771356447347235\n" 78 | }, 79 | { 80 | "Unmapped integer as data", 81 | { 82 | 0x6d, 0x70, 0x65, 0x72, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x02, 0x03, 83 | 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff 84 | }, 85 | 24, 86 | "dmap.persistentid: <00 01 02 03 04 05 06 07 08 09 aa bb cc dd ee ff>\n" 87 | }, 88 | { 89 | "Data", 90 | { 0x61, 0x65, 0x43, 0x44, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x02, 0x03 }, 91 | 12, 92 | "com.apple.itunes.flat-chapter-data: <00 01 02 03>\n" 93 | }, 94 | { 95 | "Date", 96 | { 0x6d, 0x73, 0x74, 0x63, 0x00, 0x00, 0x00, 0x04, 0x52, 0xc5, 0xb9, 0x2c }, 97 | 12, 98 | "dmap.utctime: 2014-01-02 19:08:28 +0000\n" 99 | }, 100 | { 101 | "Version", 102 | { 0x6d, 0x70, 0x72, 0x6f, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x0a }, 103 | 12, 104 | "dmap.protocolversion: 2.10\n" 105 | }, 106 | 107 | /* Unknown types */ 108 | { 109 | "Unknown type: 8-bit integer", 110 | { 0x74, 0x76, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x01, 0x01 }, 111 | 9, 112 | "tval: 1\n" 113 | }, 114 | { 115 | "Unknown type: 64-bit unsigned integer", 116 | { 117 | 0x74, 0x76, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x00, 118 | 0x00, 0x00, 0x00, 0x00 119 | }, 120 | 16, 121 | "tval: 9223372036854775808\n" 122 | }, 123 | { 124 | "Unknown type: dictionary", 125 | { 126 | 0x64, 0x69, 0x63, 0x74, 0x00, 0x00, 0x00, 0x0c, 0x74, 0x76, 0x61, 0x6c, 127 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01 128 | }, 129 | 20, 130 | "dict:\n" 131 | " tval: 1\n" 132 | }, 133 | 134 | /* Parsing errors */ 135 | { 136 | "Incomplete fourchar", 137 | { 0x64, 0x69, 0x63 }, 138 | 3, 139 | NULL 140 | }, 141 | { 142 | "No length", 143 | { 0x64, 0x69, 0x63, 0x74 }, 144 | 4, 145 | NULL 146 | }, 147 | { 148 | "Incomplete length", 149 | { 0x64, 0x69, 0x63, 0x74, 0x00, 0x00, 0x00 }, 150 | 7, 151 | NULL 152 | }, 153 | { 154 | "No data", 155 | { 0x64, 0x69, 0x63, 0x74, 0x00, 0x00, 0x00, 0x02 }, 156 | 8, 157 | NULL 158 | }, 159 | { 160 | "Invalid length", 161 | { 0x64, 0x69, 0x63, 0x74, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01 }, 162 | 12, 163 | NULL 164 | }, 165 | { 166 | "Invalid inner dictionary length", 167 | { 168 | 0x61, 0x64, 0x62, 0x73, 0x00, 0x00, 0x00, 0x0c, 0x74, 0x76, 0x61, 0x6c, 169 | 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01 170 | }, 171 | 20, 172 | NULL 173 | }, 174 | 175 | /* Messages */ 176 | { 177 | "Login response", 178 | { 179 | 0x6d, 0x6c, 0x6f, 0x67, 0x00, 0x00, 0x00, 0x18, 0x6d, 0x73, 0x74, 0x74, 180 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x6c, 0x69, 0x64, 181 | 0x00, 0x00, 0x00, 0x04, 0x3b, 0x9a, 0x7c, 0x99 182 | }, 183 | 32, 184 | "dmap.loginresponse:\n" 185 | " dmap.status: 200\n" 186 | " dmap.sessionid: 999980185\n" 187 | }, 188 | { 189 | "Simple database item", 190 | { 191 | 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x30, 0x6d, 0x69, 0x69, 0x64, 192 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x29, 0x6b, 0x6d, 0x69, 0x6e, 0x6d, 193 | 0x00, 0x00, 0x00, 0x10, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 194 | 0x69, 0x76, 0x65, 0x20, 0x52, 0x6f, 0x63, 0x6b, 0x6d, 0x69, 0x6d, 0x63, 195 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x05 196 | }, 197 | 56, 198 | "dmap.listingitem:\n" 199 | " dmap.itemid: 10603\n" 200 | " dmap.itemname: Alternative Rock\n" 201 | " dmap.itemcount: 261\n" 202 | }, 203 | { 204 | "Speaker list response", 205 | { 206 | 0x63, 0x61, 0x73, 0x70, 0x00, 0x00, 0x00, 0xf5, 0x6d, 0x73, 0x74, 0x74, 207 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x64, 0x63, 0x6c, 208 | 0x00, 0x00, 0x00, 0x3e, 0x6d, 0x69, 0x6e, 0x6d, 0x00, 0x00, 0x00, 0x08, 209 | 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x63, 0x6d, 0x76, 0x6f, 210 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x63, 0x61, 0x76, 0x64, 211 | 0x00, 0x00, 0x00, 0x01, 0x01, 0x6d, 0x73, 0x6d, 0x61, 0x00, 0x00, 0x00, 212 | 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x61, 0x69, 213 | 0x61, 0x00, 0x00, 0x00, 0x01, 0x01, 0x6d, 0x64, 0x63, 0x6c, 0x00, 0x00, 214 | 0x00, 0x35, 0x6d, 0x69, 0x6e, 0x6d, 0x00, 0x00, 0x00, 0x08, 0x41, 0x70, 215 | 0x70, 0x6c, 0x65, 0x20, 0x54, 0x56, 0x63, 0x6d, 0x76, 0x6f, 0x00, 0x00, 216 | 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x6d, 0x73, 0x6d, 0x61, 0x00, 0x00, 217 | 0x00, 0x08, 0x00, 0x00, 0x70, 0x73, 0xcb, 0xd4, 0x98, 0x72, 0x63, 0x61, 218 | 0x69, 0x76, 0x00, 0x00, 0x00, 0x01, 0x01, 0x6d, 0x64, 0x63, 0x6c, 0x00, 219 | 0x00, 0x00, 0x2b, 0x63, 0x6d, 0x76, 0x6f, 0x00, 0x00, 0x00, 0x04, 0x00, 220 | 0x00, 0x00, 0x64, 0x6d, 0x69, 0x6e, 0x6d, 0x00, 0x00, 0x00, 0x07, 0x42, 221 | 0x65, 0x64, 0x72, 0x6f, 0x6f, 0x6d, 0x6d, 0x73, 0x6d, 0x61, 0x00, 0x00, 222 | 0x00, 0x08, 0x00, 0x00, 0x44, 0xd8, 0x84, 0x6a, 0xb6, 0xfc, 0x6d, 0x64, 223 | 0x63, 0x6c, 0x00, 0x00, 0x00, 0x2b, 0x63, 0x6d, 0x76, 0x6f, 0x00, 0x00, 224 | 0x00, 0x04, 0x00, 0x00, 0x00, 0x64, 0x6d, 0x69, 0x6e, 0x6d, 0x00, 0x00, 225 | 0x00, 0x07, 0x4b, 0x69, 0x74, 0x63, 0x68, 0x65, 0x6e, 0x6d, 0x73, 0x6d, 226 | 0x61, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0xf0, 0xb4, 0x79, 0x05, 0x66, 227 | 0x0a 228 | }, 229 | 253, 230 | "dacp.speakers:\n" 231 | " dmap.status: 200\n" 232 | " dmap.dictionary:\n" 233 | " dmap.itemname: Computer\n" 234 | " dmcp.volume: 100\n" 235 | " cavd: 1\n" 236 | " dmap.machineaddress: 0\n" 237 | " dacp.isactive: 1\n" 238 | " dmap.dictionary:\n" 239 | " dmap.itemname: Apple TV\n" 240 | " dmcp.volume: 100\n" 241 | " dmap.machineaddress: 123642643257458\n" 242 | " caiv: 1\n" 243 | " dmap.dictionary:\n" 244 | " dmcp.volume: 100\n" 245 | " dmap.itemname: Bedroom\n" 246 | " dmap.machineaddress: 75696725210876\n" 247 | " dmap.dictionary:\n" 248 | " dmcp.volume: 100\n" 249 | " dmap.itemname: Kitchen\n" 250 | " dmap.machineaddress: 264657915176458\n" 251 | }, 252 | { 253 | "Database songs response", 254 | { 255 | 0x61, 0x64, 0x62, 0x73, 0x00, 0x00, 0x00, 0xb5, 0x6d, 0x73, 0x74, 0x74, 256 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 257 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 258 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 259 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x6c, 0x63, 0x6c, 0x00, 0x00, 0x00, 260 | 0x80, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x78, 0x6d, 0x69, 0x6b, 261 | 0x64, 0x00, 0x00, 0x00, 0x01, 0x02, 0x61, 0x73, 0x61, 0x72, 0x00, 0x00, 262 | 0x00, 0x06, 0x4d, 0x6f, 0x67, 0x77, 0x61, 0x69, 0x61, 0x73, 0x64, 0x6e, 263 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x61, 0x73, 0x74, 0x6e, 0x00, 0x00, 264 | 0x00, 0x02, 0x00, 0x01, 0x6d, 0x69, 0x69, 0x64, 0x00, 0x00, 0x00, 0x04, 265 | 0x00, 0x00, 0x11, 0x3e, 0x6d, 0x69, 0x6e, 0x6d, 0x00, 0x00, 0x00, 0x09, 266 | 0x41, 0x75, 0x74, 0x6f, 0x20, 0x52, 0x6f, 0x63, 0x6b, 0x6d, 0x70, 0x65, 267 | 0x72, 0x00, 0x00, 0x00, 0x08, 0xe6, 0xcc, 0x5e, 0x6f, 0x61, 0xc9, 0x12, 268 | 0x23, 0x61, 0x73, 0x61, 0x69, 0x00, 0x00, 0x00, 0x08, 0x51, 0xe5, 0x26, 269 | 0xcf, 0xc7, 0xcd, 0x8f, 0x53, 0x61, 0x73, 0x72, 0x69, 0x00, 0x00, 0x00, 270 | 0x08, 0xd2, 0xc1, 0x7b, 0x9c, 0x90, 0xb1, 0xf2, 0x9a 271 | }, 272 | 189, 273 | "daap.databasesongs:\n" 274 | " dmap.status: 200\n" 275 | " dmap.updatetype: 0\n" 276 | " dmap.specifiedtotalcount: 1\n" 277 | " dmap.returnedcount: 1\n" 278 | " dmap.listing:\n" 279 | " dmap.listingitem:\n" 280 | " dmap.itemkind: 2\n" 281 | " daap.songartist: Mogwai\n" 282 | " daap.songdiscnumber: 1\n" 283 | " daap.songtracknumber: 1\n" 284 | " dmap.itemid: 4414\n" 285 | " dmap.itemname: Auto Rock\n" 286 | " dmap.persistentid: 16630771356447347235\n" 287 | " daap.songalbumid: 5901165560591126355\n" 288 | " daap.songartistid: 15186555330842718874\n" 289 | }, 290 | { 291 | "Database songs response with date", 292 | { 293 | 0x61, 0x64, 0x62, 0x73, 0x00, 0x00, 0x00, 0x5e, 0x6d, 0x73, 0x74, 0x74, 294 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 295 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 296 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 297 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x6c, 0x63, 0x6c, 0x00, 0x00, 0x00, 298 | 0x29, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x21, 0x6d, 0x69, 0x6b, 299 | 0x64, 0x00, 0x00, 0x00, 0x01, 0x02, 0x61, 0x73, 0x64, 0x61, 0x00, 0x00, 300 | 0x00, 0x04, 0x45, 0x02, 0x1f, 0x23, 0x6d, 0x69, 0x69, 0x64, 0x00, 0x00, 301 | 0x00, 0x04, 0x00, 0x00, 0x07, 0x6f 302 | }, 303 | 102, 304 | "daap.databasesongs:\n" 305 | " dmap.status: 200\n" 306 | " dmap.updatetype: 0\n" 307 | " dmap.specifiedtotalcount: 1\n" 308 | " dmap.returnedcount: 1\n" 309 | " dmap.listing:\n" 310 | " dmap.listingitem:\n" 311 | " dmap.itemkind: 2\n" 312 | " daap.songdateadded: 2006-09-09 01:55:47 +0000\n" 313 | " dmap.itemid: 1903\n" 314 | }, 315 | { 316 | "Database songs response with relative volume", 317 | { 318 | 0x61, 0x64, 0x62, 0x73, 0x00, 0x00, 0x00, 0x4f, 0x6d, 0x73, 0x74, 0x74, 319 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 320 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 321 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 322 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x6c, 0x63, 0x6c, 0x00, 0x00, 0x00, 323 | 0x1a, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x12, 0x6d, 0x69, 0x6b, 324 | 0x64, 0x00, 0x00, 0x00, 0x01, 0x02, 0x61, 0x73, 0x72, 0x76, 0x00, 0x00, 325 | 0x00, 0x01, 0xc4 326 | }, 327 | 87, 328 | "daap.databasesongs:\n" 329 | " dmap.status: 200\n" 330 | " dmap.updatetype: 0\n" 331 | " dmap.specifiedtotalcount: 1\n" 332 | " dmap.returnedcount: 1\n" 333 | " dmap.listing:\n" 334 | " dmap.listingitem:\n" 335 | " dmap.itemkind: 2\n" 336 | " daap.songrelativevolume: -60\n" 337 | }, 338 | { 339 | "Database playlists response with dmap.haschildcontainers", 340 | { 341 | 0x61, 0x70, 0x6c, 0x79, 0x00, 0x00, 0x00, 0x61, 0x6d, 0x73, 0x74, 0x74, 342 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 343 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 344 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 345 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x6c, 0x63, 0x6c, 0x00, 0x00, 0x00, 346 | 0x2c, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x24, 0x6d, 0x69, 0x69, 347 | 0x64, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0xe8, 0x1d, 0x66, 0x8d, 0x63, 348 | 0x68, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x69, 0x6d, 349 | 0x63, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02 350 | }, 351 | 105, 352 | "daap.databaseplaylists:\n" 353 | " dmap.status: 200\n" 354 | " dmap.updatetype: 0\n" 355 | " dmap.specifiedtotalcount: 1\n" 356 | " dmap.returnedcount: 1\n" 357 | " dmap.listing:\n" 358 | " dmap.listingitem:\n" 359 | " dmap.itemid: 59421\n" 360 | " dmap.haschildcontainers: 1\n" 361 | " dmap.itemcount: 2\n" 362 | }, 363 | { 364 | "Browse genre listing", 365 | { 366 | 0x61, 0x62, 0x72, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x6d, 0x73, 0x74, 0x74, 367 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 368 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 369 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 370 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x61, 0x62, 0x67, 0x6e, 0x00, 0x00, 0x00, 371 | 0x0c, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73, 372 | 0x74 373 | }, 374 | 73, 375 | "daap.databasebrowse:\n" 376 | " dmap.status: 200\n" 377 | " dmap.updatetype: 0\n" 378 | " dmap.specifiedtotalcount: 1\n" 379 | " dmap.returnedcount: 1\n" 380 | " daap.browsegenrelisting:\n" 381 | " dmap.listingitem: Test\n" 382 | }, 383 | { 384 | "Browse artist listing", 385 | { 386 | 0x61, 0x62, 0x72, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x6d, 0x73, 0x74, 0x74, 387 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 388 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 389 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 390 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x61, 0x62, 0x61, 0x72, 0x00, 0x00, 0x00, 391 | 0x0c, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73, 392 | 0x74 393 | }, 394 | 73, 395 | "daap.databasebrowse:\n" 396 | " dmap.status: 200\n" 397 | " dmap.updatetype: 0\n" 398 | " dmap.specifiedtotalcount: 1\n" 399 | " dmap.returnedcount: 1\n" 400 | " daap.browseartistlisting:\n" 401 | " dmap.listingitem: Test\n" 402 | }, 403 | { 404 | "Browse composer listing", 405 | { 406 | 0x61, 0x62, 0x72, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x6d, 0x73, 0x74, 0x74, 407 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 408 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 409 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 410 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x61, 0x62, 0x63, 0x70, 0x00, 0x00, 0x00, 411 | 0x0c, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73, 412 | 0x74 413 | }, 414 | 73, 415 | "daap.databasebrowse:\n" 416 | " dmap.status: 200\n" 417 | " dmap.updatetype: 0\n" 418 | " dmap.specifiedtotalcount: 1\n" 419 | " dmap.returnedcount: 1\n" 420 | " daap.browsecomposerlisting:\n" 421 | " dmap.listingitem: Test\n" 422 | }, 423 | { 424 | "Browse album listing", 425 | { 426 | 0x61, 0x62, 0x72, 0x6f, 0x00, 0x00, 0x00, 0x41, 0x6d, 0x73, 0x74, 0x74, 427 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xc8, 0x6d, 0x75, 0x74, 0x79, 428 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x6d, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 429 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x6d, 0x72, 0x63, 0x6f, 0x00, 0x00, 0x00, 430 | 0x04, 0x00, 0x00, 0x00, 0x01, 0x61, 0x62, 0x61, 0x6c, 0x00, 0x00, 0x00, 431 | 0x0c, 0x6d, 0x6c, 0x69, 0x74, 0x00, 0x00, 0x00, 0x04, 0x54, 0x65, 0x73, 432 | 0x74 433 | }, 434 | 73, 435 | "daap.databasebrowse:\n" 436 | " dmap.status: 200\n" 437 | " dmap.updatetype: 0\n" 438 | " dmap.specifiedtotalcount: 1\n" 439 | " dmap.returnedcount: 1\n" 440 | " daap.browsealbumlisting:\n" 441 | " dmap.listingitem: Test\n" 442 | } 443 | }; 444 | 445 | static void indent() { 446 | strcat(prefix, " "); 447 | } 448 | 449 | static void outdent() { 450 | size_t len = strlen(prefix); 451 | if (len >= 2) { 452 | prefix[len - 2] = '\0'; 453 | } 454 | } 455 | 456 | #if defined(__GNUC__) 457 | __attribute__((format(printf, 1, 2))) 458 | #endif 459 | static void append(const char *line, ...) { 460 | int count; 461 | va_list args; 462 | va_start(args, line); 463 | 464 | strcat(&output[outpos], prefix); 465 | outpos += strlen(prefix); 466 | count = vsprintf(&output[outpos], line, args); 467 | if (count < 0) { 468 | abort(); 469 | } 470 | outpos += (size_t)count; 471 | strcat(&output[outpos], "\n"); 472 | outpos++; 473 | 474 | va_end(args); 475 | } 476 | 477 | static void on_dict_start(void *ctx, const char *code, const char *name) { 478 | append("%s:", name); 479 | indent(); 480 | } 481 | 482 | static void on_dict_end(void *ctx, const char *code, const char *name) { 483 | outdent(); 484 | } 485 | 486 | static void on_int32(void *ctx, const char *code, const char *name, int32_t value) { 487 | append("%s: %d", name, value); 488 | } 489 | 490 | static void on_int64(void *ctx, const char *code, const char *name, int64_t value) { 491 | append("%s: %lld", name, value); 492 | } 493 | 494 | static void on_uint32(void *ctx, const char *code, const char *name, uint32_t value) { 495 | append("%s: %u", name, value); 496 | } 497 | 498 | static void on_uint64(void *ctx, const char *code, const char *name, uint64_t value) { 499 | append("%s: %llu", name, value); 500 | } 501 | 502 | static void on_date(void *ctx, const char *code, const char *name, uint32_t value) { 503 | char buf[32]; 504 | time_t timeval = value; 505 | struct tm *timestruct = gmtime(&timeval); 506 | strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S +0000", timestruct); 507 | append("%s: %s", name, buf); 508 | } 509 | 510 | static void on_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) { 511 | char *str = (char *)malloc(len + 1); 512 | strncpy(str, buf, len); 513 | str[len] = '\0'; 514 | append("%s: %s", name, str); 515 | free(str); 516 | } 517 | 518 | static void on_data(void *ctx, const char *code, const char *name, const char *buf, size_t len) { 519 | char *str = malloc((len * 3) + 1); 520 | size_t i; 521 | char *p = str; 522 | for (i = 0; i < len; i++) { 523 | if (i > 0) 524 | *p++ = ' '; 525 | 526 | *p++ = hexchars[(unsigned char)buf[i] >> 4]; 527 | *p++ = hexchars[(unsigned char)buf[i] & 0x0f]; 528 | } 529 | *p = '\0'; 530 | append("%s: <%s>", name, str); 531 | free(str); 532 | } 533 | 534 | int main() { 535 | dmap_settings settings = { 536 | on_dict_start, 537 | on_dict_end, 538 | on_int32, 539 | on_int64, 540 | on_uint32, 541 | on_uint64, 542 | on_date, 543 | on_string, 544 | on_data, 545 | NULL 546 | }; 547 | 548 | int i; 549 | int count = sizeof(tests) / sizeof(test); 550 | int failcount = 0; 551 | int result; 552 | 553 | assert(dmap_version() > 1000000); 554 | assert(strlen(dmap_version_string()) > 0); 555 | 556 | assert(dmap_name_from_code(NULL) == NULL); 557 | assert(dmap_name_from_code("") == NULL); 558 | assert(strcmp(dmap_name_from_code("minm"), "dmap.itemname") == 0); 559 | 560 | assert(dmap_parse(NULL, NULL, 0) == -1); 561 | assert(dmap_parse(&settings, NULL, 0) == -1); 562 | 563 | for (i = 0; i < count; i++) { 564 | output[0] = '\0'; 565 | outpos = 0; 566 | prefix[0] = '\0'; 567 | result = dmap_parse(&settings, (const char *)tests[i].msg, tests[i].msglen); 568 | if (tests[i].expected != NULL) { 569 | if (result != 0) { 570 | failcount++; 571 | printf("Failed test: %s returned error response\n", 572 | tests[i].name); 573 | } else if (strcmp(output, tests[i].expected) != 0) { 574 | failcount++; 575 | printf("Failed test: %s\nExpected:\n%sActual:\n%s\n", 576 | tests[i].name, 577 | tests[i].expected, 578 | output); 579 | } 580 | } else if (result != -1) { 581 | failcount++; 582 | printf("Failed test: %s, should have errored\n", 583 | tests[i].name); 584 | } 585 | } 586 | 587 | printf("%d tests passed", count - failcount); 588 | if (failcount > 0) 589 | printf(", %d failed", failcount); 590 | printf("\n"); 591 | 592 | return failcount ? 1 : 0; 593 | } 594 | --------------------------------------------------------------------------------