1 // detector.c written by Mitchell Foral. mitchell<att>caladbolg.net.
2 // See COPYING for license information.
11 #include "languages.h"
14 #include "hash/cppheader_hash.h"
15 #include "hash/disambiguatefunc_hash.h"
16 #include "hash/extension_hash.h"
17 #include "hash/filename_hash.h"
19 #define ISBINARY(x) (x[0] == '\1')
20 #define ISAMBIGUOUS(x) (x[0] == '\2')
21 #define DISAMBIGUATEWHAT(x) &x[1]
25 # define mkstemp(p) _open(_mktemp(p), _O_CREAT | _O_SHORT_LIVED | _O_EXCL)
28 /* Replaces single quotes (') with an escape sequence ('\'')
29 * suitable for use on the command line.
31 void escape_path(char *safe, const char *unsafe) {
47 const char *ohcount_detect_language(SourceFile *sourcefile) {
48 const char *language = NULL;
52 // Attempt to detect using Emacs mode line (/^-\*-\s*mode[\s:]*\w/i).
53 char line[81] = { '\0' }, buf[81];
54 p = ohcount_sourcefile_get_contents(sourcefile);
56 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
58 // Get the contents of the first line.
59 while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
60 length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
61 strncpy(line, p, length);
63 if (*line == '#' && *(line + 1) == '!') {
64 // First line was sh-bang; loop to get contents of second line.
65 while (*pe == '\r' || *pe == '\n') pe++;
69 p = strstr(line, "-*-");
72 while (*p == ' ' || *p == '\t') p++;
73 // detect "mode" (any capitalization)
74 if (strncasecmp(p, "mode", 4) == 0) {
76 while (*p == ' ' || *p == '\t' || *p == ':') p++;
79 while (!isspace(*pe) && *pe != ';' && pe != strstr(pe, "-*-")) pe++;
80 length = (pe - p <= sizeof(buf)) ? pe - p : sizeof(buf);
81 strncpy(buf, p, length);
84 // Special case for "c" or "C" emacs mode header: always means C, not C++
85 if (strcasecmp(buf, "c") == 0) {
89 // First try it with the language name.
90 struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
91 if (rl) language = rl->name;
93 // Then try it with the extension table.
94 struct ExtensionMap *re = ohcount_hash_language_from_ext(buf, length);
95 if (re) language = re->value;
98 // Try the lower-case version of this modeline.
99 for (pe = buf; pe < buf+length; pe++) *pe = tolower(*pe);
100 // First try it with the language name.
101 rl = ohcount_hash_language_from_name(buf, length);
102 if (rl) language = rl->name;
105 // Then try it with the extension table.
106 struct ExtensionMap *re = ohcount_hash_language_from_ext(buf, length);
107 if (re) language = re->value;
111 // Attempt to detect based on file extension.
113 length = strlen(sourcefile->ext);
114 struct ExtensionMap *re = ohcount_hash_language_from_ext(sourcefile->ext,
116 if (re) language = re->value;
118 // Try the lower-case version of this extension.
119 char lowerext[length + 1];
120 strncpy(lowerext, sourcefile->ext, length);
121 lowerext[length] = '\0';
122 for (p = lowerext; p < lowerext + length; p++) *p = tolower(*p);
123 struct ExtensionMap *re = ohcount_hash_language_from_ext(lowerext, length);
124 if (re) language = re->value;
128 // Attempt to detect based on filename.
130 length = strlen(sourcefile->filename);
131 struct FilenameMap *rf =
132 ohcount_hash_language_from_filename(sourcefile->filename, length);
133 if (rf) language = rf->value;
136 // Attempt to detect based on Unix 'file' command.
139 char *path = sourcefile->filepath;
140 if (sourcefile->diskpath)
141 path = sourcefile->diskpath;
142 if (access(path, F_OK) != 0) { // create temporary file
144 strncpy(path, "/tmp/ohcount_XXXXXXX\0", 21);
145 int fd = mkstemp(path);
146 char *contents = ohcount_sourcefile_get_contents(sourcefile);
149 length = contents ? strlen(contents) : 0;
150 if (write(fd, contents, length) != length) {
151 fprintf(stderr, "src/detector.c: Could not write temporary file %s.\n", path);
158 /* Filenames may include single quotes, which must be escaped */
159 char escaped_path[strlen(path) * 4 + 1];
160 escape_path(escaped_path, path);
162 char command[strlen(escaped_path) + 11];
163 sprintf(command, "file -b '%s'", escaped_path);
164 FILE *f = popen(command, "r");
166 if (fgets(line, sizeof(line), f) == NULL) {
167 fprintf(stderr, "src/detector.c: fgets() failed\n");
170 char *eol = line + strlen(line);
171 for (p = line; p < eol; p++) *p = tolower(*p);
172 p = strstr(line, "script text");
173 if (p && p == line) { // /^script text(?: executable)? for \w/
174 p = strstr(line, "for ");
178 while (isalnum(*pe)) pe++;
180 strncpy(buf, p, length);
182 struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
183 if (rl) language = rl->name;
185 } else if (p) { // /(\w+)(?: -\w+)* script text/
189 while (*p == ' ') p--;
190 while (p != line && isalnum(*(p - 1))) p--;
191 if (p != line && *(p - 1) == '-') p--;
192 } while (*p == '-'); // Skip over any switches.
194 strncpy(buf, p, length);
196 struct LanguageMap *rl = ohcount_hash_language_from_name(buf, length);
197 if (rl) language = rl->name;
198 } else if (strstr(line, "xml")) language = LANG_XML;
207 if (ISAMBIGUOUS(language)) {
208 // Call the appropriate function for disambiguation.
209 length = strlen(DISAMBIGUATEWHAT(language));
210 struct DisambiguateFuncsMap *rd =
211 ohcount_hash_disambiguate_func_from_id(DISAMBIGUATEWHAT(language),
213 if (rd) language = rd->value(sourcefile);
214 } else language = ISBINARY(language) ? NULL : language;
219 const char *disambiguate_aspx(SourceFile *sourcefile) {
220 char *p = ohcount_sourcefile_get_contents(sourcefile);
221 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
222 for (; p < eof; p++) {
223 // /<%@\s*Page[^>]+Language="VB"[^>]+%>/
224 p = strstr(p, "<%@");
227 char *pe = strstr(p, "%>");
230 const int length = pe - p;
232 strncpy(buf, p, length);
234 char *eol = buf + strlen(buf);
235 for (p = buf; p < eol; p++) *p = tolower(*p);
237 while (*p == ' ' || *p == '\t') p++;
238 if (strncmp(p, "page", 4) == 0) {
240 if (strstr(p, "language=\"vb\""))
248 // 6502 assembly or XML-based Advanced Stream Redirector ?
249 const char *disambiguate_asx(SourceFile *sourcefile) {
250 char *p = ohcount_sourcefile_get_contents(sourcefile);
251 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
252 for (; p < eof; p++) {
267 return LANG_ASSEMBLER;
270 return LANG_ASSEMBLER; // only blanks - not valid XML, may be valid asm
273 const char *disambiguate_b(SourceFile *sourcefile) {
274 char *p = ohcount_sourcefile_get_contents(sourcefile);
275 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
277 // /(implement[ \t])|(include[ \t]+"[^"]*";)|
278 // ((return|break|continue).*;|(pick|case).*\{)/
279 if (strncmp(p, "implement", 9) == 0 &&
280 (*(p + 9) == ' ' || *(p + 9) == '\t'))
282 else if (strncmp(p, "include", 7) == 0 &&
283 (*(p + 7) == ' ' || *(p + 7) == '\t')) {
285 while (*p == ' ' || *p == '\t') p++;
287 while (*p != '"' && p < eof) p++;
288 if (*p == '"' && *(p + 1) == ';')
291 } else if (strncmp(p, "return", 6) == 0 ||
292 strncmp(p, "break", 5) == 0 ||
293 strncmp(p, "continue", 8) == 0) {
296 } else if (strncmp(p, "pick", 4) == 0 ||
297 strncmp(p, "case", 4) == 0) {
303 return disambiguate_basic(sourcefile);
306 const char *disambiguate_basic(SourceFile *sourcefile) {
310 // Attempt to detect based on file contents.
312 p = ohcount_sourcefile_get_contents(sourcefile);
314 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
316 // Get a line at a time.
317 while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
318 length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
319 strncpy(line, p, length);
327 while (isdigit(*p)) p++;
328 if (*p == ' ' || *p == '\t') {
330 while (*p == ' ' || *p == '\t') p++;
332 return LANG_CLASSIC_BASIC;
338 while (*pe == '\r' || *pe == '\n') pe++;
342 // Attempt to detect from associated VB files in file context.
343 char **filenames = sourcefile->filenames;
346 for (i = 0; filenames[i] != NULL; i++) {
347 pe = filenames[i] + strlen(filenames[i]);
349 while (p > filenames[i] && *(p - 1) != '.') p--;
352 (strncmp(p, "frm", length) == 0 ||
353 strncmp(p, "frx", length) == 0 ||
354 strncmp(p, "vba", length) == 0 ||
355 strncmp(p, "vbp", length) == 0 ||
356 strncmp(p, "vbs", length) == 0)) {
357 return LANG_VISUALBASIC;
362 return LANG_STRUCTURED_BASIC;
365 const char *disambiguate_cs(SourceFile *sourcefile) {
366 // Attempt to detect based on file contents.
367 char *contents = ohcount_sourcefile_get_contents(sourcefile);
368 if (contents && strstr(contents, "<?cs"))
369 return LANG_CLEARSILVER_TEMPLATE;
374 const char *disambiguate_def(SourceFile *sourcefile) {
375 char *p = ohcount_sourcefile_get_contents(sourcefile);
376 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
377 for (; p < eof; p++) {
385 if (p[1] == '*') // Modula-2 comment
389 if (strncmp(p, "DEFINITION", 10) == 0) // Modula-2 "DEFINITION MODULE"
393 return NULL; // not Modula-2
396 return NULL; // only blanks
399 const char *disambiguate_fortran(SourceFile *sourcefile) {
402 p = ohcount_sourcefile_get_contents(sourcefile);
403 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
405 if (*p == ' ' && p + 5 < eof) {
407 for (i = 1; i <= 5; i++)
408 if (!isdigit(*(p + i)) && *(p + i) != ' ')
409 return LANG_FORTRANFREE; // definately not fixed
410 // Possibly fixed (doesn't match /^\s*\d+\s*$/).
412 while (*pe == ' ' || *pe == '\t') pe++;
415 return LANG_FORTRANFIXED;
416 while (isdigit(*pe)) pe++;
417 while (*pe == ' ' || *pe == '\t') pe++;
418 if (*pe != '\r' && *pe != '\n' && pe - p == 5)
419 return LANG_FORTRANFIXED;
422 while (*p != '\r' && *p != '\n' && *p != '&' && p < eof) p++;
425 // Look for free-form continuation.
426 while (*p == ' ' || *p == '\t') p++;
427 if (*p == '\r' || *p == '\n') {
429 while (*pe == '\r' || *pe == '\n' || *pe == ' ' || *pe == '\t') pe++;
431 return LANG_FORTRANFREE;
434 while (*p == '\r' || *p == '\n') p++;
436 return LANG_FORTRANFREE; // might as well be free-form
439 const char *disambiguate_h(SourceFile *sourcefile) {
443 // If the directory contains a matching *.m file, likely Objective C.
444 length = strlen(sourcefile->filename);
445 if (strcmp(sourcefile->ext, "h") == 0) {
447 strncpy(path, sourcefile->filename, length);
449 *(path + length - 1) = 'm';
450 char **filenames = sourcefile->filenames;
453 for (i = 0; filenames[i] != NULL; i++)
454 if (strcmp(path, filenames[i]) == 0)
455 return LANG_OBJECTIVE_C;
459 // Attempt to detect based on file contents.
460 char line[81], buf[81];
461 bof = ohcount_sourcefile_get_contents(sourcefile);
464 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
466 // Get a line at a time.
467 while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
468 length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
469 strncpy(line, p, length);
471 char *eol = line + strlen(line);
474 // Look for C++ headers.
477 while (*p == ' ' || *p == '\t') p++;
478 if (strncmp(p, "include", 7) == 0 &&
479 (*(p + 7) == ' ' || *(p + 7) == '\t')) {
480 // /^#\s*include\s+[<"][^>"]+[>"]/
482 while (*p == ' ' || *p == '\t') p++;
483 if (*p == '<' || *p == '"') {
484 // Is the header file a C++ header file?
487 while (pe < eol && *pe != '>' && *pe != '"') pe++;
489 strncpy(buf, p, length);
491 if (ohcount_hash_is_cppheader(buf, length))
493 // Is the extension for the header file a C++ file?
495 while (p > line && *(p - 1) != '.') p--;
497 strncpy(buf, p, length);
499 struct ExtensionMap *re = ohcount_hash_language_from_ext(buf, length);
500 if (re && strcmp(re->value, LANG_CPP) == 0)
506 // Look for C++ keywords.
509 if (islower(*p) && p != bof && !isalnum(*(p - 1)) && *(p - 1) != '_') {
511 while (islower(*pe)) pe++;
512 if (!isalnum(*pe) && *pe != '_') {
514 strncpy(buf, p, length);
516 if (strcmp(buf, "class") == 0 ||
517 strcmp(buf, "namespace") == 0 ||
518 strcmp(buf, "template") == 0 ||
519 strcmp(buf, "typename") == 0)
528 while (*pe == '\r' || *pe == '\n') pe++;
532 // Nothing to suggest C++.
536 const char *disambiguate_in(SourceFile *sourcefile) {
539 const char *language = NULL;
541 p = sourcefile->filepath;
542 pe = p + strlen(p) - 3;
543 if (strstr(p, ".") <= pe) {
544 // Only if the filename has an extension prior to the .in
547 strncpy(buf, p, length);
549 SourceFile *undecorated = ohcount_sourcefile_new(buf);
550 p = ohcount_sourcefile_get_contents(sourcefile);
554 // The filepath without the '.in' extension does not exist on disk. The
555 // sourcefile->diskpath field must be set incase the detector needs to run
556 // 'file -b' on the file.
557 ohcount_sourcefile_set_diskpath(undecorated, sourcefile->filepath);
558 ohcount_sourcefile_set_contents(undecorated, p);
559 undecorated->filenames = sourcefile->filenames;
560 language = ohcount_sourcefile_get_language(undecorated);
561 ohcount_sourcefile_free(undecorated);
566 const char *disambiguate_inc(SourceFile *sourcefile) {
567 char *p = ohcount_sourcefile_get_contents(sourcefile);
569 char *eof = p + strlen(p);
573 else if (*p == '?' && strncmp(p + 1, "php", 3) == 0)
581 const char *disambiguate_m(SourceFile *sourcefile) {
585 // Attempt to detect based on a weighted heuristic of file contents.
586 int matlab_score = 0;
587 int objective_c_score = 0;
589 int octave_syntax_detected = 0;
591 int i, has_h_headers = 0, has_c_files = 0;
592 char **filenames = sourcefile->filenames;
594 for (i = 0; filenames[i] != NULL; i++) {
598 if (*(pe - 4) == '.' && *(pe - 3) == 'c' &&
599 ((*(pe - 2) == 'p' && *(pe - 1) == 'p') ||
600 (*(pe - 2) == '+' && *(pe - 1) == '+') ||
601 (*(pe - 2) == 'x' && *(pe - 1) == 'x'))) {
603 break; // short circuit
605 } else if (pe - p >= 3) {
606 if (*(pe - 3) == '.' && *(pe - 2) == 'c' && *(pe - 1) == 'c') {
608 break; // short circuit
610 } else if (pe - p >= 2) {
611 if (*(pe - 2) == '.') {
612 if (*(pe - 1) == 'h')
614 else if (*(pe - 1) == 'c' || *(pe - 1) == 'C') {
616 break; // short circuit
622 if (has_h_headers && !has_c_files)
623 objective_c_score += 5;
625 char line[81], buf[81];
626 p = ohcount_sourcefile_get_contents(sourcefile);
628 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
630 // Get a line at a time.
631 while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
632 length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
633 strncpy(line, p, length);
635 char *eol = line + strlen(line);
638 // Look for tell-tale lines.
640 while (*p == ' ' || *p == '\t') p++;
641 if (*p == '%') { // Matlab comment
643 } else if (*p == '#' && strncmp(p, "#import", 7) == 0) { // Objective C
645 } else if (*p == '#') { // Limbo or Octave comment
646 while (*p == '#') p++;
647 if (*p == ' ' || *p == '\t') {
650 octave_syntax_detected = 1;
652 } else if (*p == '/' && *(p + 1) == '/' || *(p + 1) == '*') {
653 objective_c_score++; // Objective C comment
654 } else if (*p == '+' || *p == '-') { // Objective C method signature
656 } else if (*p == '@' || *p == '#') { // Objective C method signature
657 if (strncmp(p, "@implementation", 15) == 0 ||
658 strncmp(p, "@interface", 10) == 0)
660 } else if (strncmp(p, "function", 8) == 0) { // Matlab or Octave function
662 while (*p == ' ' || *p == '\t') p++;
665 } else if (strncmp(p, "include", 7) == 0) { // Limbo include
666 // /^include[ \t]+"[^"]+\.m";/
668 if (*p == ' ' || *p == '\t') {
669 while (*p == ' ' || *p == '\t') p++;
671 while (*p != '"' && p < eol) p++;
672 if (*p == '"' && *(p - 2) == '.' && *(p - 1) == 'm')
678 // Look for Octave keywords.
681 if (islower(*p) && p != line && !isalnum(*(p - 1))) {
683 while (islower(*pe) || *pe == '_') pe++;
686 strncpy(buf, p, length);
688 if (strcmp(buf, "end_try_catch") == 0 ||
689 strcmp(buf, "end_unwind_protect") == 0 ||
690 strcmp(buf, "endfunction") == 0 ||
691 strcmp(buf, "endwhile") == 0)
692 octave_syntax_detected = 1;
698 // Look for Limbo declarations
701 if (*p == ':' && (*(p + 1) == ' ' || *(p + 1) == '\t')) {
702 // /:[ \t]+(module|adt|fn ?\(|con[ \t])/
704 if (strncmp(p, "module", 6) == 0 && !isalnum(*(p + 6)) ||
705 strncmp(p, "adt", 3) == 0 && !isalnum(*(p + 3)) ||
706 strncmp(p, "fn", 2) == 0 &&
707 (*(p + 2) == ' ' && *(p + 3) == '(' || *(p + 2) == '(') ||
708 strncmp(p, "con", 3) == 0 &&
709 (*(p + 3) == ' ' || *(p + 3) == '\t'))
716 while (*pe == '\r' || *pe == '\n') pe++;
720 if (limbo_score > objective_c_score && limbo_score > matlab_score)
722 else if (objective_c_score > matlab_score)
723 return LANG_OBJECTIVE_C;
725 return octave_syntax_detected ? LANG_OCTAVE : LANG_MATLAB;
730 // strnlen is not available on OS X, so we roll our own
731 size_t mystrnlen(const char *begin, size_t maxlen) {
732 const char *end = memchr(begin, '\0', maxlen);
733 return end ? (end - begin) : maxlen;
736 const char *disambiguate_pp(SourceFile *sourcefile) {
737 char *p = ohcount_sourcefile_get_contents(sourcefile);
738 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
740 /* prepare regular expressions */
744 re = pcre_compile("(define\\s+\\w+\\s*\\(|class \\s+\\w+\\s*{)", 0, &error, &erroffset, NULL);
746 for (; p < eof; p++) {
747 if (strncmp(p, "$include", 8) == 0 ||
748 strncmp(p, "$INCLUDE", 8) == 0 ||
749 strncmp(p, "end.", 4) == 0)
751 if (strncmp(p, "enable =>", 9) == 0 ||
752 strncmp(p, "ensure =>", 9) == 0 ||
753 strncmp(p, "content =>", 10) == 0 ||
754 strncmp(p, "source =>", 9) == 0 ||
755 strncmp(p, "include ", 8) == 0)
758 /* regexp for checking for define and class declarations */
762 rc = pcre_exec(re, NULL, p, mystrnlen(p, 100), 0, 0, ovector, 30);
771 const char *disambiguate_pl(SourceFile *sourcefile) {
772 // Attempt to detect based on file contents.
773 char *contents = ohcount_sourcefile_get_contents(sourcefile);
774 if (contents && strstr(contents, "#!/usr/bin/perl"))
776 else if (contents && strstr(contents, "#!/usr/local/bin/perl"))
778 else if (contents && strstr(contents, ":-"))
784 #define QMAKE_SOURCES_SPACE "SOURCES +="
785 #define QMAKE_SOURCES "SOURCES+="
786 #define QMAKE_CONFIG_SPACE "CONFIG +="
787 #define QMAKE_CONFIG "CONFIG+="
789 const char *disambiguate_pro(SourceFile *sourcefile) {
790 char *p = ohcount_sourcefile_get_contents(sourcefile);
791 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
792 for (; p < eof; p++) {
793 if (strncmp(p, QMAKE_SOURCES_SPACE, strlen(QMAKE_SOURCES_SPACE)) == 0 ||
794 strncmp(p, QMAKE_SOURCES, strlen(QMAKE_SOURCES)) == 0 ||
795 strncmp(p, QMAKE_CONFIG_SPACE, strlen(QMAKE_CONFIG_SPACE)) == 0 ||
796 strncmp(p, QMAKE_CONFIG, strlen(QMAKE_CONFIG)) == 0)
797 return LANG_MAKE; // really QMAKE
799 return LANG_IDL_PVWAVE;
802 const char *disambiguate_r(SourceFile *sourcefile) {
803 char *contents = ohcount_sourcefile_get_contents(sourcefile);
807 char *eof = contents + ohcount_sourcefile_get_contents_size(sourcefile);
809 // Detect REBOL by looking for the occurence of "rebol" in the contents
810 // (case-insensitive). Correct REBOL scripts have a "REBOL [...]" header
812 char *needle = "rebol";
813 int len = strlen(needle);
814 for (; contents < eof - len; ++contents)
815 if (tolower(*contents) == *needle &&
816 !strncasecmp(contents, needle, len))
822 const char *disambiguate_st(SourceFile *sourcefile) {
826 // Attempt to detect based on file contents.
827 int found_assignment = 0, found_block_start = 0, found_block_end = 0;
830 p = ohcount_sourcefile_get_contents(sourcefile);
832 char *eof = p + ohcount_sourcefile_get_contents_size(sourcefile);
834 // Get a line at a time.
835 while (pe < eof && *pe != '\r' && *pe != '\n') pe++;
836 length = (pe - p <= sizeof(line)) ? pe - p : sizeof(line);
837 strncpy(line, p, length);
839 char *eol = line + strlen(line);
842 for (p = line; p < eol; p++) {
845 while (p < eol && (*p == ' ' || *p == '\t')) p++;
847 found_assignment = 1;
849 found_block_start = 1;
850 } else if (*p == ']' && *(p + 1) == '.') found_block_end = 1;
851 if (found_assignment && found_block_start && found_block_end)
852 return LANG_SMALLTALK;
857 while (*pe == '\r' || *pe == '\n') pe++;
864 int ohcount_is_binary_filename(const char *filename) {
865 char *p = (char *)filename + strlen(filename);
866 while (p > filename && *(p - 1) != '.') p--;
868 struct ExtensionMap *re;
869 int length = strlen(p);
870 re = ohcount_hash_language_from_ext(p, length);
871 if (re) return ISBINARY(re->value);
872 // Try the lower-case version of this extension.
873 char lowerext[length];
874 strncpy(lowerext, p, length);
875 lowerext[length] = '\0';
876 for (p = lowerext; p < lowerext + length; p++) *p = tolower(*p);
877 re = ohcount_hash_language_from_ext(lowerext, length);
878 if (re) return ISBINARY(re->value);