// // aegis - project change supervisor // Copyright (C) 1999, 2002-2008 Peter Miller // // 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 // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see // . // #include #include #include #include #include // for assert #include #include #include #include #include #include static int change_maximum_filename_length(change::pointer cp) { pconf_ty *pconf_data; assert(cp->reference_count >= 1); pconf_data = change_pconf_get(cp, 0); assert(pconf_data->maximum_filename_length); return pconf_data->maximum_filename_length; } static bool contains_moronic_ms_restrictions(string_ty *fn) { static const char *const moronic[] = { "[aA][uU][xX]", "[aA][uU][xX].*", "[cC][oO][mM][1-4]", "[cC][oO][mM][1-4].*", "[cC][oO][nN]", "[cC][oO][nN].*", "[nN][uU][lL]", "[nN][uU][lL].*", }; const char *const *cpp; for (cpp = moronic; cpp < ENDOF(moronic); ++cpp) if (gmatch(*cpp, fn->str_text)) return true; return false; } static bool is_a_dos_filename(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data; string_list_ty wl; size_t j; pconf_data = change_pconf_get(cp, 0); if (!pconf_data->dos_filename_required) return true; // // watch out for the moronic ms restrictions // if (contains_moronic_ms_restrictions(fn)) return false; // // make sure the filename // 1. contains at most one dot // 2. contains a non-empty prefix of at most 8 characters // 3. optionally contains a non-empty suffix of at // most 3 characters (reject empty suffix, because DOS // will think that "foo" and "foo." are the same name) // wl.split(fn, "."); if (wl.nstrings < 1 || wl.nstrings > 2) return false; if (wl.string[0]->str_length < 1 || wl.string[0]->str_length > 8) return false; if ( wl.nstrings > 1 && (wl.string[1]->str_length < 1 || wl.string[1]->str_length > 3) ) return false; // // make sure the characters are acceptable // only allow alphanumerics // for (j = 0; j < wl.nstrings; ++j) { for (const char *tp = wl.string[j]->str_text; *tp; ++tp) { if (!isalnum((unsigned char)*tp)) return false; } } return true; } static bool is_a_windows_filename(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data = change_pconf_get(cp, 0); if (!pconf_data->windows_filename_required) return true; // // watch out for the moronic ms restrictions // if (contains_moronic_ms_restrictions(fn)) return false; // // make sure the characters are acceptable // return (0 == strpbrk(fn->str_text, ":\"'\\")); } static int filename_pattern_test(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data; size_t j; assert(cp->reference_count >= 1); pconf_data = change_pconf_get(cp, 0); // // check rejections first // assert(pconf_data->filename_pattern_reject); for (j = 0; j < pconf_data->filename_pattern_reject->length; ++j) { string_ty *s; s = pconf_data->filename_pattern_reject->list[j]; if (gmatch(s->str_text, fn->str_text)) return 0; } // // check acceptable patterns // (defaults to "*" if not set, or set but empty) // assert(pconf_data->filename_pattern_accept); assert(pconf_data->filename_pattern_accept->length); for (j = 0; j < pconf_data->filename_pattern_accept->length; ++j) { string_ty *s; s = pconf_data->filename_pattern_accept->list[j]; if (gmatch(s->str_text, fn->str_text)) return 1; } // // did not match any accept pattern // return 0; } static int change_filename_in_charset(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data; char *s; assert(cp->reference_count >= 1); pconf_data = change_pconf_get(cp, 0); if (pconf_data->posix_filename_charset) { // // For a filename to be portable across conforming // implementations of IEEE Std 1003.1-1988, it shall // consist only of the following characters. Hyphen // shall not be used as the first character of a // portable filename. // if (fn->str_text[0] == '-') return 0; for (s = fn->str_text; *s; ++s) { switch (*s) { default: return 0; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-': case '.': case '_': break; } } } else { // // exclude control characters, space characters // and high-bit-on characters // (This is a "C" locale test.) // for (s = fn->str_text; *s; ++s) if (!isgraph((unsigned char)*s)) return 0; } return 1; } static bool change_filename_shell_safe(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data; char *s; // // Though not a shell special character as such, always disallow // a leading minus. This is because it is the option introducer, // and will cause confusion if allowed into filenames. Also, // there is no elegant and context-free way to quote it. // assert(cp->reference_count >= 1); pconf_data = change_pconf_get(cp, 0); if (fn->str_text[0] == '-') return false; if (!pconf_data->shell_safe_filenames) return true; // // Some shells also treat a leading tilde (~) as meaning "home // directory of" // if (fn->str_text[0] == '~') return false; // // The rest of the restrictions apply to all characters of // the filename. // for (s = fn->str_text;; ++s) { switch ((unsigned char)*s) { default: continue; case 0: break; case '\t': case '\n': case ' ': case '!': case '"': case '#': case '$': case '&': case '\'': case '(': case ')': case '*': case ':': case ';': case '<': case '=': case '>': case '?': case '[': case '\\': case ']': case '^': case '`': case '{': case '|': case '}': return false; } break; } return true; } static bool contains_white_space(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data = change_pconf_get(cp, 0); if (pconf_data->allow_white_space_in_filenames) return false; return (0 != strpbrk(fn->str_text, "\b\t\n\v\f\r ")); } static bool contains_non_ascii(change::pointer cp, string_ty *fn) { pconf_ty *pconf_data = change_pconf_get(cp, 0); if (pconf_data->allow_non_ascii_filenames) return false; const char *s = fn->str_text; const char *end = s + fn->str_length; while (s < end) { unsigned char c = *s++; switch (c) { default: return true; case '\b': case '\t': case '\n': case '\v': case '\f': case '\r': case ' ': case '!': case '"': case '#': case '$': case '%': case '&': case '\'': case '(': case ')': case '*': case '+': case ',': case '-': case '.': case '/': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case ':': case ';': case '<': case '=': case '>': case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': break; } } return false; } // // NAME // change_filename_check // // SYNOPSIS // string_ty *change_filename_check(change::pointer cp, string_ty *fn); // // DESCRIPTION // The change_filename_check function is used to determine if a // filename is acceptable, accirding to the various filename // constraints. // 1. it must not have any element which is too long // 2. it must not have any element which ends in ,D // 3. no directory may duplicate an existing file // 4. you can't call it the name of the default log file // 5. it must not have any illegal characters // // ARGUMENTS // cp - the change the filename is related to // fn - the filename to check // // RETURNS // 0 on success, otherwise a pointer to an error message. // The error message will need to be freed after printing. // string_ty * change_filename_check(change::pointer cp, string_ty *filename) { string_list_ty part; size_t k; string_ty *result; string_ty *s2; // // figure the limits // name_max_dir is for directories // name_max_fil is for the basename // // The margin of 2 on the end is for ",D" suffixes, // and for ",v" in RCS, "s." in SCCS, etc. // trace(("change_filename_check(cp = %08lX, filename = \"%s\")\n{\n", (long)cp, filename->str_text)); assert(cp->reference_count >= 1); int name_max_dir = change_maximum_filename_length(cp); int name_max_fil = change_pathconf_name_max(cp); if (name_max_dir > name_max_fil) name_max_dir = name_max_fil; name_max_fil -= 2; if (name_max_fil > name_max_dir) name_max_fil = name_max_dir; // // break into path elements // and check each element // part.split(filename, "/"); result = 0; for (k = 0; k < part.nstrings; ++k) { s2 = part.string[k]; // // Check for white space characters // if (contains_white_space(cp, s2)) { sub_context_ty sc; sc.var_set_string("File_Name", filename); result = sc.subst_intl ( i18n("file name \"$filename\" contains illegal characters") ); goto done; } // // check for non-white-space not-ascii-printable characters // (i.e. probably UTF8 or something) // if (contains_non_ascii(cp, s2)) { sub_context_ty sc; sc.var_set_string("File_Name", filename); result = sc.subst_intl ( i18n("file name \"$filename\" contains illegal characters") ); goto done; } // // check DOS-full-ness // if (!is_a_dos_filename(cp, s2)) { sub_context_ty *scp; string_ty *s3; s3 = abbreviate_8dos3(s2); scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); sub_var_set_string(scp, "SUGgest", s3); sub_var_optional(scp, "SUGgest"); if (part.nstrings == 1) { result = subst_intl ( scp, i18n("file name \"$filename\" not suitable for DOS") ); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" not suitable for DOS") ); } sub_context_delete(scp); str_free(s3); goto done; } if (!is_a_windows_filename(cp, s2)) { sub_context_ty *scp; scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); if (part.nstrings == 1) { result = subst_intl ( scp, i18n("file name \"$filename\" not suitable for Windows") ); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" not suitable for Windows") ); } sub_context_delete(scp); goto done; } // // check name length // int name_max = name_max_dir; if (k == part.nstrings - 1) name_max = name_max_fil; if (s2->str_length > (size_t)name_max) { sub_context_ty *scp; string_ty *s3; scp = sub_context_new(); if (k == part.nstrings - 1) s3 = abbreviate_filename(s2, name_max); else s3 = abbreviate_dirname(s2, name_max); sub_var_set_string(scp, "File_Name", filename); sub_var_set_long(scp, "Number", (int)(s2->str_length - name_max)); sub_var_optional(scp, "Number"); sub_var_set_string(scp, "SUGgest", s3); sub_var_optional(scp, "SUGgest"); if (part.nstrings == 1) { result = subst_intl ( scp, i18n("file \"$filename\" too long, suggest \"$suggest\" instead") ); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" too long, suggest \"$suggest\" instead") ); } sub_context_delete(scp); str_free(s3); goto done; } // // check for logfile // if (str_equal(s2, change_logfile_basename())) { sub_context_ty *scp; scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); if (part.nstrings == 1) { result = subst_intl(scp, i18n("file \"$filename\" is log file")); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" is log file") ); } sub_context_delete(scp); goto done; } // // check filename for valid characters // if (!change_filename_in_charset(cp, s2)) { sub_context_ty *scp; scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); if (part.nstrings == 1) { result = subst_intl ( scp, i18n("file name \"$filename\" contains illegal characters") ); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" contains illegal characters") ); } sub_context_delete(scp); goto done; } // // check filename for shell special characters // if (!change_filename_shell_safe(cp, s2)) { sub_context_ty *scp; scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); if (part.nstrings == 1) { result = subst_intl ( scp, i18n("file name \"$filename\" contains shell special characters") ); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" contains shell special characters") ); } sub_context_delete(scp); goto done; } if (!filename_pattern_test(cp, s2)) { sub_context_ty *scp; scp = sub_context_new(); sub_var_set_string(scp, "File_Name", filename); if (part.nstrings == 1) { result = subst_intl(scp, i18n("file \"$filename\" bombs filters")); } else { sub_var_set_string(scp, "Part", s2); result = subst_intl ( scp, i18n("file \"$filename\" part \"$part\" bombs filters") ); } sub_context_delete(scp); goto done; } } // // here for all exits // done: trace(("return %08lX;\n", (long)result)); trace(("}\n")); return result; }