QNX: enhance config reading (#938)

QNX prefers config read from confstr(_CS_RESOLVE, ...) and
confstr(_CS_DOMAIN, ...) rather than those read from /etc/resolv.conf
and friends which are only used as fallbacks. Its documented that
confstr() reads from /etc/net.cfg, but that file format is NOT
documented so we can't read directly from there ... however, we can
monitor that file for changes to know when the system config has been
updated.

This has been validated to successfully build, but has not yet had the
tests run on an actual QNX platform. Build results here:

https://github.com/bradh352/c-ares/actions/runs/12324751945/job/34402863134

Signed-off-by: Brad House (@bradh352)
diff --git a/.github/workflows/qnx.yml b/.github/workflows/qnx.yml
index e8533da..ac44237 100644
--- a/.github/workflows/qnx.yml
+++ b/.github/workflows/qnx.yml
@@ -3,7 +3,6 @@
 name: QNX
 on:
   push:
-  pull_request:
 
 concurrency:
   group: ${{ github.ref }}-qnx
@@ -68,6 +67,14 @@
           source "${{ github.workspace }}/qnx800/qnxsdp-env.sh"
           echo "QNX_TARGET=${QNX_TARGET}" >> $GITHUB_ENV
           echo "CMAKE_FIND_ROOT_PATH=${QNX_TARGET};${QNX_TARGET}/${CPUVARDIR}" >> $GITHUB_ENV
+      - name: "CMake: build c-ares"
+        env:
+          BUILD_TYPE: CMAKE
+          CMAKE_FLAGS: "-DCMAKE_BUILD_TYPE=DEBUG -DCARES_STATIC=ON -DCARES_STATIC_PIC=ON -DCARES_BUILD_TESTS=ON -G Ninja -DCMAKE_SYSTEM_NAME=QNX -DCMAKE_FIND_ROOT_PATH=${{ env.CMAKE_FIND_ROOT_PATH }} -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY"
+        run: |
+          source "${{ github.workspace }}/qnx800/qnxsdp-env.sh"
+          cd c-ares
+          ./ci/build.sh
       - name: "Autotools: build c-ares"
         env:
           BUILD_TYPE: autotools
@@ -77,13 +84,5 @@
           source "${{ github.workspace }}/qnx800/qnxsdp-env.sh"
           cd c-ares
           ./ci/build.sh
-      - name: "CMake: build c-ares"
-        env:
-          BUILD_TYPE: CMAKE
-          CMAKE_FLAGS: "-DCMAKE_BUILD_TYPE=DEBUG -DCARES_STATIC=ON -DCARES_STATIC_PIC=ON -DCARES_BUILD_TESTS=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_SYSTEM_NAME=QNX -DCMAKE_FIND_ROOT_PATH=${{ env.CMAKE_FIND_ROOT_PATH }} -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY"
-        run: |
-          source "${{ github.workspace }}/qnx800/qnxsdp-env.sh"
-          cd c-ares
-          ./ci/build.sh
 
 
diff --git a/configure.ac b/configure.ac
index f3ec1b8..260880b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -247,13 +247,9 @@
     [AM_CFLAGS], [-Werror])
 fi
 
-dnl Android requires c99, QNX requires gnu90, all others should use c90
+dnl Android and QNX require c99, all others should use c90
 case $host_os in
-  *qnx*)
-    AX_APPEND_COMPILE_FLAGS([-std=gnu90], [AM_CFLAGS], [-Werror])
-    AX_APPEND_COMPILE_FLAGS([-D_QNX_SOURCE], [AM_CPPFLAGS], [-Werror])
-    ;;
-  *android*)
+  *qnx*|*android*)
     AX_APPEND_COMPILE_FLAGS([-std=c99], [AM_CFLAGS], [-Werror])
     ;;
   *)
@@ -261,6 +257,13 @@
     ;;
 esac
 
+dnl QNX needs -D_QNX_SOURCE
+case $host_os in
+  *qnx*)
+    AX_APPEND_COMPILE_FLAGS([-D_QNX_SOURCE], [AM_CPPFLAGS], [-Werror])
+  ;;
+esac
+
 if test "$ax_cv_c_compiler_vendor" = "intel"; then
   AX_APPEND_COMPILE_FLAGS([-shared-intel], [AM_CFLAGS])
 fi
diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
index ce8c3f2..e6d44e8 100644
--- a/src/lib/ares_private.h
+++ b/src/lib/ares_private.h
@@ -388,8 +388,23 @@
 
 ares_status_t ares_init_by_environment(ares_sysconfig_t *sysconfig);
 
+
+typedef ares_status_t (*ares_sysconfig_line_cb_t)(const ares_channel_t *channel,
+                                                  ares_sysconfig_t     *sysconfig,
+                                                  ares_buf_t           *line);
+
+ares_status_t ares_sysconfig_parse_resolv_line(const ares_channel_t *channel,
+                                               ares_sysconfig_t     *sysconfig,
+                                               ares_buf_t           *line);
+
+ares_status_t ares_sysconfig_process_buf(const ares_channel_t    *channel,
+                                         ares_sysconfig_t        *sysconfig,
+                                         ares_buf_t              *buf,
+                                         ares_sysconfig_line_cb_t cb);
+
 ares_status_t ares_init_sysconfig_files(const ares_channel_t *channel,
-                                        ares_sysconfig_t     *sysconfig);
+                                        ares_sysconfig_t     *sysconfig,
+                                        ares_bool_t process_resolvconf);
 #ifdef __APPLE__
 ares_status_t ares_init_sysconfig_macos(const ares_channel_t *channel,
                                         ares_sysconfig_t     *sysconfig);
diff --git a/src/lib/ares_sysconfig.c b/src/lib/ares_sysconfig.c
index 9f0d7e5..286db60 100644
--- a/src/lib/ares_sysconfig.c
+++ b/src/lib/ares_sysconfig.c
@@ -260,6 +260,94 @@
 }
 #endif
 
+#if defined(__QNX__)
+static ares_status_t
+  ares_init_sysconfig_qnx(const ares_channel_t *channel,
+                          ares_sysconfig_t     *sysconfig)
+{
+  /* QNX:
+   *   1. use confstr(_CS_RESOLVE, ...) as primary resolv.conf data, replacing
+   *      "_" with " ".  If that is empty, then do normal /etc/resolv.conf
+   *      processing.
+   *   2. We want to process /etc/nsswitch.conf as normal.
+   *   3. if confstr(_CS_DOMAIN, ...) this is the domain name.  Use this as
+   *      preference over anything else found.
+   */
+  ares_buf_t    *buf                = ares_buf_create();
+  unsigned char *data               = NULL;
+  size_t         data_size          = 0;
+  ares_bool_t    process_resolvconf = ARES_TRUE;
+  ares_status_t  status             = ARES_SUCCESS;
+
+  /* Prefer confstr(_CS_RESOLVE, ...) */
+  buf = ares_buf_create();
+  if (buf == NULL) {
+    status = ARES_ENOMEM;
+    goto done;
+  }
+
+  data_size = 1024;
+  data      = ares_buf_append_start(buf, &data_size);
+  if (data == NULL) {
+    status = ARES_ENOMEM;
+    goto done;
+  }
+
+  data_size = confstr(_CS_RESOLVE, (char *)data, data_size);
+  if (data_size > 1) {
+    /* confstr returns byte for NULL terminator, strip */
+    data_size--;
+
+    ares_buf_append_finish(buf, data_size);
+    /* Its odd, this uses _ instead of " " between keywords, otherwise the
+     * format is the same as resolv.conf, replace. */
+    ares_buf_replace(buf, (const unsigned char *)"_", 1,
+                     (const unsigned char *)" ", 1);
+
+    status = ares_sysconfig_process_buf(channel, sysconfig, buf,
+                                        ares_sysconfig_parse_resolv_line);
+    if (status != ARES_SUCCESS) {
+      /* ENOMEM is really the only error we'll get here */
+      goto done;
+    }
+
+    /* don't read resolv.conf if we processed *any* nameservers */
+    if (ares_llist_len(sysconfig->sconfig) != 0) {
+      process_resolvconf = ARES_FALSE;
+    }
+  }
+
+  /* Process files */
+  status = ares_init_sysconfig_files(channel, sysconfig, process_resolvconf);
+  if (status != ARES_SUCCESS) {
+    goto done;
+  }
+
+  /* Read confstr(_CS_DOMAIN, ...), but if we had a search path specified with
+   * more than one domain, lets prefer that instead.  Its not exactly clear
+   * the best way to handle this. */
+  if (sysconfig->ndomains <= 1) {
+    char   domain[256];
+    size_t domain_len;
+
+    domain_len = confstr(_CS_DOMAIN, domain, sizeof(domain_len));
+    if (domain_len != 0) {
+      ares_strsplit_free(sysconfig->domains, sysconfig->ndomains);
+      sysconfig->domains = ares_strsplit(domain, ", ", &sysconfig->ndomains);
+      if (sysconfig->domains == NULL) {
+        status = ARES_ENOMEM;
+        goto done;
+      }
+    }
+  }
+
+done:
+  ares_buf_destroy(buf);
+
+  return status;
+}
+#endif
+
 #if defined(CARES_USE_LIBRESOLV)
 static ares_status_t
   ares_init_sysconfig_libresolv(const ares_channel_t *channel,
@@ -516,8 +604,10 @@
   status = ares_init_sysconfig_macos(channel, &sysconfig);
 #elif defined(CARES_USE_LIBRESOLV)
   status = ares_init_sysconfig_libresolv(channel, &sysconfig);
+#elif defined(__QNX__)
+  status = ares_init_sysconfig_qnx(channel, &sysconfig);
 #else
-  status = ares_init_sysconfig_files(channel, &sysconfig);
+  status = ares_init_sysconfig_files(channel, &sysconfig, ARES_TRUE);
 #endif
 
   if (status != ARES_SUCCESS) {
diff --git a/src/lib/ares_sysconfig_files.c b/src/lib/ares_sysconfig_files.c
index 49bc330..a6c2a8e 100644
--- a/src/lib/ares_sysconfig_files.c
+++ b/src/lib/ares_sysconfig_files.c
@@ -549,9 +549,9 @@
 /* This function will only return ARES_SUCCESS or ARES_ENOMEM.  Any other
  * conditions are ignored.  Users may mess up config files, but we want to
  * process anything we can. */
-static ares_status_t parse_resolvconf_line(const ares_channel_t *channel,
-                                           ares_sysconfig_t     *sysconfig,
-                                           ares_buf_t           *line)
+ares_status_t ares_sysconfig_parse_resolv_line(const ares_channel_t *channel,
+                                               ares_sysconfig_t     *sysconfig,
+                                               ares_buf_t           *line)
 {
   char          option[32];
   char          value[512];
@@ -726,38 +726,16 @@
   return status;
 }
 
-typedef ares_status_t (*line_callback_t)(const ares_channel_t *channel,
-                                         ares_sysconfig_t     *sysconfig,
-                                         ares_buf_t           *line);
 
-/* Should only return:
- *  ARES_ENOTFOUND - file not found
- *  ARES_EFILE     - error reading file (perms)
- *  ARES_ENOMEM    - out of memory
- *  ARES_SUCCESS   - file processed, doesn't necessarily mean it was a good
- *                   file, but we're not erroring out if we can't parse
- *                   something (or anything at all) */
-static ares_status_t process_config_lines(const ares_channel_t *channel,
-                                          const char           *filename,
-                                          ares_sysconfig_t     *sysconfig,
-                                          line_callback_t       cb)
+ares_status_t ares_sysconfig_process_buf(const ares_channel_t    *channel,
+                                         ares_sysconfig_t        *sysconfig,
+                                         ares_buf_t              *buf,
+                                         ares_sysconfig_line_cb_t cb)
 {
-  ares_status_t status = ARES_SUCCESS;
   ares_array_t *lines  = NULL;
-  ares_buf_t   *buf    = NULL;
   size_t        num;
   size_t        i;
-
-  buf = ares_buf_create();
-  if (buf == NULL) {
-    status = ARES_ENOMEM;
-    goto done;
-  }
-
-  status = ares_buf_load_file(filename, buf);
-  if (status != ARES_SUCCESS) {
-    goto done;
-  }
+  ares_status_t status;
 
   status = ares_buf_split(buf, (const unsigned char *)"\n", 1,
                           ARES_BUF_SPLIT_TRIM, 0, &lines);
@@ -777,25 +755,60 @@
   }
 
 done:
-  ares_buf_destroy(buf);
   ares_array_destroy(lines);
+  return status;
+}
+
+/* Should only return:
+ *  ARES_ENOTFOUND - file not found
+ *  ARES_EFILE     - error reading file (perms)
+ *  ARES_ENOMEM    - out of memory
+ *  ARES_SUCCESS   - file processed, doesn't necessarily mean it was a good
+ *                   file, but we're not erroring out if we can't parse
+ *                   something (or anything at all) */
+static ares_status_t process_config_lines(const ares_channel_t    *channel,
+                                          const char              *filename,
+                                          ares_sysconfig_t        *sysconfig,
+                                          ares_sysconfig_line_cb_t cb)
+{
+  ares_status_t status = ARES_SUCCESS;
+  ares_buf_t   *buf    = NULL;
+
+  buf = ares_buf_create();
+  if (buf == NULL) {
+    status = ARES_ENOMEM;
+    goto done;
+  }
+
+  status = ares_buf_load_file(filename, buf);
+  if (status != ARES_SUCCESS) {
+    goto done;
+  }
+
+  status = ares_sysconfig_process_buf(channel, sysconfig, buf, cb);
+
+done:
+  ares_buf_destroy(buf);
 
   return status;
 }
 
 ares_status_t ares_init_sysconfig_files(const ares_channel_t *channel,
-                                        ares_sysconfig_t     *sysconfig)
+                                        ares_sysconfig_t     *sysconfig,
+                                        ares_bool_t process_resolvconf)
 {
   ares_status_t status = ARES_SUCCESS;
 
   /* Resolv.conf */
-  status = process_config_lines(channel,
-                                (channel->resolvconf_path != NULL)
-                                  ? channel->resolvconf_path
-                                  : PATH_RESOLV_CONF,
-                                sysconfig, parse_resolvconf_line);
-  if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) {
-    goto done;
+  if (process_resolvconf) {
+    status = process_config_lines(channel,
+                                  (channel->resolvconf_path != NULL)
+                                    ? channel->resolvconf_path
+                                    : PATH_RESOLV_CONF,
+                                  sysconfig, ares_sysconfig_parse_resolv_line);
+    if (status != ARES_SUCCESS && status != ARES_ENOTFOUND) {
+      goto done;
+    }
   }
 
   /* Nsswitch.conf */
diff --git a/src/lib/event/ares_event_configchg.c b/src/lib/event/ares_event_configchg.c
index e3e665b..5ecc688 100644
--- a/src/lib/event/ares_event_configchg.c
+++ b/src/lib/event/ares_event_configchg.c
@@ -558,14 +558,24 @@
                                          const char          *resolvconf_path)
 {
   size_t      i;
-  const char *configfiles[5];
+  const char *configfiles[16];
   ares_bool_t changed = ARES_FALSE;
+  size_t      cnt = 0;
 
-  configfiles[0] = resolvconf_path;
-  configfiles[1] = "/etc/nsswitch.conf";
-  configfiles[2] = "/etc/netsvc.conf";
-  configfiles[3] = "/etc/svc.conf";
-  configfiles[4] = NULL;
+  memset(configfiles, 0, sizeof(configfiles));
+
+  configfiles[cnt++] = resolvconf_path;
+  configfiles[cnt++] = "/etc/nsswitch.conf";
+#ifdef _AIX
+  configfiles[cnt++] = "/etc/netsvc.conf";
+#endif
+#ifdef __osf /* Tru64 */
+  configfiles[cnt++] = "/etc/svc.conf";
+#endif
+#ifdef __QNX__
+  configfiles[cnt++] = "/etc/net.cfg";
+#endif
+  configfiles[cnt++] = NULL;
 
   for (i = 0; configfiles[i] != NULL; i++) {
     fileinfo_t *fi = ares_htable_strvp_get_direct(filestat, configfiles[i]);
diff --git a/src/lib/include/ares_buf.h b/src/lib/include/ares_buf.h
index 7836a31..10d29ea 100644
--- a/src/lib/include/ares_buf.h
+++ b/src/lib/include/ares_buf.h
@@ -219,6 +219,26 @@
  */
 CARES_EXTERN char          *ares_buf_finish_str(ares_buf_t *buf, size_t *len);
 
+/*! Replace the given search byte sequence with the replacement byte sequence.
+ *  This is only valid for allocated buffers, not const buffers.  Will replace
+ *  all byte sequences starting at the current offset to the end of the buffer.
+ *
+ *  \param[in]  buf       Initialized buffer object. Can not be a "const" buffer.
+ *  \param[in]  srch      Search byte sequence, must not be NULL.
+ *  \param[in]  srch_size Size of byte sequence, must not be zero.
+ *  \param[in]  rplc      Byte sequence to use as replacement.  May be NULL if
+ *                        rplc_size is zero.
+ *  \param[in]  rplc_size Size of replacement byte sequence, may be 0.
+ *  \return ARES_SUCCESS on success, otherwise on may return failure only on
+ *          memory allocation failure or misuse.  Will not return indication
+ *          if any replacements occurred
+ */
+CARES_EXTERN ares_status_t  ares_buf_replace(ares_buf_t *buf,
+                                             const unsigned char *srch,
+                                             size_t srch_size,
+                                             const unsigned char *rplc,
+                                             size_t rplc_size);
+
 /*! Tag a position to save in the buffer in case parsing needs to rollback,
  *  such as if insufficient data is available, but more data may be added in
  *  the future.  Only a single tag can be set per buffer object.  Setting a
diff --git a/src/lib/str/ares_buf.c b/src/lib/str/ares_buf.c
index 69e6b38..63acc6c 100644
--- a/src/lib/str/ares_buf.c
+++ b/src/lib/str/ares_buf.c
@@ -1104,6 +1104,72 @@
   return ares_buf_fetch(buf, len);
 }
 
+ares_status_t ares_buf_replace(ares_buf_t *buf, const unsigned char *srch,
+                               size_t srch_size, const unsigned char *rplc,
+                               size_t rplc_size)
+{
+  size_t        processed_len = 0;
+  ares_status_t status;
+
+  if (buf->alloc_buf == NULL || srch == NULL || srch_size == 0 ||
+      (rplc == NULL && rplc_size != 0)) {
+    return ARES_EFORMERR;
+  }
+
+  while (1) {
+    unsigned char *ptr           = buf->alloc_buf + buf->offset + processed_len;
+    size_t         remaining_len = buf->data_len - buf->offset - processed_len;
+    size_t         found_offset  = 0;
+    size_t         move_data_len;
+
+    /* Find pattern */
+    ptr = ares_memmem(ptr, remaining_len, srch, srch_size);
+    if (ptr == NULL) {
+      break;
+    }
+
+    /* Store the offset this was found because our actual pointer might be
+     * switched out from under us by the call to ensure_space() if the
+     * replacement pattern is larger than the search pattern */
+    found_offset   = (size_t)(ptr - (size_t)(buf->alloc_buf + buf->offset));
+    if (rplc_size > srch_size) {
+      status = ares_buf_ensure_space(buf, rplc_size - srch_size);
+      if (status != ARES_SUCCESS) {
+        return status;
+      }
+    }
+
+    /* Impossible, but silence clang */
+    if (buf->alloc_buf == NULL) {
+      return ARES_ENOMEM;
+    }
+
+    /* Recalculate actual pointer */
+    ptr = buf->alloc_buf + buf->offset + found_offset;
+
+    /* Move the data */
+    move_data_len = buf->data_len - buf->offset - found_offset - srch_size;
+    memmove(ptr + rplc_size,
+            ptr + srch_size,
+            move_data_len);
+
+    /* Copy in the replacement data */
+    if (rplc != NULL && rplc_size > 0) {
+      memcpy(ptr, rplc, rplc_size);
+    }
+
+    if (rplc_size > srch_size) {
+      buf->data_len += rplc_size - srch_size;
+    } else {
+      buf->data_len -= srch_size - rplc_size;
+    }
+
+    processed_len = found_offset + rplc_size;
+  }
+
+  return ARES_SUCCESS;
+}
+
 ares_status_t ares_buf_peek_byte(const ares_buf_t *buf, unsigned char *b)
 {
   size_t               remaining_len = 0;
diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
index b88329f..3e3760b 100644
--- a/test/ares-test-internal.cc
+++ b/test/ares-test-internal.cc
@@ -1516,6 +1516,34 @@
   ares_free_array(strs, nstrs, ares_free);
 }
 
+TEST_F(LibraryTest, BufReplace) {
+  ares_buf_t  *buf = NULL;
+  size_t       i;
+  struct {
+    const char *input;
+    const char *srch;
+    const char *rplc;
+    const char *output;
+  } tests[] = {
+    /* Same size */
+    { "nameserver_1.2.3.4\nnameserver_2.3.4.5\n", "_", " ", "nameserver 1.2.3.4\nnameserver 2.3.4.5\n" },
+    /* Longer */
+    { "nameserver_1.2.3.4\nnameserver_2.3.4.5\n", "_", "|||", "nameserver|||1.2.3.4\nnameserver|||2.3.4.5\n" },
+    /* Shorter */
+    { "nameserver_1.2.3.4\nnameserver_2.3.4.5\n", "_", "", "nameserver1.2.3.4\nnameserver2.3.4.5\n" }
+  };
+  char        *str = NULL;
+
+  for (i=0; i<sizeof(tests)/sizeof(*tests); i++) {
+    buf = ares_buf_create();
+    EXPECT_EQ(ARES_SUCCESS, ares_buf_append_str(buf, tests[i].input));
+    EXPECT_EQ(ARES_SUCCESS, ares_buf_replace(buf, (const unsigned char *)tests[i].srch, ares_strlen(tests[i].srch), (const unsigned char *)tests[i].rplc, ares_strlen(tests[i].rplc)));
+    str = ares_buf_finish_str(buf, NULL);
+    EXPECT_STREQ(str, tests[i].output);
+    ares_free(str);
+  }
+}
+
 typedef struct {
   ares_socket_t s;
 } test_htable_asvp_t;