Browse Source

fix stb_fclose to do a better job of preserving content

Sean Barrett 8 years ago
parent
commit
d74530cd10
1 changed files with 90 additions and 38 deletions
  1. 90 38
      stb.h

+ 90 - 38
stb.h

@@ -1024,8 +1024,8 @@ stb__wchar *stb__from_utf8(char *str)
 
 stb__wchar *stb__from_utf8_alt(char *str)
 {
-   static stb__wchar buffer[64];
-   return stb_from_utf8(buffer, str, 64);
+   static stb__wchar buffer[4096];
+   return stb_from_utf8(buffer, str, 4096);
 }
 
 char *stb__to_utf8(stb__wchar *str)
@@ -5435,63 +5435,73 @@ typedef struct
    int   errors;
 } stb__file_data;
 
-FILE *  stb_fopen(char *filename, char *mode)
+static FILE *stb__open_temp_file(char *temp_name, char *src_name, char *mode)
 {
-   FILE *f;
-   char name_full[4096];
-   char temp_full[sizeof(name_full) + 12];
    int p;
 #ifdef _MSC_VER
    int j;
 #endif
-   if (mode[0] != 'w' && !strchr(mode, '+'))
-      return stb__fopen(filename, mode);
-
-   // save away the full path to the file so if the program
-   // changes the cwd everything still works right! unix has
-   // better ways to do this, but we have to work in windows
-   name_full[0] = '\0'; // stb_fullpath reads name_full[0]
-   if (stb_fullpath(name_full, sizeof(name_full), filename)==0)
-      return 0;
-
+   FILE *f;
    // try to generate a temporary file in the same directory
-   p = strlen(name_full)-1;
-   while (p > 0 && name_full[p] != '/' && name_full[p] != '\\'
-                && name_full[p] != ':' && name_full[p] != '~')
+   p = strlen(src_name)-1;
+   while (p > 0 && src_name[p] != '/' && src_name[p] != '\\'
+                && src_name[p] != ':' && src_name[p] != '~')
       --p;
    ++p;
 
-   memcpy(temp_full, name_full, p);
+   memcpy(temp_name, src_name, p);
 
    #ifdef _MSC_VER
    // try multiple times to make a temp file... just in
    // case some other process makes the name first
    for (j=0; j < 32; ++j) {
-      strcpy(temp_full+p, "stmpXXXXXX");
-      if (stb_mktemp(temp_full) == NULL)
+      strcpy(temp_name+p, "stmpXXXXXX");
+      if (stb_mktemp(temp_name) == NULL)
          return 0;
 
-      f = fopen(temp_full, mode);
+      f = fopen(temp_name, mode);
       if (f != NULL)
          break;
    }
    #else
    {
-      strcpy(temp_full+p, "stmpXXXXXX");
+      strcpy(temp_name+p, "stmpXXXXXX");
       #ifdef __MINGW32__
-         int fd = open(mktemp(temp_full), O_RDWR);
+         int fd = open(mktemp(temp_name), O_RDWR);
       #else
-         int fd = mkstemp(temp_full);
+         int fd = mkstemp(temp_name);
       #endif
       if (fd == -1) return NULL;
       f = fdopen(fd, mode);
       if (f == NULL) {
-         unlink(temp_full);
+         unlink(temp_name);
          close(fd);
          return NULL;
       }
    }
    #endif
+   return f;
+}
+
+
+FILE *  stb_fopen(char *filename, char *mode)
+{
+   FILE *f;
+   char name_full[4096];
+   char temp_full[sizeof(name_full) + 12];
+
+   // @TODO: if the file doesn't exist, we can also use the fastpath here
+   if (mode[0] != 'w' && !strchr(mode, '+'))
+      return stb__fopen(filename, mode);
+
+   // save away the full path to the file so if the program
+   // changes the cwd everything still works right! unix has
+   // better ways to do this, but we have to work in windows
+   name_full[0] = '\0'; // stb_fullpath reads name_full[0]
+   if (stb_fullpath(name_full, sizeof(name_full), filename)==0)
+      return 0;
+
+   f = stb__open_temp_file(temp_full, name_full, mode);
    if (f != NULL) {
       stb__file_data *d = (stb__file_data *) malloc(sizeof(*d));
       if (!d) { assert(0);  /* NOTREACHED */fclose(f); return NULL; }
@@ -5534,21 +5544,63 @@ int     stb_fclose(FILE *f, int keep)
       }
    }
 
-   if (keep != stb_keep_no) {
-      if (stb_fexists(d->name) && remove(d->name)) {
-         // failed to delete old, so don't keep new
-         keep = stb_keep_no;
+   if (keep == stb_keep_no) {
+      remove(d->temp_name);
+   } else {
+      if (!stb_fexists(d->name)) {
+         // old file doesn't exist, so just move the new file over it
+         stb_rename(d->temp_name, d->name);
       } else {
-         if (!stb_rename(d->temp_name, d->name))
-            ok = STB_TRUE;
-         else
-            keep=stb_keep_no;
+         // don't delete the old file yet in case there are troubles! First rename it!
+         char preserved_old_file[4096];
+
+         // generate a temp filename in the same directory (also creates it, which we don't need)
+         FILE *dummy = stb__open_temp_file(preserved_old_file, d->name, "wb");
+         if (dummy != NULL) {
+            // we don't actually want the open file
+            fclose(dummy);
+
+            // discard what we just created
+            remove(preserved_old_file);  // if this fails, there's nothing we can do, and following logic handles it as best as possible anyway
+
+            // move the existing file to the preserved name
+            if (0 != stb_rename(d->name, preserved_old_file)) {  // 0 on success
+               // failed, state is:
+               //    filename  -> old file
+               //    tempname  -> new file
+               // keep tempname around so we don't lose data
+            } else {
+               //  state is:
+               //    preserved -> old file
+               //    tempname  -> new file
+               // move the new file to the old name
+               if (0 == stb_rename(d->temp_name, d->name)) {
+                  //  state is:
+                  //    preserved -> old file
+                  //    filename  -> new file
+                  ok = STB_TRUE;
+
+                  // 'filename -> new file' has always been the goal, so clean up
+                  remove(preserved_old_file); // nothing to be done if it fails
+               } else {
+                  // couldn't rename, so try renaming preserved file back
+
+                  //  state is:
+                  //    preserved -> old file
+                  //    tempname  -> new file
+                  stb_rename(preserved_old_file, d->name);
+                  // if the rename failed, there's nothing more we can do
+               }
+            }
+         } else {
+            // we couldn't get a temp filename. do this the naive way; the worst case failure here
+            // leaves the filename pointing to nothing and the new file as a tempfile
+            remove(d->name);
+            stb_rename(d->temp_name, d->name);
+         }
       }
    }
 
-   if (keep == stb_keep_no)
-      remove(d->temp_name);
-
    free(d->temp_name);
    free(d->name);
    free(d);