Merge branch 'ma/unpack-trees-free-msgs'
[gitweb.git] / convert.c
index d7144201f82710170ec8d7a05690aa00af339768..64d0d30e08de4acd496bf955d9ce64afa0ff5b8b 100644 (file)
--- a/convert.c
+++ b/convert.c
@@ -7,6 +7,7 @@
 #include "sigchain.h"
 #include "pkt-line.h"
 #include "sub-process.h"
+#include "utf8.h"
 
 /*
  * convert.c - convert a file when checking it out and checking it in.
@@ -193,45 +194,54 @@ static enum eol output_eol(enum crlf_action crlf_action)
        return core_eol;
 }
 
-static void check_safe_crlf(const char *path, enum crlf_action crlf_action,
+static void check_global_conv_flags_eol(const char *path, enum crlf_action crlf_action,
                            struct text_stat *old_stats, struct text_stat *new_stats,
-                           enum safe_crlf checksafe)
+                           int conv_flags)
 {
        if (old_stats->crlf && !new_stats->crlf ) {
                /*
                 * CRLFs would not be restored by checkout
                 */
-               if (checksafe == SAFE_CRLF_WARN)
+               if (conv_flags & CONV_EOL_RNDTRP_DIE)
+                       die(_("CRLF would be replaced by LF in %s."), path);
+               else if (conv_flags & CONV_EOL_RNDTRP_WARN)
                        warning(_("CRLF will be replaced by LF in %s.\n"
                                  "The file will have its original line"
                                  " endings in your working directory."), path);
-               else /* i.e. SAFE_CRLF_FAIL */
-                       die(_("CRLF would be replaced by LF in %s."), path);
        } else if (old_stats->lonelf && !new_stats->lonelf ) {
                /*
                 * CRLFs would be added by checkout
                 */
-               if (checksafe == SAFE_CRLF_WARN)
+               if (conv_flags & CONV_EOL_RNDTRP_DIE)
+                       die(_("LF would be replaced by CRLF in %s"), path);
+               else if (conv_flags & CONV_EOL_RNDTRP_WARN)
                        warning(_("LF will be replaced by CRLF in %s.\n"
                                  "The file will have its original line"
                                  " endings in your working directory."), path);
-               else /* i.e. SAFE_CRLF_FAIL */
-                       die(_("LF would be replaced by CRLF in %s"), path);
        }
 }
 
-static int has_cr_in_index(const struct index_state *istate, const char *path)
+static int has_crlf_in_index(const struct index_state *istate, const char *path)
 {
        unsigned long sz;
        void *data;
-       int has_cr;
+       const char *crp;
+       int has_crlf = 0;
 
        data = read_blob_data_from_index(istate, path, &sz);
        if (!data)
                return 0;
-       has_cr = memchr(data, '\r', sz) != NULL;
+
+       crp = memchr(data, '\r', sz);
+       if (crp) {
+               unsigned int ret_stats;
+               ret_stats = gather_convert_stats(data, sz);
+               if (!(ret_stats & CONVERT_STAT_BITS_BIN) &&
+                   (ret_stats & CONVERT_STAT_BITS_TXT_CRLF))
+                       has_crlf = 1;
+       }
        free(data);
-       return has_cr;
+       return has_crlf;
 }
 
 static int will_convert_lf_to_crlf(size_t len, struct text_stat *stats,
@@ -256,10 +266,245 @@ static int will_convert_lf_to_crlf(size_t len, struct text_stat *stats,
 
 }
 
+static int validate_encoding(const char *path, const char *enc,
+                     const char *data, size_t len, int die_on_error)
+{
+       /* We only check for UTF here as UTF?? can be an alias for UTF-?? */
+       if (istarts_with(enc, "UTF")) {
+               /*
+                * Check for detectable errors in UTF encodings
+                */
+               if (has_prohibited_utf_bom(enc, data, len)) {
+                       const char *error_msg = _(
+                               "BOM is prohibited in '%s' if encoded as %s");
+                       /*
+                        * This advice is shown for UTF-??BE and UTF-??LE encodings.
+                        * We cut off the last two characters of the encoding name
+                        * to generate the encoding name suitable for BOMs.
+                        */
+                       const char *advise_msg = _(
+                               "The file '%s' contains a byte order "
+                               "mark (BOM). Please use UTF-%s as "
+                               "working-tree-encoding.");
+                       const char *stripped = NULL;
+                       char *upper = xstrdup_toupper(enc);
+                       upper[strlen(upper)-2] = '\0';
+                       if (!skip_prefix(upper, "UTF-", &stripped))
+                               skip_prefix(stripped, "UTF", &stripped);
+                       advise(advise_msg, path, stripped);
+                       free(upper);
+                       if (die_on_error)
+                               die(error_msg, path, enc);
+                       else {
+                               return error(error_msg, path, enc);
+                       }
+
+               } else if (is_missing_required_utf_bom(enc, data, len)) {
+                       const char *error_msg = _(
+                               "BOM is required in '%s' if encoded as %s");
+                       const char *advise_msg = _(
+                               "The file '%s' is missing a byte order "
+                               "mark (BOM). Please use UTF-%sBE or UTF-%sLE "
+                               "(depending on the byte order) as "
+                               "working-tree-encoding.");
+                       const char *stripped = NULL;
+                       char *upper = xstrdup_toupper(enc);
+                       if (!skip_prefix(upper, "UTF-", &stripped))
+                               skip_prefix(stripped, "UTF", &stripped);
+                       advise(advise_msg, path, stripped, stripped);
+                       free(upper);
+                       if (die_on_error)
+                               die(error_msg, path, enc);
+                       else {
+                               return error(error_msg, path, enc);
+                       }
+               }
+
+       }
+       return 0;
+}
+
+static void trace_encoding(const char *context, const char *path,
+                          const char *encoding, const char *buf, size_t len)
+{
+       static struct trace_key coe = TRACE_KEY_INIT(WORKING_TREE_ENCODING);
+       struct strbuf trace = STRBUF_INIT;
+       int i;
+
+       strbuf_addf(&trace, "%s (%s, considered %s):\n", context, path, encoding);
+       for (i = 0; i < len && buf; ++i) {
+               strbuf_addf(
+                       &trace,"| \e[2m%2i:\e[0m %2x \e[2m%c\e[0m%c",
+                       i,
+                       (unsigned char) buf[i],
+                       (buf[i] > 32 && buf[i] < 127 ? buf[i] : ' '),
+                       ((i+1) % 8 && (i+1) < len ? ' ' : '\n')
+               );
+       }
+       strbuf_addchars(&trace, '\n', 1);
+
+       trace_strbuf(&coe, &trace);
+       strbuf_release(&trace);
+}
+
+static int check_roundtrip(const char *enc_name)
+{
+       /*
+        * check_roundtrip_encoding contains a string of comma and/or
+        * space separated encodings (eg. "UTF-16, ASCII, CP1125").
+        * Search for the given encoding in that string.
+        */
+       const char *found = strcasestr(check_roundtrip_encoding, enc_name);
+       const char *next;
+       int len;
+       if (!found)
+               return 0;
+       next = found + strlen(enc_name);
+       len = strlen(check_roundtrip_encoding);
+       return (found && (
+                       /*
+                        * check that the found encoding is at the
+                        * beginning of check_roundtrip_encoding or
+                        * that it is prefixed with a space or comma
+                        */
+                       found == check_roundtrip_encoding || (
+                               (isspace(found[-1]) || found[-1] == ',')
+                       )
+               ) && (
+                       /*
+                        * check that the found encoding is at the
+                        * end of check_roundtrip_encoding or
+                        * that it is suffixed with a space or comma
+                        */
+                       next == check_roundtrip_encoding + len || (
+                               next < check_roundtrip_encoding + len &&
+                               (isspace(next[0]) || next[0] == ',')
+                       )
+               ));
+}
+
+static const char *default_encoding = "UTF-8";
+
+static int encode_to_git(const char *path, const char *src, size_t src_len,
+                        struct strbuf *buf, const char *enc, int conv_flags)
+{
+       char *dst;
+       int dst_len;
+       int die_on_error = conv_flags & CONV_WRITE_OBJECT;
+
+       /*
+        * No encoding is specified or there is nothing to encode.
+        * Tell the caller that the content was not modified.
+        */
+       if (!enc || (src && !src_len))
+               return 0;
+
+       /*
+        * Looks like we got called from "would_convert_to_git()".
+        * This means Git wants to know if it would encode (= modify!)
+        * the content. Let's answer with "yes", since an encoding was
+        * specified.
+        */
+       if (!buf && !src)
+               return 1;
+
+       if (validate_encoding(path, enc, src, src_len, die_on_error))
+               return 0;
+
+       trace_encoding("source", path, enc, src, src_len);
+       dst = reencode_string_len(src, src_len, default_encoding, enc,
+                                 &dst_len);
+       if (!dst) {
+               /*
+                * We could add the blob "as-is" to Git. However, on checkout
+                * we would try to reencode to the original encoding. This
+                * would fail and we would leave the user with a messed-up
+                * working tree. Let's try to avoid this by screaming loud.
+                */
+               const char* msg = _("failed to encode '%s' from %s to %s");
+               if (die_on_error)
+                       die(msg, path, enc, default_encoding);
+               else {
+                       error(msg, path, enc, default_encoding);
+                       return 0;
+               }
+       }
+       trace_encoding("destination", path, default_encoding, dst, dst_len);
+
+       /*
+        * UTF supports lossless conversion round tripping [1] and conversions
+        * between UTF and other encodings are mostly round trip safe as
+        * Unicode aims to be a superset of all other character encodings.
+        * However, certain encodings (e.g. SHIFT-JIS) are known to have round
+        * trip issues [2]. Check the round trip conversion for all encodings
+        * listed in core.checkRoundtripEncoding.
+        *
+        * The round trip check is only performed if content is written to Git.
+        * This ensures that no information is lost during conversion to/from
+        * the internal UTF-8 representation.
+        *
+        * Please note, the code below is not tested because I was not able to
+        * generate a faulty round trip without an iconv error. Iconv errors
+        * are already caught above.
+        *
+        * [1] http://unicode.org/faq/utf_bom.html#gen2
+        * [2] https://support.microsoft.com/en-us/help/170559/prb-conversion-problem-between-shift-jis-and-unicode
+        */
+       if (die_on_error && check_roundtrip(enc)) {
+               char *re_src;
+               int re_src_len;
+
+               re_src = reencode_string_len(dst, dst_len,
+                                            enc, default_encoding,
+                                            &re_src_len);
+
+               trace_printf("Checking roundtrip encoding for %s...\n", enc);
+               trace_encoding("reencoded source", path, enc,
+                              re_src, re_src_len);
+
+               if (!re_src || src_len != re_src_len ||
+                   memcmp(src, re_src, src_len)) {
+                       const char* msg = _("encoding '%s' from %s to %s and "
+                                           "back is not the same");
+                       die(msg, path, enc, default_encoding);
+               }
+
+               free(re_src);
+       }
+
+       strbuf_attach(buf, dst, dst_len, dst_len + 1);
+       return 1;
+}
+
+static int encode_to_worktree(const char *path, const char *src, size_t src_len,
+                             struct strbuf *buf, const char *enc)
+{
+       char *dst;
+       int dst_len;
+
+       /*
+        * No encoding is specified or there is nothing to encode.
+        * Tell the caller that the content was not modified.
+        */
+       if (!enc || (src && !src_len))
+               return 0;
+
+       dst = reencode_string_len(src, src_len, enc, default_encoding,
+                                 &dst_len);
+       if (!dst) {
+               error("failed to encode '%s' from %s to %s",
+                       path, default_encoding, enc);
+               return 0;
+       }
+
+       strbuf_attach(buf, dst, dst_len, dst_len + 1);
+       return 1;
+}
+
 static int crlf_to_git(const struct index_state *istate,
                       const char *path, const char *src, size_t len,
                       struct strbuf *buf,
-                      enum crlf_action crlf_action, enum safe_crlf checksafe)
+                      enum crlf_action crlf_action, int conv_flags)
 {
        struct text_stat stats;
        char *dst;
@@ -289,12 +534,12 @@ static int crlf_to_git(const struct index_state *istate,
                 * unless we want to renormalize in a merge or
                 * cherry-pick.
                 */
-               if ((checksafe != SAFE_CRLF_RENORMALIZE) &&
-                   has_cr_in_index(istate, path))
+               if ((!(conv_flags & CONV_EOL_RENORMALIZE)) &&
+                   has_crlf_in_index(istate, path))
                        convert_crlf_into_lf = 0;
        }
-       if ((checksafe == SAFE_CRLF_WARN ||
-           (checksafe == SAFE_CRLF_FAIL)) && len) {
+       if (((conv_flags & CONV_EOL_RNDTRP_WARN) ||
+            ((conv_flags & CONV_EOL_RNDTRP_DIE) && len))) {
                struct text_stat new_stats;
                memcpy(&new_stats, &stats, sizeof(new_stats));
                /* simulate "git add" */
@@ -307,7 +552,7 @@ static int crlf_to_git(const struct index_state *istate,
                        new_stats.crlf += new_stats.lonelf;
                        new_stats.lonelf = 0;
                }
-               check_safe_crlf(path, crlf_action, &stats, &new_stats, checksafe);
+               check_global_conv_flags_eol(path, crlf_action, &stats, &new_stats, conv_flags);
        }
        if (!convert_crlf_into_lf)
                return 0;
@@ -423,8 +668,10 @@ static int filter_buffer_or_fd(int in, int out, void *data)
        child_process.in = -1;
        child_process.out = out;
 
-       if (start_command(&child_process))
+       if (start_command(&child_process)) {
+               strbuf_release(&cmd);
                return error("cannot fork to run external filter '%s'", params->cmd);
+       }
 
        sigchain_push(SIGPIPE, SIG_IGN);
 
@@ -564,8 +811,7 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len
 
        if (!subprocess_map_initialized) {
                subprocess_map_initialized = 1;
-               hashmap_init(&subprocess_map, (hashmap_cmp_fn) cmd2process_cmp,
-                            NULL, 0);
+               hashmap_init(&subprocess_map, cmd2process_cmp, NULL, 0);
                entry = NULL;
        } else {
                entry = (struct cmd2process *)subprocess_find_entry(&subprocess_map, cmd);
@@ -888,7 +1134,7 @@ static int ident_to_git(const char *path, const char *src, size_t len,
 static int ident_to_worktree(const char *path, const char *src, size_t len,
                              struct strbuf *buf, int ident)
 {
-       unsigned char sha1[20];
+       struct object_id oid;
        char *to_free = NULL, *dollar, *spc;
        int cnt;
 
@@ -902,9 +1148,9 @@ static int ident_to_worktree(const char *path, const char *src, size_t len,
        /* are we "faking" in place editing ? */
        if (src == buf->buf)
                to_free = strbuf_detach(buf, NULL);
-       hash_sha1_file(src, len, "blob", sha1);
+       hash_object_file(src, len, "blob", &oid);
 
-       strbuf_grow(buf, len + cnt * 43);
+       strbuf_grow(buf, len + cnt * (the_hash_algo->hexsz + 3));
        for (;;) {
                /* step 1: run to the next '$' */
                dollar = memchr(src, '$', len);
@@ -959,7 +1205,7 @@ static int ident_to_worktree(const char *path, const char *src, size_t len,
 
                /* step 4: substitute */
                strbuf_addstr(buf, "Id: ");
-               strbuf_add(buf, sha1_to_hex(sha1), 40);
+               strbuf_addstr(buf, oid_to_hex(&oid));
                strbuf_addstr(buf, " $");
        }
        strbuf_add(buf, src, len);
@@ -968,6 +1214,24 @@ static int ident_to_worktree(const char *path, const char *src, size_t len,
        return 1;
 }
 
+static const char *git_path_check_encoding(struct attr_check_item *check)
+{
+       const char *value = check->value;
+
+       if (ATTR_UNSET(value) || !strlen(value))
+               return NULL;
+
+       if (ATTR_TRUE(value) || ATTR_FALSE(value)) {
+               die(_("true/false are no valid working-tree-encodings"));
+       }
+
+       /* Don't encode to the default encoding */
+       if (same_encoding(value, default_encoding))
+               return NULL;
+
+       return value;
+}
+
 static enum crlf_action git_path_check_crlf(struct attr_check_item *check)
 {
        const char *value = check->value;
@@ -1023,6 +1287,7 @@ struct conv_attrs {
        enum crlf_action attr_action; /* What attr says */
        enum crlf_action crlf_action; /* When no attr is set, use core.autocrlf */
        int ident;
+       const char *working_tree_encoding; /* Supported encoding or default encoding if NULL */
 };
 
 static void convert_attrs(struct conv_attrs *ca, const char *path)
@@ -1031,7 +1296,8 @@ static void convert_attrs(struct conv_attrs *ca, const char *path)
 
        if (!check) {
                check = attr_check_initl("crlf", "ident", "filter",
-                                        "eol", "text", NULL);
+                                        "eol", "text", "working-tree-encoding",
+                                        NULL);
                user_convert_tail = &user_convert;
                git_config(read_convert_config, NULL);
        }
@@ -1054,6 +1320,7 @@ static void convert_attrs(struct conv_attrs *ca, const char *path)
                        else if (eol_attr == EOL_CRLF)
                                ca->crlf_action = CRLF_TEXT_CRLF;
                }
+               ca->working_tree_encoding = git_path_check_encoding(ccheck + 5);
        } else {
                ca->drv = NULL;
                ca->crlf_action = CRLF_UNDEFINED;
@@ -1119,7 +1386,7 @@ const char *get_convert_attr_ascii(const char *path)
 
 int convert_to_git(const struct index_state *istate,
                   const char *path, const char *src, size_t len,
-                   struct strbuf *dst, enum safe_crlf checksafe)
+                  struct strbuf *dst, int conv_flags)
 {
        int ret = 0;
        struct conv_attrs ca;
@@ -1134,8 +1401,15 @@ int convert_to_git(const struct index_state *istate,
                src = dst->buf;
                len = dst->len;
        }
-       if (checksafe != SAFE_CRLF_KEEP_CRLF) {
-               ret |= crlf_to_git(istate, path, src, len, dst, ca.crlf_action, checksafe);
+
+       ret |= encode_to_git(path, src, len, dst, ca.working_tree_encoding, conv_flags);
+       if (ret && dst) {
+               src = dst->buf;
+               len = dst->len;
+       }
+
+       if (!(conv_flags & CONV_EOL_KEEP_CRLF)) {
+               ret |= crlf_to_git(istate, path, src, len, dst, ca.crlf_action, conv_flags);
                if (ret && dst) {
                        src = dst->buf;
                        len = dst->len;
@@ -1146,7 +1420,7 @@ int convert_to_git(const struct index_state *istate,
 
 void convert_to_git_filter_fd(const struct index_state *istate,
                              const char *path, int fd, struct strbuf *dst,
-                             enum safe_crlf checksafe)
+                             int conv_flags)
 {
        struct conv_attrs ca;
        convert_attrs(&ca, path);
@@ -1157,7 +1431,8 @@ void convert_to_git_filter_fd(const struct index_state *istate,
        if (!apply_filter(path, NULL, 0, fd, dst, ca.drv, CAP_CLEAN, NULL))
                die("%s: clean filter '%s' failed", path, ca.drv->name);
 
-       crlf_to_git(istate, path, dst->buf, dst->len, dst, ca.crlf_action, checksafe);
+       encode_to_git(path, dst->buf, dst->len, dst, ca.working_tree_encoding, conv_flags);
+       crlf_to_git(istate, path, dst->buf, dst->len, dst, ca.crlf_action, conv_flags);
        ident_to_git(path, dst->buf, dst->len, dst, ca.ident);
 }
 
@@ -1188,6 +1463,12 @@ static int convert_to_working_tree_internal(const char *path, const char *src,
                }
        }
 
+       ret |= encode_to_worktree(path, src, len, dst, ca.working_tree_encoding);
+       if (ret) {
+               src = dst->buf;
+               len = dst->len;
+       }
+
        ret_filter = apply_filter(
                path, src, len, -1, dst, ca.drv, CAP_SMUDGE, dco);
        if (!ret_filter && ca.drv && ca.drv->required)
@@ -1216,7 +1497,7 @@ int renormalize_buffer(const struct index_state *istate, const char *path,
                src = dst->buf;
                len = dst->len;
        }
-       return ret | convert_to_git(istate, path, src, len, dst, SAFE_CRLF_RENORMALIZE);
+       return ret | convert_to_git(istate, path, src, len, dst, CONV_EOL_RENORMALIZE);
 }
 
 /*****************************************************************
@@ -1500,7 +1781,7 @@ struct ident_filter {
        struct stream_filter filter;
        struct strbuf left;
        int state;
-       char ident[45]; /* ": x40 $" */
+       char ident[GIT_MAX_HEXSZ + 5]; /* ": x40 $" */
 };
 
 static int is_foreign_ident(const char *str)
@@ -1544,8 +1825,9 @@ static int ident_filter_fn(struct stream_filter *filter,
                switch (ident->state) {
                default:
                        strbuf_add(&ident->left, head, ident->state);
+                       /* fallthrough */
                case IDENT_SKIPPING:
-                       /* fallthru */
+                       /* fallthrough */
                case IDENT_DRAINING:
                        ident_drain(ident, &output, osize_p);
                }
@@ -1624,12 +1906,12 @@ static struct stream_filter_vtbl ident_vtbl = {
        ident_free_fn,
 };
 
-static struct stream_filter *ident_filter(const unsigned char *sha1)
+static struct stream_filter *ident_filter(const struct object_id *oid)
 {
        struct ident_filter *ident = xmalloc(sizeof(*ident));
 
        xsnprintf(ident->ident, sizeof(ident->ident),
-                 ": %s $", sha1_to_hex(sha1));
+                 ": %s $", oid_to_hex(oid));
        strbuf_init(&ident->left, 0);
        ident->filter.vtbl = &ident_vtbl;
        ident->state = 0;
@@ -1644,7 +1926,7 @@ static struct stream_filter *ident_filter(const unsigned char *sha1)
  * Note that you would be crazy to set CRLF, smuge/clean or ident to a
  * large binary blob you would want us not to slurp into the memory!
  */
-struct stream_filter *get_stream_filter(const char *path, const unsigned char *sha1)
+struct stream_filter *get_stream_filter(const char *path, const struct object_id *oid)
 {
        struct conv_attrs ca;
        struct stream_filter *filter = NULL;
@@ -1653,11 +1935,14 @@ struct stream_filter *get_stream_filter(const char *path, const unsigned char *s
        if (ca.drv && (ca.drv->process || ca.drv->smudge || ca.drv->clean))
                return NULL;
 
+       if (ca.working_tree_encoding)
+               return NULL;
+
        if (ca.crlf_action == CRLF_AUTO || ca.crlf_action == CRLF_AUTO_CRLF)
                return NULL;
 
        if (ca.ident)
-               filter = ident_filter(sha1);
+               filter = ident_filter(oid);
 
        if (output_eol(ca.crlf_action) == EOL_CRLF)
                filter = cascade_filter(filter, lf_to_crlf_filter());