Use UTF-8 case-insensitive hashes and comparisons for language identifiers.
[pspp] / src / data / sys-file-reader.c
index 6a5afff226130937f8ae387019ca538a705a2cee..defe460f5cb6cc8f5249bfba6c9254d41ee6b7a6 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 1997-2000, 2006-2007, 2009-2011 Free Software Foundation, Inc.
+   Copyright (C) 1997-2000, 2006-2007, 2009-2012 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
@@ -50,6 +50,7 @@
 #include "libpspp/str.h"
 #include "libpspp/stringi-set.h"
 
+#include "gl/c-strtod.h"
 #include "gl/c-ctype.h"
 #include "gl/inttostr.h"
 #include "gl/localcharset.h"
@@ -85,6 +86,21 @@ enum
     EXT_LONG_LABELS   = 21      /* Value labels for long strings. */
   };
 
+/* Fields from the top-level header record. */
+struct sfm_header_record
+  {
+    char magic[5];              /* First 4 bytes of file, then null. */
+    int weight_idx;             /* 0 if unweighted, otherwise a var index. */
+    int nominal_case_size;      /* Number of var positions. */
+
+    /* These correspond to the members of struct sfm_file_info or a dictionary
+       but in the system file's encoding rather than ASCII. */
+    char creation_date[10];    /* "dd mmm yy". */
+    char creation_time[9];     /* "hh:mm:ss". */
+    char eye_catcher[61];       /* Eye-catcher string, then product name. */
+    char file_label[65];        /* File label. */
+  };
+
 struct sfm_var_record
   {
     off_t pos;
@@ -199,11 +215,13 @@ static void skip_extension_record (struct sfm_reader *, int subtype);
 
 static const char *choose_encoding (
   struct sfm_reader *,
+  const struct sfm_header_record *,
   const struct sfm_extension_record *ext_integer,
   const struct sfm_extension_record *ext_encoding);
 
 static struct text_record *open_text_record (
-  struct sfm_reader *, const struct sfm_extension_record *);
+  struct sfm_reader *, const struct sfm_extension_record *,
+  bool recode_to_utf8);
 static void close_text_record (struct sfm_reader *,
                                struct text_record *);
 static bool read_variable_to_value_pair (struct sfm_reader *,
@@ -238,11 +256,11 @@ enum which_format
     WRITE_FORMAT
   };
 
-static void read_header (struct sfm_reader *, int *weight_idx,
-                         int *claimed_oct_cnt, struct sfm_read_info *,
-                         char **file_labelp);
-static void parse_file_label (struct sfm_reader *, const char *file_label,
-                              struct dictionary *);
+static void read_header (struct sfm_reader *, struct sfm_read_info *,
+                         struct sfm_header_record *);
+static void parse_header (struct sfm_reader *,
+                          const struct sfm_header_record *,
+                          struct sfm_read_info *, struct dictionary *);
 static void parse_variable_records (struct sfm_reader *, struct dictionary *,
                                     struct sfm_var_record *, size_t n);
 static void parse_format_spec (struct sfm_reader *, off_t pos,
@@ -280,16 +298,37 @@ static void parse_long_string_value_labels (struct sfm_reader *,
                                             const struct sfm_extension_record *,
                                             struct dictionary *);
 
-/* Opens the system file designated by file handle FH for
-   reading.  Reads the system file's dictionary into *DICT.
-   If INFO is non-null, then it receives additional info about the
-   system file. */
+/* Frees the strings inside INFO. */
+void
+sfm_read_info_destroy (struct sfm_read_info *info)
+{
+  if (info)
+    {
+      free (info->creation_date);
+      free (info->creation_time);
+      free (info->product);
+    }
+}
+
+/* Opens the system file designated by file handle FH for reading.  Reads the
+   system file's dictionary into *DICT.
+
+   Ordinarily the reader attempts to automatically detect the character
+   encoding based on the file's contents.  This isn't always possible,
+   especially for files written by old versions of SPSS or PSPP, so specifying
+   a nonnull ENCODING overrides the choice of character encoding.
+
+   If INFO is non-null, then it receives additional info about the system file,
+   which the caller must eventually free with sfm_read_info_destroy() when it
+   is no longer needed. */
 struct casereader *
-sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
-                 struct sfm_read_info *volatile info)
+sfm_open_reader (struct file_handle *fh, const char *volatile encoding,
+                 struct dictionary **dictp, struct sfm_read_info *infop)
 {
   struct sfm_reader *volatile r = NULL;
-  struct sfm_read_info local_info;
+  struct sfm_read_info *volatile info;
+
+  struct sfm_header_record header;
 
   struct sfm_var_record *vars;
   size_t n_vars, allocated_vars;
@@ -301,11 +340,7 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
 
   struct sfm_extension_record *extensions[32];
 
-  int weight_idx;
-  int claimed_oct_cnt;
-  char *file_label;
-
-  struct dictionary *dict = NULL;
+  struct dictionary *volatile dict = NULL;
   size_t i;
 
   /* Create and initialize reader. */
@@ -318,6 +353,9 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
   r->opcode_idx = sizeof r->opcodes;
   r->corruption_warning = false;
 
+  info = infop ? infop : xmalloc (sizeof *info);
+  memset (info, 0, sizeof *info);
+
   /* TRANSLATORS: this fragment will be interpolated into
      messages in fh_lock() that identify types of files. */
   r->lock = fh_lock (fh, FH_REF_FILE, N_("system file"), FH_ACC_READ, false);
@@ -332,16 +370,11 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
       goto error;
     }
 
-  /* Initialize info. */
-  if (info == NULL)
-    info = &local_info;
-  memset (info, 0, sizeof *info);
-
   if (setjmp (r->bail_out))
     goto error;
 
   /* Read header. */
-  read_header (r, &weight_idx, &claimed_oct_cnt, info, &file_label);
+  read_header (r, info, &header);
 
   vars = NULL;
   n_vars = allocated_vars = 0;
@@ -428,8 +461,11 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
 
      First, figure out the correct character encoding, because this determines
      how the rest of the header data is to be interpreted. */
-  dict = dict_create (choose_encoding (r, extensions[EXT_INTEGER],
-                                       extensions[EXT_ENCODING]));
+  dict = dict_create (encoding
+                      ? encoding
+                      : choose_encoding (r, &header, extensions[EXT_INTEGER],
+                                         extensions[EXT_ENCODING]));
+  r->encoding = dict_get_encoding (dict);
 
   /* These records don't use variables at all. */
   if (document != NULL)
@@ -444,7 +480,7 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
   if (extensions[EXT_FILE_ATTRS] != NULL)
     parse_data_file_attributes (r, extensions[EXT_FILE_ATTRS], dict);
 
-  parse_file_label (r, file_label, dict);
+  parse_header (r, &header, info, dict);
 
   /* Parse the variable records, the basis of almost everything else. */
   parse_variable_records (r, dict, vars, n_vars);
@@ -454,11 +490,12 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
      before those indexes become invalidated by very long string variables. */
   for (i = 0; i < n_labels; i++)
     parse_value_labels (r, dict, vars, n_vars, &labels[i]);
-  if (weight_idx != 0)
+  if (header.weight_idx != 0)
     {
       struct variable *weight_var;
 
-      weight_var = lookup_var_by_index (r, 76, vars, n_vars, weight_idx);
+      weight_var = lookup_var_by_index (r, 76, vars, n_vars,
+                                        header.weight_idx);
       if (var_is_numeric (weight_var))
         dict_set_weight (dict, weight_var);
       else
@@ -495,11 +532,11 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
      amount that the header claims.  SPSS version 13 gets this
      wrong when very long strings are involved, so don't warn in
      that case. */
-  if (claimed_oct_cnt != -1 && claimed_oct_cnt != n_vars
+  if (header.nominal_case_size != -1 && header.nominal_case_size != n_vars
       && info->version_major != 13)
     sys_warn (r, -1, _("File header claims %d variable positions but "
-                       "%d were read from file."),
-              claimed_oct_cnt, n_vars);
+                       "%zu were read from file."),
+              header.nominal_case_size, n_vars);
 
   /* Create an index of dictionary variable widths for
      sfm_read_case to use.  We cannot use the `struct variable's
@@ -510,12 +547,24 @@ sfm_open_reader (struct file_handle *fh, struct dictionary **dictp,
   r->proto = caseproto_ref_pool (dict_get_proto (dict), r->pool);
 
   *dictp = dict;
+  if (infop != info)
+    {
+      sfm_read_info_destroy (info);
+      free (info);
+    }
+
   return casereader_create_sequential
     (NULL, r->proto,
      r->case_cnt == -1 ? CASENUMBER_MAX: r->case_cnt,
                                        &sys_file_casereader_class, r);
 
 error:
+  if (infop != info)
+    {
+      sfm_read_info_destroy (info);
+      free (info);
+    }
+
   close_reader (r);
   dict_destroy (dict);
   *dictp = NULL;
@@ -566,39 +615,30 @@ sys_file_casereader_destroy (struct casereader *reader UNUSED, void *r_)
 bool
 sfm_detect (FILE *file)
 {
-  char rec_type[5];
+  char magic[5];
 
-  if (fread (rec_type, 4, 1, file) != 1)
+  if (fread (magic, 4, 1, file) != 1)
     return false;
-  rec_type[4] = '\0';
+  magic[4] = '\0';
 
-  return !strcmp ("$FL2", rec_type);
+  return !strcmp (ASCII_MAGIC, magic) || !strcmp (EBCDIC_MAGIC, magic);
 }
 \f
-/* Reads the global header of the system file.  Sets *WEIGHT_IDX to 0 if the
-   system file is unweighted, or to the value index of the weight variable
-   otherwise.  Sets *CLAIMED_OCT_CNT to the number of "octs" (8-byte units) per
-   case that the file claims to have (although it is not always correct).
-   Initializes INFO with header information.  Stores the file label as a string
-   in dictionary encoding into *FILE_LABELP. */
+/* Reads the global header of the system file.  Initializes *HEADER and *INFO,
+   except for the string fields in *INFO, which parse_header() will initialize
+   later once the file's encoding is known. */
 static void
-read_header (struct sfm_reader *r, int *weight_idx,
-             int *claimed_oct_cnt, struct sfm_read_info *info,
-             char **file_labelp)
+read_header (struct sfm_reader *r, struct sfm_read_info *info,
+             struct sfm_header_record *header)
 {
-  char rec_type[5];
-  char eye_catcher[61];
   uint8_t raw_layout_code[4];
   uint8_t raw_bias[8];
-  char creation_date[10];
-  char creation_time[9];
-  char file_label[65];
-  struct substring product;
 
-  read_string (r, rec_type, sizeof rec_type);
-  read_string (r, eye_catcher, sizeof eye_catcher);
+  read_string (r, header->magic, sizeof header->magic);
+  read_string (r, header->eye_catcher, sizeof header->eye_catcher);
 
-  if (strcmp ("$FL2", rec_type) != 0)
+  if (strcmp (ASCII_MAGIC, header->magic)
+      && strcmp (EBCDIC_MAGIC, header->magic))
     sys_error (r, 0, _("This is not an SPSS system file."));
 
   /* Identify integer format. */
@@ -611,13 +651,14 @@ read_header (struct sfm_reader *r, int *weight_idx,
           && r->integer_format != INTEGER_LSB_FIRST))
     sys_error (r, 64, _("This is not an SPSS system file."));
 
-  *claimed_oct_cnt = read_int (r);
-  if (*claimed_oct_cnt < 0 || *claimed_oct_cnt > INT_MAX / 16)
-    *claimed_oct_cnt = -1;
+  header->nominal_case_size = read_int (r);
+  if (header->nominal_case_size < 0
+      || header->nominal_case_size > INT_MAX / 16)
+    header->nominal_case_size = -1;
 
   r->compressed = read_int (r) != 0;
 
-  *weight_idx = read_int (r);
+  header->weight_idx = read_int (r);
 
   r->case_cnt = read_int (r);
   if ( r->case_cnt > INT_MAX / 2)
@@ -650,25 +691,15 @@ read_header (struct sfm_reader *r, int *weight_idx,
     }
   float_convert (r->float_format, raw_bias, FLOAT_NATIVE_DOUBLE, &r->bias);
 
-  read_string (r, creation_date, sizeof creation_date);
-  read_string (r, creation_time, sizeof creation_time);
-  read_string (r, file_label, sizeof file_label);
+  read_string (r, header->creation_date, sizeof header->creation_date);
+  read_string (r, header->creation_time, sizeof header->creation_time);
+  read_string (r, header->file_label, sizeof header->file_label);
   skip_bytes (r, 3);
 
-  strcpy (info->creation_date, creation_date);
-  strcpy (info->creation_time, creation_time);
   info->integer_format = r->integer_format;
   info->float_format = r->float_format;
   info->compressed = r->compressed;
   info->case_cnt = r->case_cnt;
-
-  product = ss_cstr (eye_catcher);
-  ss_match_string (&product, ss_cstr ("@(#) SPSS DATA FILE"));
-  ss_trim (&product, ss_cstr (" "));
-  str_copy_buf_trunc (info->product, sizeof info->product,
-                      ss_data (product), ss_length (product));
-
-  *file_labelp = pool_strdup0 (r->pool, file_label, sizeof file_label - 1);
 }
 
 /* Reads a variable (type 2) record from R into RECORD. */
@@ -696,7 +727,7 @@ read_variable_record (struct sfm_reader *r, struct sfm_var_record *record)
 
       /* Read up to MAX_LABEL_LEN bytes of label. */
       read_len = MIN (MAX_LABEL_LEN, len);
-      record->label = xmalloc (read_len + 1);
+      record->label = pool_malloc (r->pool, read_len + 1);
       read_string (r, record->label, read_len + 1);
 
       /* Skip unread label bytes. */
@@ -777,7 +808,7 @@ read_value_label_record (struct sfm_reader *r,
   record->n_vars = read_int (r);
   if (record->n_vars < 1 || record->n_vars > n_vars)
     sys_error (r, r->pos - 4,
-               _("Number of variables associated with a value label (%d) "
+               _("Number of variables associated with a value label (%zu) "
                  "is not between 1 and the number of variables (%zu)."),
                record->n_vars, n_vars);
   record->vars = pool_nmalloc (r->pool, record->n_vars, sizeof *record->vars);
@@ -914,19 +945,32 @@ skip_extension_record (struct sfm_reader *r, int subtype)
 }
 
 static void
-parse_file_label (struct sfm_reader *r, const char *file_label,
-                  struct dictionary *dict)
+parse_header (struct sfm_reader *r, const struct sfm_header_record *header,
+              struct sfm_read_info *info, struct dictionary *dict)
 {
-  char *utf8_file_label;
-  size_t file_label_len;
-
-  utf8_file_label = recode_string_pool ("UTF-8", dict_get_encoding (dict),
-                                        file_label, -1, r->pool);
-  file_label_len = strlen (utf8_file_label);
-  while (file_label_len > 0 && utf8_file_label[file_label_len - 1] == ' ')
-    file_label_len--;
-  utf8_file_label[file_label_len] = '\0';
-  dict_set_label (dict, utf8_file_label);
+  const char *dict_encoding = dict_get_encoding (dict);
+  struct substring product;
+  struct substring label;
+
+  /* Convert file label to UTF-8 and put it into DICT. */
+  label = recode_substring_pool ("UTF-8", dict_encoding,
+                                 ss_cstr (header->file_label), r->pool);
+  ss_trim (&label, ss_cstr (" "));
+  label.string[label.length] = '\0';
+  dict_set_label (dict, label.string);
+
+  /* Put creation date and time in UTF-8 into INFO. */
+  info->creation_date = recode_string ("UTF-8", dict_encoding,
+                                       header->creation_date, -1);
+  info->creation_time = recode_string ("UTF-8", dict_encoding,
+                                       header->creation_time, -1);
+
+  /* Put product name into INFO, dropping eye-catcher string if present. */
+  product = recode_substring_pool ("UTF-8", dict_encoding,
+                                   ss_cstr (header->eye_catcher), r->pool);
+  ss_match_string (&product, ss_cstr ("@(#) SPSS DATA FILE"));
+  ss_trim (&product, ss_cstr (" "));
+  info->product = ss_xstrdup (product);
 }
 
 /* Reads a variable (type 2) record from R and adds the
@@ -1050,16 +1094,15 @@ parse_format_spec (struct sfm_reader *r, off_t pos, unsigned int format,
   uint8_t w = format >> 8;
   uint8_t d = format;
   struct fmt_spec f;
-
   bool ok;
 
-  if (!fmt_from_io (raw_type, &f.type))
-    sys_error (r, pos, _("Unknown variable format %"PRIu8"."), raw_type);
   f.w = w;
   f.d = d;
 
   msg_disable ();
-  ok = fmt_check_output (&f) && fmt_check_width_compat (&f, var_get_width (v));
+  ok = (fmt_from_io (raw_type, &f.type)
+        && fmt_check_output (&f)
+        && fmt_check_width_compat (&f, var_get_width (v)));
   msg_enable ();
 
   if (ok)
@@ -1069,14 +1112,20 @@ parse_format_spec (struct sfm_reader *r, off_t pos, unsigned int format,
       else
         var_set_write_format (v, &f);
     }
+  else if (format == 0)
+    {
+      /* Actually observed in the wild.  No point in warning about it. */
+    }
   else if (++*n_warnings <= max_warnings)
     {
-      char fmt_string[FMT_STRING_LEN_MAX + 1];
-      sys_warn (r, pos, _("%s variable %s has invalid %s format %s."),
-                var_is_numeric (v) ? _("Numeric") : _("String"),
-                var_get_name (v),
-                which == PRINT_FORMAT ? _("print") : _("write"),
-                fmt_to_string (&f, fmt_string));
+      if (which == PRINT_FORMAT)
+        sys_warn (r, pos, _("Variable %s with width %d has invalid print "
+                            "format 0x%x."),
+                  var_get_name (v), var_get_width (v), format);
+      else
+        sys_warn (r, pos, _("Variable %s with width %d has invalid write "
+                            "format 0x%x."),
+                  var_get_name (v), var_get_width (v), format);
 
       if (*n_warnings == max_warnings)
         sys_warn (r, -1, _("Suppressing further invalid format warnings."));
@@ -1153,6 +1202,7 @@ parse_machine_integer_info (struct sfm_reader *r,
 
 static const char *
 choose_encoding (struct sfm_reader *r,
+                 const struct sfm_header_record *header,
                  const struct sfm_extension_record *ext_integer,
                  const struct sfm_extension_record *ext_encoding)
 {
@@ -1165,6 +1215,7 @@ choose_encoding (struct sfm_reader *r,
   if (ext_integer)
     {
       int codepage = parse_int (r, ext_integer->data, 7 * 4);
+      const char *encoding;
 
       switch (codepage)
         {
@@ -1182,17 +1233,18 @@ choose_encoding (struct sfm_reader *r,
         case 4:
           return "MS_KANJI";
 
-        case 65000:
-          return "UTF-7";
-
-        case 65001:
-          return "UTF-8";
-
         default:
-          return pool_asprintf (r->pool, "CP%d", codepage);
+          encoding = sys_get_encoding_from_codepage (codepage);
+          if (encoding != NULL)
+            return encoding;
+          break;
         }
     }
 
+  /* If the file magic number is EBCDIC then its character data is too. */
+  if (!strcmp (header->magic, EBCDIC_MAGIC))
+    return "EBCDIC-US";
+
   return locale_charset ();
 }
 
@@ -1226,7 +1278,7 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
   struct text_record *text;
   struct mrset *mrset;
 
-  text = open_text_record (r, record);
+  text = open_text_record (r, record, false);
   for (;;)
     {
       const char *counted = NULL;
@@ -1242,12 +1294,12 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
       name = text_get_token (text, ss_cstr ("="), NULL);
       if (name == NULL)
         break;
-      mrset->name = xstrdup (name);
+      mrset->name = recode_string ("UTF-8", r->encoding, name, -1);
 
       if (mrset->name[0] != '$')
         {
           sys_warn (r, record->pos,
-                    _("`%s' does not begin with `$' at UTF-8 offset %zu "
+                    _("`%s' does not begin with `$' at offset %zu "
                       "in MRSETS record."), mrset->name, text_pos (text));
           break;
         }
@@ -1258,7 +1310,7 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
           if (!text_match (text, ' '))
             {
               sys_warn (r, record->pos,
-                        _("Missing space following `%c' at UTF-8 offset %zu "
+                        _("Missing space following `%c' at offset %zu "
                           "in MRSETS record."), 'C', text_pos (text));
               break;
             }
@@ -1277,7 +1329,7 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
           if (!text_match (text, ' '))
             {
               sys_warn (r, record->pos,
-                        _("Missing space following `%c' at UTF-8 offset %zu "
+                        _("Missing space following `%c' at offset %zu "
                           "in MRSETS record."), 'E',  text_pos (text));
               break;
             }
@@ -1288,13 +1340,13 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
           else if (strcmp (number, "1"))
             sys_warn (r, record->pos,
                       _("Unexpected label source value `%s' following `E' "
-                        "at UTF-8 offset %zu in MRSETS record."),
+                        "at offset %zu in MRSETS record."),
                       number, text_pos (text));
         }
       else
         {
           sys_warn (r, record->pos,
-                    _("Missing `C', `D', or `E' at UTF-8 offset %zu "
+                    _("Missing `C', `D', or `E' at offset %zu "
                       "in MRSETS record."),
                     text_pos (text));
           break;
@@ -1310,37 +1362,45 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
       label = text_parse_counted_string (r, text);
       if (label == NULL)
         break;
-      mrset->label = label[0] != '\0' ? xstrdup (label) : NULL;
+      if (label[0] != '\0')
+        mrset->label = recode_string ("UTF-8", r->encoding, label, -1);
 
       stringi_set_init (&var_names);
       allocated_vars = 0;
       width = INT_MAX;
       do
         {
+          const char *raw_var_name;
           struct variable *var;
-          const char *var_name;
+          char *var_name;
 
-          var_name = text_get_token (text, ss_cstr (" \n"), &delimiter);
-          if (var_name == NULL)
+          raw_var_name = text_get_token (text, ss_cstr (" \n"), &delimiter);
+          if (raw_var_name == NULL)
             {
               sys_warn (r, record->pos,
                         _("Missing new-line parsing variable names "
-                          "at UTF-8 offset %zu in MRSETS record."),
+                          "at offset %zu in MRSETS record."),
                         text_pos (text));
               break;
             }
+          var_name = recode_string ("UTF-8", r->encoding, raw_var_name, -1);
 
           var = dict_lookup_var (dict, var_name);
           if (var == NULL)
-            continue;
+            {
+              free (var_name);
+              continue;
+            }
           if (!stringi_set_insert (&var_names, var_name))
             {
               sys_warn (r, record->pos,
                         _("Duplicate variable name %s "
-                          "at UTF-8 offset %zu in MRSETS record."),
+                          "at offset %zu in MRSETS record."),
                         var_name, text_pos (text));
+              free (var_name);
               continue;
             }
+          free (var_name);
 
           if (mrset->label == NULL && mrset->label_from_var_label
               && var_has_label (var))
@@ -1369,6 +1429,7 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
                     _("MRSET %s has only %zu variables."), mrset->name,
                     mrset->n_vars);
           mrset_destroy (mrset);
+         stringi_set_destroy (&var_names);
           continue;
         }
 
@@ -1377,7 +1438,7 @@ parse_mrsets (struct sfm_reader *r, const struct sfm_extension_record *record,
           mrset->width = width;
           value_init (&mrset->counted, width);
           if (width == 0)
-            mrset->counted.f = strtod (counted, NULL);
+            mrset->counted.f = c_strtod (counted, NULL);
           else
             value_copy_str_rpad (&mrset->counted, width,
                                  (const uint8_t *) counted, ' ');
@@ -1535,11 +1596,10 @@ parse_long_var_name_map (struct sfm_reader *r,
      system file, this cannot create any intermediate duplicate variable names,
      because all of the new variable names are longer than any of the old
      variable names and thus there cannot be any overlaps.) */
-  text = open_text_record (r, record);
+  text = open_text_record (r, record, true);
   while (read_variable_to_value_pair (r, dict, text, &var, &long_name))
     {
       /* Validate long name. */
-      /* XXX need to reencode name to UTF-8 */
       if (!dict_id_is_valid (dict, long_name, false))
         {
           sys_warn (r, record->pos,
@@ -1550,7 +1610,7 @@ parse_long_var_name_map (struct sfm_reader *r,
         }
 
       /* Identify any duplicates. */
-      if (strcasecmp (var_get_short_name (var, 0), long_name)
+      if (utf8_strcasecmp (var_get_short_name (var, 0), long_name)
           && dict_lookup_var (dict, long_name) != NULL)
         {
           sys_warn (r, record->pos,
@@ -1574,7 +1634,7 @@ parse_long_string_map (struct sfm_reader *r,
   struct variable *var;
   char *length_s;
 
-  text = open_text_record (r, record);
+  text = open_text_record (r, record, true);
   while (read_variable_to_value_pair (r, dict, text, &var, &length_s))
     {
       size_t idx = var_get_dict_index (var);
@@ -1719,7 +1779,7 @@ lookup_var_by_index (struct sfm_reader *r, off_t offset,
   if (idx < 1 || idx > n_var_recs)
     {
       sys_error (r, offset,
-                 _("Variable index %d not in valid range 1...%d."),
+                 _("Variable index %d not in valid range 1...%zu."),
                  idx, n_var_recs);
       return NULL;
     }
@@ -1802,7 +1862,7 @@ parse_data_file_attributes (struct sfm_reader *r,
                             const struct sfm_extension_record *record,
                             struct dictionary *dict)
 {
-  struct text_record *text = open_text_record (r, record);
+  struct text_record *text = open_text_record (r, record, true);
   parse_attributes (r, text, dict_get_attributes (dict));
   close_text_record (r, text);
 }
@@ -1817,7 +1877,7 @@ parse_variable_attributes (struct sfm_reader *r,
   struct text_record *text;
   struct variable *var;
 
-  text = open_text_record (r, record);
+  text = open_text_record (r, record, true);
   while (text_read_variable_name (r, dict, text, ss_cstr (":"), &var))
     parse_attributes (r, text, var != NULL ? var_get_attributes (var) : NULL);
   close_text_record (r, text);
@@ -2239,15 +2299,17 @@ skip_whole_strings (struct sfm_reader *r, size_t length)
 /* State. */
 struct text_record
   {
-    struct substring buffer;    /* Record contents, in UTF-8. */
+    struct substring buffer;    /* Record contents. */
     off_t start;                /* Starting offset in file. */
     size_t pos;                 /* Current position in buffer. */
     int n_warnings;             /* Number of warnings issued or suppressed. */
+    bool recoded;               /* Recoded into UTF-8? */
   };
 
 static struct text_record *
 open_text_record (struct sfm_reader *r,
-                  const struct sfm_extension_record *record)
+                  const struct sfm_extension_record *record,
+                  bool recode_to_utf8)
 {
   struct text_record *text;
   struct substring raw;
@@ -2255,9 +2317,12 @@ open_text_record (struct sfm_reader *r,
   text = pool_alloc (r->pool, sizeof *text);
   raw = ss_buffer (record->data, record->size * record->count);
   text->start = record->pos;
-  text->buffer = recode_substring_pool ("UTF-8", r->encoding, raw, r->pool);
+  text->buffer = (recode_to_utf8
+                  ? recode_substring_pool ("UTF-8", r->encoding, raw, r->pool)
+                  : raw);
   text->pos = 0;
   text->n_warnings = 0;
+  text->recoded = recode_to_utf8;
 
   return text;
 }
@@ -2270,7 +2335,8 @@ close_text_record (struct sfm_reader *r, struct text_record *text)
   if (text->n_warnings > MAX_TEXT_WARNINGS)
     sys_warn (r, -1, _("Suppressed %d additional related warnings."),
               text->n_warnings - MAX_TEXT_WARNINGS);
-  pool_free (r->pool, ss_data (text->buffer));
+  if (text->recoded)
+    pool_free (r->pool, ss_data (text->buffer));
 }
 
 /* Reads a variable=value pair from TEXT.
@@ -2381,7 +2447,7 @@ text_parse_counted_string (struct sfm_reader *r, struct text_record *text)
 
   start = text->pos;
   n = 0;
-  for (;;)
+  while (text->pos < text->buffer.length)
     {
       int c = text->buffer.string[text->pos];
       if (c < '0' || c > '9')
@@ -2389,10 +2455,10 @@ text_parse_counted_string (struct sfm_reader *r, struct text_record *text)
       n = (n * 10) + (c - '0');
       text->pos++;
     }
-  if (start == text->pos)
+  if (text->pos >= text->buffer.length || start == text->pos)
     {
       sys_warn (r, text->start,
-                _("Expecting digit at UTF-8 offset %zu in MRSETS record."),
+                _("Expecting digit at offset %zu in MRSETS record."),
                 text->pos);
       return NULL;
     }
@@ -2400,7 +2466,7 @@ text_parse_counted_string (struct sfm_reader *r, struct text_record *text)
   if (!text_match (text, ' '))
     {
       sys_warn (r, text->start,
-                _("Expecting space at UTF-8 offset %zu in MRSETS record."),
+                _("Expecting space at offset %zu in MRSETS record."),
                 text->pos);
       return NULL;
     }
@@ -2408,7 +2474,7 @@ text_parse_counted_string (struct sfm_reader *r, struct text_record *text)
   if (text->pos + n > text->buffer.length)
     {
       sys_warn (r, text->start,
-                _("%zu-byte string starting at UTF-8 offset %zu "
+                _("%zu-byte string starting at offset %zu "
                   "exceeds record length %zu."),
                 n, text->pos, text->buffer.length);
       return NULL;
@@ -2418,8 +2484,7 @@ text_parse_counted_string (struct sfm_reader *r, struct text_record *text)
   if (s[n] != ' ')
     {
       sys_warn (r, text->start,
-                _("Expecting space at UTF-8 offset %zu following %zu-byte "
-                  "string."),
+                _("Expecting space at offset %zu following %zu-byte string."),
                 text->pos + n, n);
       return NULL;
     }
@@ -2440,8 +2505,8 @@ text_match (struct text_record *text, char c)
     return false;
 }
 
-/* Returns the current byte offset (as convertd to UTF-8) inside the TEXT's
-   string. */
+/* Returns the current byte offset (as converted to UTF-8, if it was converted)
+   inside the TEXT's string. */
 static size_t
 text_pos (const struct text_record *text)
 {