tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

mpd_common.c (37306B)


      1 #define _POSIX_C_SOURCE 200809L
      2 
      3 #include "mpd_common.h"
      4 
      5 #include <errno.h>
      6 #include <fcntl.h>
      7 #include <limits.h>
      8 #include <poll.h>
      9 #include <signal.h>
     10 #include <spawn.h>
     11 #include <stdint.h>
     12 #include <stdlib.h>
     13 #include <string.h>
     14 #include <sys/wait.h>
     15 #include <unistd.h>
     16 #include <wordexp.h>
     17 
     18 extern char **environ;
     19 
     20 enum helper_state {
     21   HELPER_UNRESOLVED,
     22   HELPER_READY,
     23   HELPER_FAILED,
     24 };
     25 
     26 static enum helper_state profile_helper_state;
     27 
     28 #define HELPER_DEFAULT_TIMEOUT_SECONDS 10U
     29 #define HELPER_MAX_TIMEOUT_SECONDS 3600U
     30 #define PLAYER_SNAPSHOT_RETRIES 3U
     31 
     32 static bool safe_helper_value(const char *value) {
     33   if (*value == '\0')
     34     return false;
     35   for (const unsigned char *cursor = (const unsigned char *)value;
     36        *cursor != '\0'; cursor++) {
     37     if (*cursor < 0x20U || *cursor == 0x7fU)
     38       return false;
     39   }
     40   return true;
     41 }
     42 
     43 static bool parse_helper_port(const char *value) {
     44   if (*value == '\0' || *value == '-')
     45     return false;
     46   errno = 0;
     47   char *end;
     48   unsigned long port = strtoul(value, &end, 10);
     49   return errno == 0 && *end == '\0' && port > 0UL && port <= 65535UL;
     50 }
     51 
     52 static unsigned helper_timeout_seconds(void) {
     53   const char *configured = getenv("MPD_CONFIG_HELPER_TIMEOUT");
     54   if (configured == NULL || *configured == '\0')
     55     return HELPER_DEFAULT_TIMEOUT_SECONDS;
     56 
     57   size_t timeout;
     58   if (!mt_parse_size(configured, &timeout) || timeout == 0U ||
     59       timeout > HELPER_MAX_TIMEOUT_SECONDS) {
     60     fprintf(stderr,
     61             "mpd: MPD_CONFIG_HELPER_TIMEOUT must be between 1 and %u "
     62             "seconds\n",
     63             HELPER_MAX_TIMEOUT_SECONDS);
     64     return 0U;
     65   }
     66   return (unsigned)timeout;
     67 }
     68 
     69 static bool monotonic_now(struct timespec *value) {
     70   if (clock_gettime(CLOCK_MONOTONIC, value) == 0)
     71     return true;
     72   perror("mpd: read monotonic clock");
     73   return false;
     74 }
     75 
     76 static int milliseconds_until(const struct timespec *deadline) {
     77   struct timespec now;
     78   if (!monotonic_now(&now))
     79     return -1;
     80   time_t seconds = deadline->tv_sec - now.tv_sec;
     81   long nanoseconds = deadline->tv_nsec - now.tv_nsec;
     82   if (nanoseconds < 0) {
     83     seconds--;
     84     nanoseconds += 1000000000L;
     85   }
     86   if (seconds < 0 || (seconds == 0 && nanoseconds == 0))
     87     return 0;
     88   if (seconds > (time_t)(INT_MAX / 1000))
     89     return INT_MAX;
     90   long milliseconds = nanoseconds / 1000000L;
     91   if (nanoseconds % 1000000L != 0)
     92     milliseconds++;
     93   return (int)(seconds * 1000 + milliseconds);
     94 }
     95 
     96 static void terminate_helper(pid_t pid) {
     97   if (kill(-pid, SIGKILL) != 0 && errno != ESRCH)
     98     perror("mpd: terminate config helper");
     99   int status;
    100   while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
    101   }
    102 }
    103 
    104 static bool wait_for_helper(pid_t pid, int *status,
    105                             const struct timespec *deadline) {
    106   for (;;) {
    107     pid_t result = waitpid(pid, status, WNOHANG);
    108     if (result == pid)
    109       return true;
    110     if (result < 0) {
    111       if (errno == EINTR)
    112         continue;
    113       perror("mpd: wait for config helper");
    114       return false;
    115     }
    116     int remaining = milliseconds_until(deadline);
    117     if (remaining <= 0)
    118       return false;
    119     int pause = remaining < 20 ? remaining : 20;
    120     struct timespec delay = {.tv_sec = 0, .tv_nsec = (long)pause * 1000000L};
    121     while (nanosleep(&delay, &delay) < 0 && errno == EINTR) {
    122     }
    123   }
    124 }
    125 
    126 static bool parse_helper_output(char *output, size_t length,
    127                                 const char *helper) {
    128   char *host = NULL;
    129   char *port = NULL;
    130   bool valid = true;
    131 
    132   if (memchr(output, '\0', length) != NULL)
    133     valid = false;
    134   output[length] = '\0';
    135   char *cursor = output;
    136   while (valid && *cursor != '\0') {
    137     char *line = cursor;
    138     char *newline = strchr(cursor, '\n');
    139     if (newline == NULL) {
    140       cursor += strlen(cursor);
    141     } else {
    142       *newline = '\0';
    143       cursor = newline + 1;
    144     }
    145     size_t line_length = strlen(line);
    146     while (line_length > 0U && line[line_length - 1U] == '\r')
    147       line[--line_length] = '\0';
    148     if (line_length == 0U)
    149       continue;
    150 
    151     char **destination = NULL;
    152     const char *value = NULL;
    153     if (strncmp(line, "MPD_HOST=", 9U) == 0) {
    154       destination = &host;
    155       value = line + 9;
    156     } else if (strncmp(line, "MPD_PORT=", 9U) == 0) {
    157       destination = &port;
    158       value = line + 9;
    159     } else {
    160       valid = false;
    161       continue;
    162     }
    163     if (*destination != NULL || !safe_helper_value(value) ||
    164         (destination == &port && !parse_helper_port(value))) {
    165       valid = false;
    166       continue;
    167     }
    168     *destination = strdup(value);
    169     if (*destination == NULL) {
    170       perror("mpd: store config helper output");
    171       valid = false;
    172     }
    173   }
    174 
    175   if (host == NULL || port == NULL)
    176     valid = false;
    177   if (!valid) {
    178     fprintf(stderr,
    179             "mpd: config helper '%s' must print exactly valid MPD_HOST and "
    180             "MPD_PORT assignments\n",
    181             helper);
    182   } else if (setenv("MPD_HOST", host, 1) != 0 ||
    183              setenv("MPD_PORT", port, 1) != 0) {
    184     perror("mpd: apply config helper output");
    185     valid = false;
    186   }
    187 
    188   free(host);
    189   free(port);
    190   return valid;
    191 }
    192 
    193 static bool collect_helper_output(int descriptor, pid_t pid, const char *helper,
    194                                   unsigned timeout_seconds) {
    195   int flags = fcntl(descriptor, F_GETFL);
    196   if (flags < 0 || fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) != 0) {
    197     perror("mpd: configure config helper output");
    198     close(descriptor);
    199     terminate_helper(pid);
    200     return false;
    201   }
    202 
    203   struct timespec deadline;
    204   if (!monotonic_now(&deadline)) {
    205     close(descriptor);
    206     terminate_helper(pid);
    207     return false;
    208   }
    209   deadline.tv_sec += (time_t)timeout_seconds;
    210 
    211   char output[4098U];
    212   size_t length = 0U;
    213   bool overflow = false;
    214   bool complete = false;
    215   while (!complete) {
    216     char discard[1024U];
    217     void *destination = overflow ? (void *)discard : (void *)(output + length);
    218     size_t available = overflow ? sizeof(discard) : 4097U - length;
    219     ssize_t count = read(descriptor, destination, available);
    220     if (count > 0) {
    221       if (!overflow) {
    222         length += (size_t)count;
    223         if (length > 4096U)
    224           overflow = true;
    225       }
    226       continue;
    227     }
    228     if (count == 0) {
    229       complete = true;
    230       break;
    231     }
    232     if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
    233       perror("mpd: read config helper output");
    234       close(descriptor);
    235       terminate_helper(pid);
    236       return false;
    237     }
    238     if (errno == EINTR)
    239       continue;
    240 
    241     int remaining = milliseconds_until(&deadline);
    242     if (remaining <= 0)
    243       break;
    244     struct pollfd poll_descriptor = {.fd = descriptor, .events = POLLIN};
    245     int result;
    246     do {
    247       result = poll(&poll_descriptor, 1U, remaining);
    248     } while (result < 0 && errno == EINTR);
    249     if (result < 0) {
    250       perror("mpd: poll config helper output");
    251       close(descriptor);
    252       terminate_helper(pid);
    253       return false;
    254     }
    255     if (result == 0)
    256       break;
    257   }
    258   if (close(descriptor) != 0)
    259     perror("mpd: close config helper output");
    260 
    261   int status = 0;
    262   if (!complete || !wait_for_helper(pid, &status, &deadline)) {
    263     fprintf(stderr, "mpd: config helper '%s' timed out after %u seconds\n",
    264             helper, timeout_seconds);
    265     terminate_helper(pid);
    266     return false;
    267   }
    268   if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
    269     fprintf(stderr, "mpd: config helper '%s' exited unsuccessfully\n", helper);
    270     return false;
    271   }
    272   if (overflow) {
    273     fprintf(stderr, "mpd: config helper '%s' output exceeds 4096 bytes\n",
    274             helper);
    275     return false;
    276   }
    277   return parse_helper_output(output, length, helper);
    278 }
    279 
    280 static bool run_profile_helper(const char *helper) {
    281   unsigned timeout_seconds = helper_timeout_seconds();
    282   if (timeout_seconds == 0U)
    283     return false;
    284   int descriptors[2];
    285   if (pipe(descriptors) != 0) {
    286     perror("mpd: create config helper pipe");
    287     return false;
    288   }
    289 
    290   posix_spawn_file_actions_t actions;
    291   posix_spawnattr_t attributes;
    292   int error = posix_spawn_file_actions_init(&actions);
    293   bool actions_initialized = error == 0;
    294   bool attributes_initialized = false;
    295   if (error == 0)
    296     error = posix_spawn_file_actions_addclose(&actions, descriptors[0]);
    297   if (error == 0)
    298     error = posix_spawn_file_actions_adddup2(&actions, descriptors[1],
    299                                              STDOUT_FILENO);
    300   if (error == 0)
    301     error = posix_spawn_file_actions_addclose(&actions, descriptors[1]);
    302   if (error == 0) {
    303     error = posix_spawnattr_init(&attributes);
    304     attributes_initialized = error == 0;
    305   }
    306   if (error == 0)
    307     error = posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETPGROUP);
    308   if (error == 0)
    309     error = posix_spawnattr_setpgroup(&attributes, 0);
    310   if (error != 0) {
    311     fprintf(stderr, "mpd: prepare config helper: %s\n", strerror(error));
    312     if (actions_initialized) {
    313       int destroy_error = posix_spawn_file_actions_destroy(&actions);
    314       if (destroy_error != 0)
    315         fprintf(stderr, "mpd: release config helper actions: %s\n",
    316                 strerror(destroy_error));
    317     }
    318     if (attributes_initialized) {
    319       int destroy_error = posix_spawnattr_destroy(&attributes);
    320       if (destroy_error != 0)
    321         fprintf(stderr, "mpd: release config helper attributes: %s\n",
    322                 strerror(destroy_error));
    323     }
    324     close(descriptors[0]);
    325     close(descriptors[1]);
    326     return false;
    327   }
    328 
    329   pid_t pid;
    330   char *arguments[] = {(char *)helper, NULL};
    331   error = posix_spawn(&pid, helper, &actions, &attributes, arguments, environ);
    332   int destroy_error = posix_spawn_file_actions_destroy(&actions);
    333   if (destroy_error != 0)
    334     fprintf(stderr, "mpd: release config helper actions: %s\n",
    335             strerror(destroy_error));
    336   destroy_error = posix_spawnattr_destroy(&attributes);
    337   if (destroy_error != 0)
    338     fprintf(stderr, "mpd: release config helper attributes: %s\n",
    339             strerror(destroy_error));
    340   if (error != 0) {
    341     fprintf(stderr, "mpd: start config helper '%s': %s\n", helper,
    342             strerror(error));
    343     close(descriptors[0]);
    344     close(descriptors[1]);
    345     return false;
    346   }
    347 
    348   close(descriptors[1]);
    349   return collect_helper_output(descriptors[0], pid, helper, timeout_seconds);
    350 }
    351 
    352 static bool resolve_profile_helper(void) {
    353   if (profile_helper_state != HELPER_UNRESOLVED)
    354     return profile_helper_state == HELPER_READY;
    355 
    356   const char *configured = getenv("MPD_CONFIG_HELPER");
    357   if (configured != NULL &&
    358       (*configured == '\0' || strcmp(configured, "off") == 0)) {
    359     profile_helper_state = HELPER_READY;
    360     return true;
    361   }
    362 
    363   char *default_helper = NULL;
    364   const char *helper = configured;
    365   if (helper == NULL) {
    366     const char *home = getenv("HOME");
    367     if (home == NULL || *home == '\0') {
    368       profile_helper_state = HELPER_READY;
    369       return true;
    370     }
    371     size_t home_length = strlen(home);
    372     if (home_length > SIZE_MAX - sizeof("/bin/amen")) {
    373       fprintf(stderr, "mpd: HOME is too long\n");
    374       profile_helper_state = HELPER_FAILED;
    375       return false;
    376     }
    377     size_t length = home_length + sizeof("/bin/amen");
    378     default_helper = malloc(length);
    379     if (default_helper == NULL) {
    380       perror("mpd: config helper path");
    381       profile_helper_state = HELPER_FAILED;
    382       return false;
    383     }
    384     snprintf(default_helper, length, "%s/bin/amen", home);
    385     if (access(default_helper, X_OK) != 0) {
    386       free(default_helper);
    387       profile_helper_state = HELPER_READY;
    388       return true;
    389     }
    390     helper = default_helper;
    391   }
    392 
    393   bool valid = run_profile_helper(helper);
    394   free(default_helper);
    395   profile_helper_state = valid ? HELPER_READY : HELPER_FAILED;
    396   return valid;
    397 }
    398 
    399 struct mpd_connection *mt_connect_to(const char *host, unsigned port) {
    400   if (!resolve_profile_helper())
    401     return NULL;
    402   struct mpd_connection *connection = mpd_connection_new(host, port, 0);
    403   if (connection == NULL) {
    404     fprintf(stderr, "mpd: out of memory while creating connection\n");
    405     return NULL;
    406   }
    407 
    408   if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
    409     mt_report_error(connection, "connect");
    410     mpd_connection_free(connection);
    411     return NULL;
    412   }
    413   return connection;
    414 }
    415 
    416 struct mpd_connection *mt_connect(void) { return mt_connect_to(NULL, 0U); }
    417 
    418 void mt_report_error(const struct mpd_connection *connection,
    419                      const char *context) {
    420   const char *message = connection == NULL
    421                             ? "unknown MPD error"
    422                             : mpd_connection_get_error_message(connection);
    423   fprintf(stderr, "mpd: %s: %s\n", context, message);
    424 }
    425 
    426 void mt_strvec_init(struct mt_strvec *vec) {
    427   vec->items = NULL;
    428   vec->len = 0;
    429   vec->cap = 0;
    430 }
    431 
    432 void mt_strvec_clear(struct mt_strvec *vec) {
    433   for (size_t i = 0; i < vec->len; i++)
    434     free(vec->items[i]);
    435   free(vec->items);
    436   mt_strvec_init(vec);
    437 }
    438 
    439 bool mt_strvec_push_n(struct mt_strvec *vec, const char *value, size_t length) {
    440   if (vec->len == vec->cap) {
    441     size_t cap = vec->cap == 0 ? 16U : vec->cap * 2U;
    442     if (cap < vec->cap || cap > SIZE_MAX / sizeof(*vec->items)) {
    443       errno = ENOMEM;
    444       return false;
    445     }
    446     char **items = realloc(vec->items, cap * sizeof(*items));
    447     if (items == NULL)
    448       return false;
    449     vec->items = items;
    450     vec->cap = cap;
    451   }
    452 
    453   if (length == SIZE_MAX) {
    454     errno = ENOMEM;
    455     return false;
    456   }
    457   char *copy = malloc(length + 1U);
    458   if (copy == NULL)
    459     return false;
    460   memcpy(copy, value, length);
    461   copy[length] = '\0';
    462   vec->items[vec->len] = copy;
    463   vec->len++;
    464   return true;
    465 }
    466 
    467 bool mt_strvec_push(struct mt_strvec *vec, const char *value) {
    468   return mt_strvec_push_n(vec, value, strlen(value));
    469 }
    470 
    471 bool mt_read_lines(FILE *input, struct mt_strvec *lines) {
    472   char *line = NULL;
    473   size_t capacity = 0;
    474   ssize_t length;
    475 
    476   while ((length = getline(&line, &capacity, input)) >= 0) {
    477     while (length > 0 && (line[(size_t)length - 1U] == '\n' ||
    478                           line[(size_t)length - 1U] == '\r'))
    479       line[--length] = '\0';
    480     if (length == 0 || line[0] == '#')
    481       continue;
    482     if (!mt_strvec_push(lines, line)) {
    483       perror("mpd: storing input line");
    484       free(line);
    485       return false;
    486     }
    487   }
    488 
    489   free(line);
    490   if (ferror(input)) {
    491     perror("mpd: reading input");
    492     return false;
    493   }
    494   return true;
    495 }
    496 
    497 FILE *mt_open_input(const char *path, bool *must_close) {
    498   if (path == NULL || strcmp(path, "-") == 0) {
    499     *must_close = false;
    500     return stdin;
    501   }
    502 
    503   FILE *input = fopen(path, "r");
    504   if (input == NULL)
    505     perror(path);
    506   *must_close = input != NULL;
    507   return input;
    508 }
    509 
    510 static bool finish_list(struct mpd_connection *connection,
    511                         const char *context) {
    512   if (!mpd_command_list_end(connection) || !mpd_response_finish(connection)) {
    513     mt_report_error(connection, context);
    514     return false;
    515   }
    516   return true;
    517 }
    518 
    519 bool mt_add_queue(struct mpd_connection *connection,
    520                   const struct mt_strvec *tracks, bool verbose) {
    521   for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) {
    522     size_t end = start + MPD_TOOLS_BATCH_SIZE;
    523     if (end > tracks->len)
    524       end = tracks->len;
    525     if (!mpd_command_list_begin(connection, false)) {
    526       mt_report_error(connection, "begin add command list");
    527       return false;
    528     }
    529     for (size_t i = start; i < end; i++) {
    530       if (!mpd_send_add(connection, tracks->items[i])) {
    531         mt_report_error(connection, "queue add");
    532         return false;
    533       }
    534     }
    535     if (!finish_list(connection, "add tracks")) {
    536       fprintf(stderr, "mpd: failed input batch %zu..%zu\n", start + 1U, end);
    537       return false;
    538     }
    539     if (verbose) {
    540       for (size_t i = start; i < end; i++)
    541         printf("Added: %s\n", tracks->items[i]);
    542     }
    543   }
    544   return true;
    545 }
    546 
    547 bool mt_insert_queue(struct mpd_connection *connection,
    548                      const struct mt_strvec *tracks, unsigned position,
    549                      bool verbose) {
    550   size_t available = (size_t)UINT_MAX - (size_t)position + 1U;
    551   if (tracks->len > available) {
    552     fprintf(stderr, "mpd: too many tracks for the requested queue position\n");
    553     return false;
    554   }
    555   size_t inserted = 0;
    556   while (inserted < tracks->len) {
    557     size_t end = inserted + MPD_TOOLS_BATCH_SIZE;
    558     if (end > tracks->len)
    559       end = tracks->len;
    560     if (!mpd_command_list_begin(connection, false)) {
    561       mt_report_error(connection, "begin insert command list");
    562       return false;
    563     }
    564     for (size_t i = inserted; i < end; i++) {
    565       if (i - inserted > UINT_MAX - position ||
    566           !mpd_send_add_id_to(connection, tracks->items[i],
    567                               position + (unsigned)(i - inserted))) {
    568         mt_report_error(connection, "queue insert");
    569         return false;
    570       }
    571     }
    572     if (!finish_list(connection, "insert tracks"))
    573       return false;
    574     if (end < tracks->len)
    575       position += (unsigned)(end - inserted);
    576     if (verbose) {
    577       for (size_t i = inserted; i < end; i++)
    578         printf("Inserted: %s\n", tracks->items[i]);
    579     }
    580     inserted = end;
    581   }
    582   return true;
    583 }
    584 
    585 bool mt_add_playlist(struct mpd_connection *connection, const char *playlist,
    586                      const struct mt_strvec *tracks, bool verbose) {
    587   for (size_t start = 0; start < tracks->len; start += MPD_TOOLS_BATCH_SIZE) {
    588     size_t end = start + MPD_TOOLS_BATCH_SIZE;
    589     if (end > tracks->len)
    590       end = tracks->len;
    591     if (!mpd_command_list_begin(connection, false)) {
    592       mt_report_error(connection, "begin playlist command list");
    593       return false;
    594     }
    595     for (size_t i = start; i < end; i++) {
    596       if (!mpd_send_playlist_add(connection, playlist, tracks->items[i])) {
    597         mt_report_error(connection, "playlist add");
    598         return false;
    599       }
    600     }
    601     if (!finish_list(connection, "add playlist tracks"))
    602       return false;
    603     if (verbose) {
    604       for (size_t i = start; i < end; i++)
    605         printf("Added to %s: %s\n", playlist, tracks->items[i]);
    606     }
    607   }
    608   return true;
    609 }
    610 
    611 static bool add_stream(struct mpd_connection *connection, const char *playlist,
    612                        FILE *input, bool verbose, size_t *count) {
    613   struct mt_strvec batch;
    614   mt_strvec_init(&batch);
    615   char *line = NULL;
    616   size_t capacity = 0U;
    617   bool ok = true;
    618   *count = 0U;
    619 
    620   while (ok) {
    621     ssize_t length = getline(&line, &capacity, input);
    622     if (length < 0)
    623       break;
    624     while (length > 0 && (line[(size_t)length - 1U] == '\n' ||
    625                           line[(size_t)length - 1U] == '\r'))
    626       line[--length] = '\0';
    627     if (length == 0 || line[0] == '#')
    628       continue;
    629     if (!mt_strvec_push(&batch, line)) {
    630       perror("mpd: storing input line");
    631       ok = false;
    632     }
    633     if (ok && batch.len == MPD_TOOLS_BATCH_SIZE) {
    634       ok = playlist == NULL
    635                ? mt_add_queue(connection, &batch, verbose)
    636                : mt_add_playlist(connection, playlist, &batch, verbose);
    637       if (ok)
    638         *count += batch.len;
    639       mt_strvec_clear(&batch);
    640       mt_strvec_init(&batch);
    641     }
    642   }
    643   if (ok && ferror(input)) {
    644     perror("mpd: reading input");
    645     ok = false;
    646   }
    647   if (ok && batch.len > 0U) {
    648     ok = playlist == NULL
    649              ? mt_add_queue(connection, &batch, verbose)
    650              : mt_add_playlist(connection, playlist, &batch, verbose);
    651     if (ok)
    652       *count += batch.len;
    653   }
    654   free(line);
    655   mt_strvec_clear(&batch);
    656   return ok;
    657 }
    658 
    659 bool mt_add_queue_stream(struct mpd_connection *connection, FILE *input,
    660                          bool verbose, size_t *count) {
    661   return add_stream(connection, NULL, input, verbose, count);
    662 }
    663 
    664 bool mt_add_playlist_stream(struct mpd_connection *connection,
    665                             const char *playlist, FILE *input, bool verbose,
    666                             size_t *count) {
    667   return add_stream(connection, playlist, input, verbose, count);
    668 }
    669 
    670 void mt_player_state_clear(struct mt_player_state *state) {
    671   free(state->current_uri);
    672   memset(state, 0, sizeof(*state));
    673   state->song_pos = UINT_MAX;
    674   state->song_id = UINT_MAX;
    675 }
    676 
    677 bool mt_get_player_state(struct mpd_connection *connection,
    678                          struct mt_player_state *state) {
    679   for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) {
    680     memset(state, 0, sizeof(*state));
    681     state->song_pos = UINT_MAX;
    682     state->song_id = UINT_MAX;
    683 
    684     struct mpd_status *status = mpd_run_status(connection);
    685     if (status == NULL) {
    686       mt_report_error(connection, "get status");
    687       return false;
    688     }
    689     state->state = mpd_status_get_state(status);
    690     state->queue_version = mpd_status_get_queue_version(status);
    691     state->queue_length = mpd_status_get_queue_length(status);
    692     int song_pos = mpd_status_get_song_pos(status);
    693     int song_id = mpd_status_get_song_id(status);
    694     if (song_pos >= 0)
    695       state->song_pos = (unsigned)song_pos;
    696     if (song_id >= 0)
    697       state->song_id = (unsigned)song_id;
    698     mpd_status_free(status);
    699 
    700     if (state->song_id == UINT_MAX)
    701       return true;
    702 
    703     struct mpd_song *song = mpd_run_current_song(connection);
    704     if (song == NULL) {
    705       if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
    706         mt_report_error(connection, "get current song");
    707         return false;
    708       }
    709       continue;
    710     }
    711     bool consistent = mpd_song_get_id(song) == state->song_id &&
    712                       mpd_song_get_pos(song) == state->song_pos;
    713     const char *uri = mpd_song_get_uri(song);
    714     bool has_uri = uri != NULL;
    715     if (consistent && has_uri)
    716       state->current_uri = strdup(uri);
    717     mpd_song_free(song);
    718     if (consistent && has_uri && state->current_uri == NULL) {
    719       perror("mpd: current song");
    720       return false;
    721     }
    722     if (consistent)
    723       return true;
    724   }
    725   fprintf(stderr, "mpd: player changed repeatedly while reading its state\n");
    726   return false;
    727 }
    728 
    729 static bool receive_song_uris(struct mpd_connection *connection,
    730                               struct mt_strvec *tracks) {
    731   struct mpd_entity *entity;
    732   while ((entity = mpd_recv_entity(connection)) != NULL) {
    733     if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) {
    734       const struct mpd_song *song = mpd_entity_get_song(entity);
    735       const char *uri = mpd_song_get_uri(song);
    736       if (uri != NULL && !mt_strvec_push(tracks, uri)) {
    737         mpd_entity_free(entity);
    738         perror("mpd: storing queue");
    739         return false;
    740       }
    741     }
    742     mpd_entity_free(entity);
    743   }
    744   if (!mpd_response_finish(connection)) {
    745     mt_report_error(connection, "receive song list");
    746     return false;
    747   }
    748   return true;
    749 }
    750 
    751 bool mt_read_queue(struct mpd_connection *connection,
    752                    struct mt_strvec *tracks) {
    753   if (!mpd_send_list_queue_meta(connection)) {
    754     mt_report_error(connection, "list queue");
    755     return false;
    756   }
    757   return receive_song_uris(connection, tracks);
    758 }
    759 
    760 bool mt_read_playlist(struct mpd_connection *connection, const char *playlist,
    761                       struct mt_strvec *tracks) {
    762   if (!mpd_send_list_playlist(connection, playlist)) {
    763     mt_report_error(connection, "list stored playlist");
    764     return false;
    765   }
    766   return receive_song_uris(connection, tracks);
    767 }
    768 
    769 enum mt_state_match mt_player_state_is(struct mpd_connection *connection,
    770                                        unsigned expected_version,
    771                                        unsigned expected_song_id) {
    772   struct mpd_status *status = mpd_run_status(connection);
    773   if (status == NULL) {
    774     mt_report_error(connection, "check player state");
    775     return MT_STATE_ERROR;
    776   }
    777   int song_id = mpd_status_get_song_id(status);
    778   bool song_matches =
    779       expected_song_id == UINT_MAX
    780           ? song_id < 0
    781           : song_id >= 0 && (unsigned)song_id == expected_song_id;
    782   bool matches =
    783       mpd_status_get_queue_version(status) == expected_version && song_matches;
    784   mpd_status_free(status);
    785   return matches ? MT_STATE_MATCH : MT_STATE_CHANGED;
    786 }
    787 
    788 static size_t first_uri(const struct mt_strvec *tracks, const char *uri) {
    789   if (uri == NULL)
    790     return SIZE_MAX;
    791   for (size_t i = 0; i < tracks->len; i++) {
    792     if (strcmp(tracks->items[i], uri) == 0)
    793       return i;
    794   }
    795   return SIZE_MAX;
    796 }
    797 
    798 static bool add_queue_skipping(struct mpd_connection *connection,
    799                                const struct mt_strvec *tracks, size_t skip) {
    800   size_t index = 0U;
    801   while (index < tracks->len) {
    802     while (index < tracks->len && index == skip)
    803       index++;
    804     if (index == tracks->len)
    805       break;
    806 
    807     if (!mpd_command_list_begin(connection, false)) {
    808       mt_report_error(connection, "begin replacement add list");
    809       return false;
    810     }
    811     size_t sent = 0U;
    812     while (index < tracks->len && sent < MPD_TOOLS_BATCH_SIZE) {
    813       if (index != skip) {
    814         if (!mpd_send_add(connection, tracks->items[index])) {
    815           mt_report_error(connection, "add replacement track");
    816           return false;
    817         }
    818         sent++;
    819       }
    820       index++;
    821     }
    822     if (!finish_list(connection, "add replacement tracks"))
    823       return false;
    824   }
    825   return true;
    826 }
    827 
    828 static bool restore_queue_snapshot(const struct mt_strvec *tracks,
    829                                    const struct mt_player_state *state) {
    830   struct mpd_connection *connection = mt_connect();
    831   if (connection == NULL)
    832     return false;
    833   bool ok = mpd_run_clear(connection);
    834   if (!ok)
    835     mt_report_error(connection, "rollback: clear partial queue");
    836   if (ok)
    837     ok = mt_add_queue(connection, tracks, false);
    838   if (ok && tracks->len > 0U && state->song_pos < tracks->len &&
    839       (state->state == MPD_STATE_PLAY || state->state == MPD_STATE_PAUSE)) {
    840     ok = mpd_run_play_pos(connection, state->song_pos);
    841     if (!ok) {
    842       mt_report_error(connection, "rollback: restore playback position");
    843     } else if (state->state == MPD_STATE_PAUSE) {
    844       ok = mpd_run_pause(connection, true);
    845       if (!ok)
    846         mt_report_error(connection, "rollback: restore paused state");
    847     }
    848   }
    849   mpd_connection_free(connection);
    850   return ok;
    851 }
    852 
    853 bool mt_replace_queue(struct mpd_connection *connection,
    854                       const struct mt_strvec *tracks, unsigned expected_version,
    855                       bool force) {
    856   struct mt_player_state state;
    857   struct mt_strvec original;
    858   mt_strvec_init(&original);
    859   bool snapshot_ready = false;
    860   bool state_changed = false;
    861   for (unsigned attempt = 0U; attempt < PLAYER_SNAPSHOT_RETRIES; attempt++) {
    862     if (!mt_get_player_state(connection, &state))
    863       break;
    864     if (!force && expected_version != UINT_MAX &&
    865         state.queue_version != expected_version) {
    866       fprintf(stderr, "mpd: queue changed while it was being edited; use "
    867                       "--force to replace it anyway\n");
    868       mt_player_state_clear(&state);
    869       break;
    870     }
    871     if (!mt_read_queue(connection, &original)) {
    872       mt_player_state_clear(&state);
    873       break;
    874     }
    875     enum mt_state_match match =
    876         mt_player_state_is(connection, state.queue_version, state.song_id);
    877     if (match == MT_STATE_MATCH) {
    878       snapshot_ready = true;
    879       break;
    880     }
    881     if (match == MT_STATE_CHANGED)
    882       state_changed = true;
    883     mt_player_state_clear(&state);
    884     mt_strvec_clear(&original);
    885     mt_strvec_init(&original);
    886     if (match == MT_STATE_ERROR)
    887       break;
    888   }
    889   if (!snapshot_ready) {
    890     if (state_changed)
    891       fprintf(stderr, "mpd: player changed repeatedly before replacement\n");
    892     mt_strvec_clear(&original);
    893     return false;
    894   }
    895 
    896   size_t keep = first_uri(tracks, state.current_uri);
    897   bool can_keep = keep != SIZE_MAX && state.song_pos != UINT_MAX &&
    898                   state.song_pos < state.queue_length;
    899   bool ok = true;
    900   bool mutation_started = false;
    901 
    902   if (can_keep) {
    903     bool needs_trim =
    904         state.song_pos > 0U || state.song_pos + 1U < state.queue_length;
    905     if (needs_trim) {
    906       mutation_started = true;
    907       if (!mpd_command_list_begin(connection, false)) {
    908         mt_report_error(connection, "begin queue trim");
    909         ok = false;
    910       }
    911       if (ok && state.song_pos + 1U < state.queue_length &&
    912           !mpd_send_delete_range(connection, state.song_pos + 1U,
    913                                  state.queue_length)) {
    914         mt_report_error(connection, "delete queue suffix");
    915         ok = false;
    916       }
    917       if (ok && state.song_pos > 0U &&
    918           !mpd_send_delete_range(connection, 0U, state.song_pos)) {
    919         mt_report_error(connection, "delete queue prefix");
    920         ok = false;
    921       }
    922       if (ok)
    923         ok = finish_list(connection, "trim queue around current song");
    924     }
    925     if (ok) {
    926       mutation_started = true;
    927       ok = add_queue_skipping(connection, tracks, keep);
    928     }
    929     if (ok && keep > 0U && !mpd_run_move(connection, 0U, (unsigned)keep)) {
    930       mt_report_error(connection, "restore current queue position");
    931       ok = false;
    932     }
    933   } else {
    934     mutation_started = true;
    935     if (!mpd_run_clear(connection)) {
    936       mt_report_error(connection, "clear queue");
    937       ok = false;
    938     }
    939     if (ok)
    940       ok = mt_add_queue(connection, tracks, false);
    941     if (ok && tracks->len > 0U &&
    942         (state.state == MPD_STATE_PLAY || state.state == MPD_STATE_PAUSE)) {
    943       if (!mpd_run_play_pos(connection, 0U)) {
    944         mt_report_error(connection, "restart playback");
    945         ok = false;
    946       } else if (state.state == MPD_STATE_PAUSE &&
    947                  !mpd_run_pause(connection, true)) {
    948         mt_report_error(connection, "restore paused state");
    949         ok = false;
    950       }
    951     }
    952   }
    953 
    954   if (!ok && mutation_started) {
    955     fprintf(stderr, "mpd: replacement failed; restoring the previous queue\n");
    956     if (!restore_queue_snapshot(&original, &state))
    957       fprintf(stderr, "mpd: rollback failed; the queue may be partial\n");
    958   }
    959 
    960   mt_player_state_clear(&state);
    961   mt_strvec_clear(&original);
    962   return ok;
    963 }
    964 
    965 static bool same_tracks(const struct mt_strvec *left,
    966                         const struct mt_strvec *right) {
    967   if (left->len != right->len)
    968     return false;
    969   for (size_t i = 0U; i < left->len; i++) {
    970     if (strcmp(left->items[i], right->items[i]) != 0)
    971       return false;
    972   }
    973   return true;
    974 }
    975 
    976 static unsigned long playlist_name_sequence;
    977 
    978 static void temporary_playlist_name(char *buffer, size_t size,
    979                                     const char *purpose) {
    980   playlist_name_sequence++;
    981   struct timespec now = {0};
    982   if (clock_gettime(CLOCK_REALTIME, &now) != 0)
    983     now.tv_nsec = (long)playlist_name_sequence;
    984   snprintf(buffer, size, "mpd-tools-%ld-%lld-%ld-%lu-%s", (long)getpid(),
    985            (long long)now.tv_sec, now.tv_nsec, playlist_name_sequence, purpose);
    986 }
    987 
    988 static void remove_temporary_playlist(const char *playlist) {
    989   struct mpd_connection *cleanup = mt_connect();
    990   if (cleanup == NULL)
    991     return;
    992   if (!mpd_run_rm(cleanup, playlist) &&
    993       mpd_connection_get_error(cleanup) != MPD_ERROR_SERVER)
    994     mt_report_error(cleanup, "remove temporary playlist");
    995   mpd_connection_free(cleanup);
    996 }
    997 
    998 bool mt_replace_playlist(struct mpd_connection *connection,
    999                          const char *playlist, const struct mt_strvec *tracks,
   1000                          const struct mt_strvec *expected, bool force) {
   1001   if (tracks->len == 0U) {
   1002     if (!force) {
   1003       struct mt_strvec current;
   1004       mt_strvec_init(&current);
   1005       bool read_ok = mt_read_playlist(connection, playlist, &current);
   1006       bool unchanged = read_ok && same_tracks(expected, &current);
   1007       mt_strvec_clear(&current);
   1008       if (!read_ok)
   1009         return false;
   1010       if (!unchanged) {
   1011         fprintf(stderr, "mpd: stored playlist changed while it was being "
   1012                         "edited; use --force to replace it anyway\n");
   1013         return false;
   1014       }
   1015     }
   1016     if (!mpd_run_playlist_clear(connection, playlist)) {
   1017       mt_report_error(connection, "clear stored playlist");
   1018       return false;
   1019     }
   1020     return true;
   1021   }
   1022 
   1023   char staged[128U];
   1024   char backup[128U];
   1025   temporary_playlist_name(staged, sizeof(staged), "new");
   1026   temporary_playlist_name(backup, sizeof(backup), "old");
   1027   if (!mt_add_playlist(connection, staged, tracks, false)) {
   1028     remove_temporary_playlist(staged);
   1029     return false;
   1030   }
   1031 
   1032   if (!force) {
   1033     struct mt_strvec current;
   1034     mt_strvec_init(&current);
   1035     bool read_ok = mt_read_playlist(connection, playlist, &current);
   1036     bool unchanged = read_ok && same_tracks(expected, &current);
   1037     mt_strvec_clear(&current);
   1038     if (!read_ok) {
   1039       remove_temporary_playlist(staged);
   1040       return false;
   1041     }
   1042     if (!unchanged) {
   1043       fprintf(stderr, "mpd: stored playlist changed while it was being "
   1044                       "edited; use --force to replace it anyway\n");
   1045       remove_temporary_playlist(staged);
   1046       return false;
   1047     }
   1048   }
   1049 
   1050   if (!mpd_run_rename(connection, playlist, backup)) {
   1051     mt_report_error(connection, "move old stored playlist aside");
   1052     remove_temporary_playlist(staged);
   1053     return false;
   1054   }
   1055   if (!mpd_run_rename(connection, staged, playlist)) {
   1056     mt_report_error(connection, "install staged stored playlist");
   1057     struct mpd_connection *rollback = mt_connect();
   1058     if (rollback == NULL || !mpd_run_rename(rollback, backup, playlist))
   1059       fprintf(stderr,
   1060               "mpd: playlist rollback failed; '%s' contains the "
   1061               "backup\n",
   1062               backup);
   1063     if (rollback != NULL)
   1064       mpd_connection_free(rollback);
   1065     remove_temporary_playlist(staged);
   1066     return false;
   1067   }
   1068   if (!mpd_run_rm(connection, backup)) {
   1069     mt_report_error(connection, "remove stored playlist backup");
   1070     fprintf(stderr, "mpd: replacement succeeded, but backup '%s' remains\n",
   1071             backup);
   1072     return false;
   1073   }
   1074   return true;
   1075 }
   1076 
   1077 static bool run_editor(const char *path) {
   1078   const char *editor = getenv("VISUAL");
   1079   if (editor == NULL || *editor == '\0')
   1080     editor = getenv("EDITOR");
   1081   if (editor == NULL || *editor == '\0')
   1082     editor = "vi";
   1083 
   1084   wordexp_t words;
   1085   int result = wordexp(editor, &words, WRDE_NOCMD);
   1086   if (result != 0 || words.we_wordc == 0U) {
   1087     fprintf(stderr, "mpd: invalid editor command\n");
   1088     if (result == 0)
   1089       wordfree(&words);
   1090     return false;
   1091   }
   1092 
   1093   char **args = calloc(words.we_wordc + 2U, sizeof(*args));
   1094   if (args == NULL) {
   1095     perror("mpd: editor arguments");
   1096     wordfree(&words);
   1097     return false;
   1098   }
   1099   for (size_t i = 0; i < words.we_wordc; i++)
   1100     args[i] = words.we_wordv[i];
   1101   args[words.we_wordc] = (char *)path;
   1102 
   1103   pid_t pid = fork();
   1104   if (pid == 0) {
   1105     execvp(args[0], args);
   1106     perror(args[0]);
   1107     _exit(127);
   1108   }
   1109   if (pid < 0) {
   1110     perror("mpd: fork editor");
   1111     free(args);
   1112     wordfree(&words);
   1113     return false;
   1114   }
   1115 
   1116   int status;
   1117   while (waitpid(pid, &status, 0) < 0) {
   1118     if (errno != EINTR) {
   1119       perror("mpd: wait for editor");
   1120       free(args);
   1121       wordfree(&words);
   1122       return false;
   1123     }
   1124   }
   1125   free(args);
   1126   wordfree(&words);
   1127   if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
   1128     fprintf(stderr, "mpd: editor exited unsuccessfully\n");
   1129     return false;
   1130   }
   1131   return true;
   1132 }
   1133 
   1134 static char *editor_file_template(void) {
   1135   const char *directory = getenv("TMPDIR");
   1136   if (directory == NULL || *directory == '\0')
   1137     directory = "/tmp";
   1138   size_t directory_length = strlen(directory);
   1139   bool needs_slash = directory[directory_length - 1U] != '/';
   1140   const char suffix[] = "mpd_tools_XXXXXX";
   1141   if (directory_length > SIZE_MAX - sizeof(suffix) - (size_t)needs_slash) {
   1142     errno = ENAMETOOLONG;
   1143     return NULL;
   1144   }
   1145   size_t length = directory_length + (size_t)needs_slash + sizeof(suffix);
   1146   char *path = malloc(length);
   1147   if (path == NULL)
   1148     return NULL;
   1149   snprintf(path, length, "%s%s%s", directory, needs_slash ? "/" : "", suffix);
   1150   return path;
   1151 }
   1152 
   1153 bool mt_edit_lines(struct mt_strvec *lines) {
   1154   char *path = editor_file_template();
   1155   if (path == NULL) {
   1156     perror("mpd: create editor file path");
   1157     return false;
   1158   }
   1159   int fd = mkstemp(path);
   1160   if (fd < 0) {
   1161     perror("mpd: create editor file");
   1162     free(path);
   1163     return false;
   1164   }
   1165   FILE *file = fdopen(fd, "w");
   1166   if (file == NULL) {
   1167     perror("mpd: open editor file");
   1168     close(fd);
   1169     unlink(path);
   1170     free(path);
   1171     return false;
   1172   }
   1173   bool ok = true;
   1174   for (size_t i = 0; i < lines->len; i++) {
   1175     if (fprintf(file, "%s\n", lines->items[i]) < 0) {
   1176       perror("mpd: write editor file");
   1177       ok = false;
   1178       break;
   1179     }
   1180   }
   1181   if (fclose(file) != 0) {
   1182     perror("mpd: close editor file");
   1183     ok = false;
   1184   }
   1185 
   1186   if (ok)
   1187     ok = run_editor(path);
   1188   if (ok) {
   1189     int read_fd = open(path, O_RDONLY | O_NOFOLLOW);
   1190     if (read_fd < 0) {
   1191       perror("mpd: reopen editor file");
   1192       ok = false;
   1193     } else {
   1194       file = fdopen(read_fd, "r");
   1195       if (file == NULL) {
   1196         perror("mpd: read editor file");
   1197         close(read_fd);
   1198         ok = false;
   1199       } else {
   1200         struct mt_strvec edited;
   1201         mt_strvec_init(&edited);
   1202         ok = mt_read_lines(file, &edited);
   1203         if (fclose(file) != 0) {
   1204           perror("mpd: close edited file");
   1205           ok = false;
   1206         }
   1207         if (ok) {
   1208           mt_strvec_clear(lines);
   1209           *lines = edited;
   1210         } else {
   1211           mt_strvec_clear(&edited);
   1212         }
   1213       }
   1214     }
   1215   }
   1216 
   1217   if (unlink(path) != 0)
   1218     perror("mpd: remove editor file");
   1219   free(path);
   1220   return ok;
   1221 }
   1222 
   1223 bool mt_parse_size(const char *text, size_t *value) {
   1224   if (text == NULL || *text == '\0' || *text == '-')
   1225     return false;
   1226   errno = 0;
   1227   char *end;
   1228   unsigned long long parsed = strtoull(text, &end, 10);
   1229   if (errno != 0 || *end != '\0' || parsed > SIZE_MAX)
   1230     return false;
   1231   *value = (size_t)parsed;
   1232   return true;
   1233 }