unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

props_acl.c (23017B)


      1 /* Unison file synchronizer: src/props_acl.c */
      2 /* Copyright 2020-2022, Tõivo Leedjärv
      3 
      4     This program is free software: you can redistribute it and/or modify
      5     it under the terms of the GNU General Public License as published by
      6     the Free Software Foundation, either version 3 of the License, or
      7     (at your option) any later version.
      8 
      9     This program is distributed in the hope that it will be useful,
     10     but WITHOUT ANY WARRANTY; without even the implied warranty of
     11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12     GNU General Public License for more details.
     13 
     14     You should have received a copy of the GNU General Public License
     15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17 
     18 /* Supporting POSIX draft ACLs is not a goal, but may incidentally work
     19  * on some platforms. Only NFSv4 ACLs and Windows ACLs are intended to be
     20  * supported.
     21  *
     22  * On Solarish, both NFSv4 ACLs and POSIX draft ACLs are supported.
     23  * There is even support for cross-synchronizing between NFSv4 and
     24  * POSIX draft ACLs, but this support is currently disabled in props.ml
     25  * by checking if the resulting ACL matches the requested ACL (the check
     26  * fails with cross-synchronization).
     27  *
     28  * On FreeBSD and NetBSD, NFSv4 ACLs are supported. There is only limited
     29  * support for synchronizing POSIX draft ACLs (no default ACLs).
     30  *
     31  * On Darwin, extended ACLs are supported.
     32  *
     33  * On Windows, NTFS ACLs are supported via SDDL format. Only explicit
     34  * ACEs are synchronized, ignoring inherited ACEs completely. Users and
     35  * groups are represented as SID strings in SDDL, not as names.
     36  */
     37 
     38 /* The external interface is defined as follows. Every supported platform
     39  * must implement this interface. ACL format can be platform-specific,
     40  * which will prevent cross-platform synchronization but still allows
     41  * synchronization within the platform.
     42  *
     43  *
     44  * SET the ACL
     45  * ===========
     46  * unit unison_acl_from_text(String path, String acl)
     47  *
     48  *   Set the requested ACL on the requested file or directory. The ACL
     49  *   must be in the same format as that returned by unison_acl_to_text().
     50  *   Empty string ACL means <no ACL> and results in removal of any
     51  *   existing ACL on the requested file or directory.
     52  *   Symbolic links are followed.
     53  *
     54  * Input parameters
     55  *   path - absolute path of a file or directory
     56  *   acl  - text representation of ACL to set on the path
     57  *
     58  * Return value
     59  *   No return value.
     60  *
     61  * Exceptions
     62  *   There are no mandatory exception conditions.
     63  *   Failure MAY voluntarily be raised for example when:
     64  *     Can't access file to set/remove ACL
     65  *     ACL not supported
     66  *     Error setting ACL
     67  *     Error removing ACL
     68  *     Error converting ACL from text
     69  *
     70  *
     71  * GET the ACL
     72  * ===========
     73  * String unison_acl_to_text(String path)
     74  *
     75  *   Get the current ACL on the requested file or directory. The ACL
     76  *   must be returned as a stable and deterministic text representation
     77  *   that meets the following criteria:
     78  *     - with multiple requests on the same file, the representation is
     79  *       always the same, unless the underlying ACL changes;
     80  *     - the same ACL on different files has the same representation.
     81  *   Symbolic links are followed.
     82  *
     83  * Input parameters
     84  *   path - absolute path of a file or directory
     85  *
     86  * Return value
     87  *   The text representation of the ACL;
     88  *   or the value of macro UNSN_ACL_EMPTY (or empty string "") meaning
     89  *     <no ACL> (or only trivial ACL)
     90  *   or the value of macro UNSN_ACL_NOT_SUPPORTED (currently "-1") if
     91  *     ACL is not supported on the requested path.
     92  *
     93  * Exceptions
     94  *   Failure MUST be raised when:
     95  *     Can't access file to get ACL
     96  *   Failure MAY voluntarily be raised for example when:
     97  *     Error getting ACL
     98  *     Error converting ACL to text
     99  *   If Failure is not raised on some error condition then an empty
    100  *   string "" MUST NOT be returned under any circumstances; return
    101  *   UNSN_ACL_NOT_SUPPORTED instead.
    102  *
    103  *
    104  * ===========
    105  * Definition of ACL format
    106  *
    107  * The format of ACL text representation is completely free as long as
    108  * following constraints are met:
    109  *   - output of unison_acl_to_text() can be used as
    110  *     input to unison_acl_from_text()
    111  *   - ACL synchronization is done only on the same platform.
    112  *
    113  * If ACLs must be synchronized between different platforms then the
    114  * currently used universal ACL format matches the definition from
    115  * illumos acl(5) man page [https://illumos.org/man/5/acl]. This applies
    116  * to both POSIX draft ACLs and NFSv4 ACLs. See the note on cross-platform
    117  * synchronization below.
    118  *
    119  * ACL is always in the form
    120  *
    121  *   acl_entry[,acl_entry]...
    122  *
    123  * Each acl_entry may be suffixed with a colon and userid/groupid.
    124  *
    125  * Examples:
    126  *
    127  *   POSIX draft ACL
    128  *
    129  *     user:tom:rw-,mask:rwx,group:staff:r-x:450
    130  *
    131  *   NFSv4 ACL
    132  *
    133  *        user:lp:rw------------:------I:allow:1300,
    134  *         owner@:--x-----------:------I:deny,
    135  *         owner@:rw-p---A-W-Co-:-------:allow,
    136  *     user:marks:r-------------:------I:deny:1270,
    137  *         group@:r-------------:-------:allow,
    138  *      everyone@:r-----a-R-c--s:-------:allow
    139  *
    140  *     (note that the example is folded, but it should actually be
    141  *     returned as one string line without newlines)
    142  *
    143  *
    144  * ===========
    145  * On cross-platform synchronization
    146  *
    147  * Currently there is no canonical ACL representation created specifically
    148  * for Unison. Existing platform APIs are used as much as possible, without
    149  * custom formatting and parsing.
    150  * A specific Unison ACL format could be truly common across platforms.
    151  *
    152  * If extended ACL synchronization capability is desired in the future then
    153  * it is only required to change the output of unison_acl_to_text() and the
    154  * input parsing in unison_acl_from_text().
    155  * The Unison archive format will not have to be changed as long as the
    156  * entire ACL and any eventual metadata is encoded within one string.
    157  * It is neither necessary to change the ACL code in props.ml.
    158  *
    159  * The issues of such cross-platform (e.g. between Windows and Unix-like)
    160  * synchronization lie not in the representation format, though. It is easy
    161  * enough to interpret the permission sets of NFSv4 and Windows ACLs in a
    162  * similar, equivalent way. It can be much more difficult to interpret the
    163  * subjects (users and groups) in a meaningful way. Purely for
    164  * synchronization, this can still work on some platforms, e.g. Solaris,
    165  * which allow the use of SIDs in ACL definition.
    166  */
    167 
    168 #include <caml/memory.h>
    169 #include <caml/alloc.h>
    170 #include <caml/mlvalues.h>
    171 #include <caml/fail.h>
    172 
    173 
    174 #if defined(sun) || defined(__sun)  /* Solarish, all illumos-based OS,   */
    175 #define __Solaris__                 /* OpenIndiana, OmniOS, SmartOS, ... */
    176 #endif
    177 
    178 /* Primitive check only, without explicitly checking for
    179  * POSIX or NFSv4. NFSv4-style ACLs are expected
    180  * but POSIX draft ACLs may work to some extent. */
    181 #undef UNSN_HAS_FS_ACL
    182 #if defined(__Solaris__) || defined(__FreeBSD__) || defined(__APPLE__)
    183 #define UNSN_HAS_FS_ACL
    184 #endif
    185 
    186 #if defined(__NetBSD__)
    187 #include <unistd.h>
    188 #if defined(_PC_ACL_NFS4)
    189 #define UNSN_HAS_FS_ACL
    190 #endif
    191 #endif
    192 
    193 #if defined(_WIN32)
    194 #define UNSN_HAS_FS_ACL
    195 #endif
    196 
    197 
    198 #define UNSN_ACL_NOT_SUPPORTED caml_copy_string("-1")
    199 
    200 
    201 #ifndef UNSN_HAS_FS_ACL
    202 
    203 CAMLprim value unison_acl_from_text(value path, value acl)
    204 {
    205   CAMLparam0();
    206   CAMLreturn(Val_unit);
    207 }
    208 
    209 CAMLprim value unison_acl_to_text(value path)
    210 {
    211   CAMLparam0();
    212   CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
    213 }
    214 
    215 #else
    216 
    217 
    218 #define UNSN_ACL_EMPTY caml_copy_string("")
    219 
    220 
    221 #if defined(_WIN32)
    222 
    223 /*#define ACL_DEBUG*/
    224 
    225 #ifndef UNICODE
    226 #define UNICODE
    227 #endif
    228 #ifndef _UNICODE
    229 #define _UNICODE
    230 #endif
    231 
    232 #include <windows.h>
    233 #include <aclapi.h>
    234 #include <sddl.h>
    235 #include <strsafe.h>
    236 
    237 #include <caml/version.h>
    238 #if OCAML_VERSION < 41300
    239 #define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */
    240 #endif
    241 #include <caml/osdeps.h>
    242 
    243 #ifdef ACL_DEBUG
    244 #include <stdio.h>
    245 #endif
    246 
    247 static void unsn_acl_fail(char *msg, DWORD err)
    248 {
    249   DWORD flags;
    250   char *sys_msg;
    251   DWORD sys_len;
    252   char fail_msg[160];
    253   const size_t LEN = sizeof(fail_msg) / sizeof(fail_msg[0]);
    254 
    255   flags =
    256     FORMAT_MESSAGE_ALLOCATE_BUFFER |
    257     FORMAT_MESSAGE_FROM_SYSTEM |
    258     FORMAT_MESSAGE_IGNORE_INSERTS;
    259 
    260   sys_len = FormatMessageA(flags, NULL, err, 0, (char *) &sys_msg, 0, NULL);
    261   if (!sys_len) {
    262     StringCbPrintfA(fail_msg, LEN, "%s (Windows error code: %d)", msg, err);
    263   } else {
    264     /* Assume last 3 characters are ".\r\n" (doesn't matter if they aren't),
    265      * and remove them. */
    266     if (sys_len > 3) {
    267       sys_msg[sys_len - 3] = '\0';
    268     }
    269 
    270     StringCbPrintfA(fail_msg, LEN,
    271       "%s (Windows error code: %d) %s", msg, err, sys_msg);
    272     LocalFree(sys_msg);
    273   }
    274 
    275   caml_failwith(fail_msg);
    276 }
    277 
    278 CAMLprim value unison_acl_from_text(value path, value acl)
    279 {
    280   CAMLparam2(path, acl);
    281   wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
    282   wchar_t *wacl = caml_stat_strdup_to_utf16(String_val(acl));
    283   PCWSTR acl_text;
    284   PSECURITY_DESCRIPTOR sd;
    285   SECURITY_DESCRIPTOR_CONTROL sdc;
    286   DWORD sdc_rev;
    287   PSID owner = NULL, group = NULL;
    288   PACL DACL;
    289   BOOL DACLpresent = FALSE, isDef;
    290   BOOL ok = TRUE;
    291   SECURITY_INFORMATION si = 0;
    292   DWORD res;
    293 
    294 #ifdef ACL_DEBUG
    295   printf_s(" ===> Setting ACL for |%ls|\n", wpath);
    296   printf_s(" ---> Input ACL value   |%ls|\n", wacl);
    297 #endif
    298 
    299   if (wcslen(wacl) == 0) {
    300     acl_text = L"D:"; /* SDDL representation of empty ACL */
    301   } else {
    302     acl_text = wacl;
    303   }
    304 #ifdef ACL_DEBUG
    305   printf_s(" ---> Setting ACL value |%ls|\n", acl_text);
    306 #endif
    307 
    308   if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(acl_text,
    309          SDDL_REVISION_1, &sd, NULL)) {
    310     caml_stat_free(wpath);
    311     caml_stat_free(wacl);
    312     unsn_acl_fail("Error converting ACL from text", GetLastError());
    313   }
    314 
    315   caml_stat_free(wacl);
    316 
    317   ok = ok && GetSecurityDescriptorDacl(sd, &DACLpresent, &DACL, &isDef);
    318   ok = ok && GetSecurityDescriptorControl(sd, &sdc, &sdc_rev);
    319 
    320   if (!ok || !DACLpresent) {
    321     LocalFree(sd);
    322     caml_stat_free(wpath);
    323 
    324     caml_failwith("Error converting ACL from text (no ACL info present?)");
    325   }
    326 
    327   si |= DACL_SECURITY_INFORMATION;
    328 
    329   if (sdc & SE_DACL_PROTECTED) {
    330     si |= PROTECTED_DACL_SECURITY_INFORMATION;
    331   } else {
    332     si |= UNPROTECTED_DACL_SECURITY_INFORMATION;
    333   }
    334 
    335   res = SetNamedSecurityInfoW(wpath, SE_FILE_OBJECT,
    336           si, owner, group, DACL, NULL);
    337 
    338   LocalFree(sd);
    339   caml_stat_free(wpath);
    340 
    341   if (res == ERROR_ACCESS_DENIED) {
    342     caml_failwith("Error setting ACL: access denied. The process may require "
    343       "Administrator or \"Restore files\" privileges to set the ACL");
    344   }
    345 
    346   if (res != ERROR_SUCCESS) {
    347     unsn_acl_fail("Error setting ACL", res);
    348   }
    349 
    350   CAMLreturn(Val_unit);
    351 }
    352 
    353 CAMLprim value unison_acl_to_text(value path)
    354 {
    355   CAMLparam1(path);
    356   CAMLlocal1(result);
    357   wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
    358   int i, aceCnt;
    359   PWSTR acl_text;
    360   PSECURITY_DESCRIPTOR sd;
    361   SECURITY_DESCRIPTOR_CONTROL sdc;
    362   DWORD sdc_rev;
    363   PSID owner, group;
    364   PACL DACL;
    365   ACL_SIZE_INFORMATION aclInfo;
    366   PVOID ace;
    367   SECURITY_INFORMATION si = DACL_SECURITY_INFORMATION;
    368   DWORD res1, err;
    369   BOOL res2;
    370 
    371 #ifdef ACL_DEBUG
    372   printf_s(" ===> Getting ACL for %ls\n", wpath);
    373 #endif
    374 
    375   res1 = GetNamedSecurityInfoW(wpath, SE_FILE_OBJECT, si,
    376            &owner, &group, &DACL, NULL, &sd);
    377   caml_stat_free(wpath);
    378 
    379   if (res1 != ERROR_SUCCESS || sd == NULL) {
    380     unsn_acl_fail("Error getting ACL", res1);
    381   }
    382 
    383 #ifdef ACL_DEBUG
    384   res2 = ConvertSecurityDescriptorToStringSecurityDescriptorW(sd,
    385            SDDL_REVISION_1, si, &acl_text, NULL);
    386 
    387   if (acl_text != NULL) {
    388     printf_s(" ---> Initial ACL text representation: %ls\n", acl_text);
    389 
    390     LocalFree(acl_text);
    391   }
    392 #endif /* ACL_DEBUG */
    393 
    394   if (DACL == NULL) {
    395     LocalFree(sd);
    396 
    397 #ifdef ACL_DEBUG
    398     printf_s(" ---> ACL not supported\n");
    399 #endif
    400     CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
    401   }
    402 
    403   if (!GetAclInformation(DACL, &aclInfo, sizeof(aclInfo), AclSizeInformation)) {
    404     LocalFree(sd);
    405     unsn_acl_fail("Error getting ACL information", GetLastError());
    406   }
    407   aceCnt = aclInfo.AceCount;
    408 
    409   /* Remove all inherited ACEs -- those cannot be restored in the other
    410    * replica, they are inherited from the parent directory. */
    411   for (i = aclInfo.AceCount - 1; i >= 0; i--) {
    412     if (!GetAce(DACL, i, &ace)) {
    413 #ifdef ACL_DEBUG
    414       printf_s("GetAce failed (Windows error code %d)\n", GetLastError());
    415 #endif
    416     } else if (((PACE_HEADER) ace)->AceFlags & INHERITED_ACE) {
    417       if (!DeleteAce(DACL, i)) {
    418 #ifdef ACL_DEBUG
    419         printf_s("DeleteAce failed (Windows error code %d)\n", GetLastError());
    420 #endif
    421       } else {
    422         aceCnt--;
    423       }
    424     }
    425   }
    426 
    427   /* Even when individual inherited ACEs have been removed, the entire ACL
    428    * may have been marked as AUTO_INHERITED. Remove this flag to make
    429    * synchronization paranoid checks more reliable. It is unknown if it
    430    * can cause synchronization failures, but it doesn't matter - inherited
    431    * ACLs can't be propagated in any case. */
    432   if (!SetSecurityDescriptorControl(sd, SE_DACL_AUTO_INHERITED, 0)) {
    433 #ifdef ACL_DEBUG
    434     unsn_acl_fail("Error in ACL control information", GetLastError());
    435 #endif
    436   }
    437 
    438   if (aceCnt == 0) { /* No explicit entries */
    439     if (!GetSecurityDescriptorControl(sd, &sdc, &sdc_rev)) {
    440       LocalFree(sd);
    441       unsn_acl_fail("Error getting ACL control information", GetLastError());
    442     }
    443 
    444     if (!(sdc & SE_DACL_PROTECTED)) { /* No control flags we care about */
    445       LocalFree(sd);
    446 
    447 #ifdef ACL_DEBUG
    448       printf_s(" ---> Empty ACL (no explicit ACE, may have inherited ACE)\n");
    449 #endif
    450       CAMLreturn(UNSN_ACL_EMPTY); /* Empty ACL (or only inherited) */
    451     }
    452   }
    453 
    454   res2 = ConvertSecurityDescriptorToStringSecurityDescriptor(sd,
    455            SDDL_REVISION_1, si, &acl_text, NULL);
    456   err = GetLastError();
    457 
    458   LocalFree(sd);
    459 
    460   if (!res2 || (acl_text == NULL)) {
    461     unsn_acl_fail("Error converting ACL to text", err);
    462   }
    463 
    464 #ifdef ACL_DEBUG
    465   printf_s(" ---> Final ACL text representation:   %ls\n", acl_text);
    466 #endif
    467 
    468   result = caml_copy_string_of_utf16(acl_text);
    469 
    470   LocalFree(acl_text);
    471 
    472   CAMLreturn(result);
    473 }
    474 
    475 
    476 #else /* defined(_WIN32) */
    477 
    478 
    479 #if defined(__Solaris__) || defined(__APPLE__)
    480 #include <fcntl.h>
    481 #include <sys/stat.h>
    482 #endif
    483 
    484 #if defined(__Solaris__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    485 #include <errno.h>
    486 #include <stdio.h>
    487 #include <string.h>
    488 #include <sys/types.h>
    489 #include <sys/acl.h>
    490 #endif
    491 
    492 #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    493 #include <unistd.h>
    494 #endif
    495 
    496 #if defined(__APPLE__)
    497 #define UNSN_ACL_T acl_t
    498 #else
    499 #define UNSN_ACL_T acl_t *
    500 #endif
    501 
    502 
    503 static void unsn_acl_fail(const char *fmtmsg)
    504 {
    505   char errmsg[255];
    506 
    507   int errnum = errno;
    508   if (strerror_r(errnum, errmsg, sizeof(errmsg)) != 0) {
    509     snprintf(errmsg, sizeof(errmsg), "(error code %d)", errnum);
    510   }
    511 
    512   caml_failwith_value(caml_alloc_sprintf(fmtmsg, errmsg));
    513 }
    514 
    515 #if defined(__FreeBSD__) || defined(__NetBSD__)
    516 static acl_type_t unsn_path_acl_type(const char *path)
    517 {
    518   if (pathconf(path, _PC_ACL_NFS4) > 0) { /* NFSv4 ACL supported */
    519     return ACL_TYPE_NFS4;
    520   } else if (pathconf(path, _PC_ACL_EXTENDED) > 0) { /* POSIX draft ACL */
    521     return ACL_TYPE_ACCESS; /* It is not possible to get or set
    522                                default and access ACL at the same time,
    523                                so fall back to access ACL only. */
    524   } else { /* ACLs not supported */
    525     return -1;
    526   }
    527 }
    528 #elif defined(__APPLE__)
    529 static acl_type_t unsn_path_acl_type(const char *path)
    530 {
    531   return ACL_TYPE_EXTENDED;
    532 }
    533 #endif
    534 
    535 
    536 static void unsn_remove_acl_os(const char *path)
    537 {
    538 #if defined(__Solaris__)
    539   struct stat st;
    540 
    541   if (stat(path, &st) != 0) {
    542     unsn_acl_fail("Can't access file to remove ACL: %s");
    543   }
    544 
    545   if (acl_strip(path, st.st_uid, st.st_gid, st.st_mode) != 0) {
    546     unsn_acl_fail("Error removing ACL: %s");
    547   }
    548 #elif defined(__FreeBSD__) || defined(__NetBSD__)
    549   /* FreeBSD has a acl_strip_np() function, but it would be
    550    * much too complicated in this code. */
    551   /* Don't even bother checking for target ACL type, just
    552    * try to remove all and ignore errors. */
    553   acl_delete_file_np(path, ACL_TYPE_DEFAULT);
    554   acl_delete_file_np(path, ACL_TYPE_ACCESS);
    555   acl_delete_file_np(path, ACL_TYPE_NFS4);
    556 #elif defined(__APPLE__)
    557   acl_set_file(path, unsn_path_acl_type(path), acl_from_text("!#acl 1"));
    558 #endif
    559 }
    560 
    561 
    562 /************************************
    563  *         Set ACL from text
    564  ************************************/
    565 static _Bool unsn_acl_from_text_os(const char *acl_text, UNSN_ACL_T *aclp)
    566 {
    567 #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    568   *aclp = acl_from_text(acl_text);
    569 
    570   return (*aclp != NULL);
    571 #elif defined(__Solaris__)
    572   int error = acl_fromtext(acl_text, aclp);
    573 
    574   return (error == 0 && aclp != NULL);
    575 #endif
    576 }
    577 
    578 CAMLprim value unison_acl_from_text(value path, value acl)
    579 {
    580   CAMLparam2(path, acl);
    581   const char *acl_text = String_val(acl);
    582   const char *name = String_val(path);
    583   UNSN_ACL_T aclp = NULL;
    584   int error;
    585 
    586 #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    587   acl_type_t type = unsn_path_acl_type(name);
    588   if (type == -1) {
    589     caml_failwith("ACL not supported on this path");
    590   }
    591 #endif
    592 
    593   /* Check if ACL must be removed */
    594   if (*acl_text == '\0') {
    595     unsn_remove_acl_os(name);
    596     CAMLreturn(Val_unit);
    597   }
    598 
    599   if (!unsn_acl_from_text_os(acl_text, &aclp)) {
    600     caml_failwith("Error converting ACL from text");
    601   }
    602 
    603 #if defined(__Solaris__)
    604   error = acl_set(name, aclp);
    605 #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    606   error = acl_set_file(name, type, aclp);
    607 #endif
    608   int real_err = errno;
    609   acl_free(aclp);
    610   errno = real_err;
    611 
    612   if (error == -1) {
    613     unsn_acl_fail("Error setting ACL: %s");
    614   }
    615 
    616   CAMLreturn(Val_unit);
    617 }
    618 
    619 
    620 /************************************
    621  *          Get ACL as text
    622  ************************************/
    623 /* This function does not allocate new,
    624  * it returns the pointer to its argument. */
    625 static char *postprocess_acl_os(char *s)
    626 {
    627 #if defined(__FreeBSD__) || defined(__NetBSD__)
    628   char *p;
    629   char *buf = s;  /* Just an alias; modify input string in place */
    630   int perms = 0, comment = 0, offs = 0;
    631 
    632   for (p = s; *p; p++) {
    633     switch (*p) {
    634       case ',' :
    635           perms = 0;
    636           break;
    637       case '#' :
    638           /* FreeBSD acl_to_text embeds the #effective permissions,
    639            * which are actually not part of the ACL. */
    640           comment = 1;
    641           break;
    642       case '@' :
    643       case ':' :
    644           if (!comment) {
    645             perms++;
    646           }
    647           break;
    648       case 'D' :
    649           /* Swap the position of d and D permissions.
    650            * Synchronization works even without swapping, but the different
    651            * ordering will show up as constant synchronization difference. */
    652           if (perms == 2) {
    653             if (buf[offs - 1] != 'd' && *(p + 1) == 'd') {
    654               *p = 'd';
    655               *(p + 1) = 'D';
    656             } else if (buf[offs - 1] != 'd' && *(p + 1) == '-') {
    657               *p = '-';
    658               *(p + 1) = 'D';
    659             }
    660             perms = 0; /* prevent further swapping */
    661           }
    662           break;
    663       case 'd' :
    664           if (perms == 2) {
    665             if (buf[offs - 1] == '-' && *(p + 1) != 'D' && *(p + 1) != '\0') {
    666               buf[offs - 1] = 'd';
    667               *p = '-';
    668             } else if (buf[offs - 1] != 'd' && *(p + 1) == '-') {
    669               *p = '-';
    670               *(p + 1) = 'D';
    671             }
    672             perms = 0; /* prevent further swapping */
    673           }
    674           break;
    675       case '\n' :
    676           /* Replace newlines with commas...
    677            * ... except if it's the last one. */
    678           if (*(p + 1) != '\0') {
    679             *p = ',';
    680           } else {
    681             *p = ' ';
    682           }
    683           perms = 0;
    684           comment = 0;
    685           break;
    686     }
    687 
    688     /* Remove all whitespace and comments. */
    689     if (*p != ' ' && *p != '\t' && !comment) {
    690       buf[offs++] = *p;
    691     }
    692   }
    693   buf[offs] = '\0';
    694 
    695   return buf;
    696 #elif defined(__APPLE__)
    697   /* Remove trailing newline */
    698   size_t last = strlen(s) - 1;
    699   if (last >= 0 && s[last] == '\n') {
    700     s[last] = '\0';
    701   }
    702 
    703   return s;
    704 #endif
    705 }
    706 
    707 static char *unsn_acl_to_text_os(UNSN_ACL_T aclp)
    708 {
    709 #if defined(__FreeBSD__) || defined(__NetBSD__)
    710   return postprocess_acl_os(acl_to_text_np(aclp, NULL, ACL_TEXT_APPEND_ID));
    711 #elif defined(__APPLE__)
    712   return postprocess_acl_os(acl_to_text(aclp, NULL));
    713 #elif defined(__Solaris__)
    714   return acl_totext(aclp, ACL_APPEND_ID | ACL_COMPACT_FMT | ACL_SID_FMT);
    715 #endif
    716 }
    717 
    718 static _Bool unsn_acl_is_empty_or_trivial_os(UNSN_ACL_T aclp)
    719 {
    720 #if defined(__FreeBSD__) || defined(__NetBSD__)
    721   int is_trivial = 0;
    722 
    723   acl_is_trivial_np(aclp, &is_trivial); /* Ignore any errors here */
    724 
    725   return (is_trivial || aclp == NULL);
    726 #elif defined(__APPLE__) || defined(__Solaris__)
    727   return (aclp == NULL);
    728 #endif
    729 }
    730 
    731 static int unsn_get_acl_os(const char *path, UNSN_ACL_T *aclp)
    732 {
    733 #if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
    734   acl_type_t type = unsn_path_acl_type(path);
    735   if (type == -1) {
    736     errno = EOPNOTSUPP;
    737     return -1;
    738   }
    739 
    740   errno = 0;
    741   *aclp = acl_get_file(path, type);
    742 
    743 #if defined(__APPLE__)
    744   if (errno != 0) {
    745     /* ACLs are always enabled on Darwin since version 10 (2009).
    746      * Unfortunately, Darwin sets errno to ENOENT also when the file
    747      * does not have an extended ACL. Since it is impossible to distinguish
    748      * from the real ENOENT (due to the path), we must check with stat(). */
    749     if (errno == ENOENT) {
    750       struct stat st;
    751       errno = 0;
    752       stat(path, &st);
    753 
    754       if (errno != ENOENT) {
    755         /* The path does not trigger ENOENT;
    756          * this means that there is no extended ACL. This is allowed. */
    757         return 0;
    758       }
    759 
    760       errno = ENOENT; /* Ignore errno from stat(), restore original errno. */
    761     }
    762 
    763     return -1;
    764   }
    765 #elif defined(__FreeBSD__) || defined(__NetBSD__)
    766   if (*aclp == NULL) {
    767     return -1;
    768   }
    769 #endif
    770 
    771   return 0;
    772 #endif  /* FreeBSD or NetBSD or Darwin */
    773 
    774 #if defined(__Solaris__)
    775   return acl_get(path, ACL_NO_TRIVIAL, aclp);
    776 #endif
    777 }
    778 
    779 CAMLprim value unison_acl_to_text(value path)
    780 {
    781   CAMLparam1(path);
    782   CAMLlocal1(result);
    783   UNSN_ACL_T aclp = NULL;
    784   char *acltxt;
    785 
    786   int err = unsn_get_acl_os(String_val(path), &aclp);
    787   if (err == -1) {
    788     if (errno == ENOSYS || errno == EOPNOTSUPP) {
    789       CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
    790     } else {
    791       unsn_acl_fail("Error getting ACL: %s");
    792     }
    793   }
    794 
    795   /* If there was no error but aclp is NULL then it means an empty
    796    * or trivial ACL (that is, just the mode), which is allowed. */
    797   if (aclp == NULL || unsn_acl_is_empty_or_trivial_os(aclp)) {
    798     if (aclp != NULL) {
    799       acl_free(aclp);
    800     }
    801 
    802     CAMLreturn(UNSN_ACL_EMPTY);
    803   }
    804 
    805   acltxt = unsn_acl_to_text_os(aclp);
    806   acl_free(aclp);
    807 
    808   if (acltxt == NULL) {
    809     caml_failwith("Error converting ACL to text");
    810   }
    811 
    812   result = caml_copy_string(acltxt);
    813   free(acltxt);
    814 
    815   CAMLreturn(result);
    816 }
    817 
    818 #endif /* !defined(_WIN32) */
    819 
    820 
    821 #endif /* UNSN_HAS_FS_ACL */