--- trunk/jscoverage-server.c 2008/06/01 14:05:47 119 +++ trunk/jscoverage-server.c 2009/08/09 16:21:27 447 @@ -1,6 +1,6 @@ /* jscoverage-server.c - JSCoverage server main routine - Copyright (C) 2008 siliconforks.com + Copyright (C) 2008, 2009 siliconforks.com 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 @@ -22,20 +22,30 @@ #include #include #include +#include #include #include +#ifdef HAVE_PTHREAD_H #include +#endif +#include "encoding.h" +#include "global.h" #include "http-server.h" #include "instrument-js.h" #include "resource-manager.h" #include "stream.h" #include "util.h" +static const char * specified_encoding = NULL; +const char * jscoverage_encoding = "ISO-8859-1"; +bool jscoverage_highlight = true; + typedef struct SourceCache { char * url; - Stream * source; + uint16_t * characters; + size_t num_characters; struct SourceCache * next; } SourceCache; @@ -64,41 +74,52 @@ static const char ** no_instrument; static size_t num_no_instrument = 0; +#ifdef __MINGW32__ +CRITICAL_SECTION javascript_mutex; +CRITICAL_SECTION source_cache_mutex; +#define LOCK EnterCriticalSection +#define UNLOCK LeaveCriticalSection +#else pthread_mutex_t javascript_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t source_cache_mutex = PTHREAD_MUTEX_INITIALIZER; - -static Stream * find_cached_source(const char * url) { - Stream * result = NULL; - pthread_mutex_lock(&source_cache_mutex); +#define LOCK pthread_mutex_lock +#define UNLOCK pthread_mutex_unlock +#endif + +static const SourceCache * find_cached_source(const char * url) { + SourceCache * result = NULL; + LOCK(&source_cache_mutex); for (SourceCache * p = source_cache; p != NULL; p = p->next) { if (strcmp(url, p->url) == 0) { - result = p->source; + result = p; break; } } - pthread_mutex_unlock(&source_cache_mutex); + UNLOCK(&source_cache_mutex); return result; } -static void add_cached_source(const char * url, Stream * source) { +static void add_cached_source(const char * url, uint16_t * characters, size_t num_characters) { SourceCache * new_source_cache = xmalloc(sizeof(SourceCache)); new_source_cache->url = xstrdup(url); - new_source_cache->source = source; - pthread_mutex_lock(&source_cache_mutex); + new_source_cache->characters = characters; + new_source_cache->num_characters = num_characters; + LOCK(&source_cache_mutex); new_source_cache->next = source_cache; source_cache = new_source_cache; - pthread_mutex_unlock(&source_cache_mutex); + UNLOCK(&source_cache_mutex); } -static int get(const char * url, Stream * stream) __attribute__((warn_unused_result)); +static int get(const char * url, uint16_t ** characters, size_t * num_characters) __attribute__((warn_unused_result)); -static int get(const char * url, Stream * stream) { +static int get(const char * url, uint16_t ** characters, size_t * num_characters) { char * host = NULL; uint16_t port; char * abs_path = NULL; char * query = NULL; HTTPConnection * connection = NULL; HTTPExchange * exchange = NULL; + Stream * stream = NULL; int result = URL_parse(url, &host, &port, &abs_path, &query); if (result != 0) { @@ -123,14 +144,27 @@ goto done; } + stream = Stream_new(0); result = HTTPExchange_read_entire_response_entity_body(exchange, stream); if (result != 0) { goto done; } + char * encoding = HTTPMessage_get_charset(HTTPExchange_get_response_message(exchange)); + if (encoding == NULL) { + encoding = xstrdup(jscoverage_encoding); + } + result = jscoverage_bytes_to_characters(encoding, stream->data, stream->length, characters, num_characters); + free(encoding); + if (result != 0) { + goto done; + } result = 0; done: + if (stream != NULL) { + Stream_delete(stream); + } if (exchange != NULL) { HTTPExchange_delete(exchange); } @@ -204,6 +238,44 @@ return result; } +static unsigned int hex_value(char c) { + if ('0' <= c && c <= '9') { + return c - '0'; + } + else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } + else if ('a' <= c && c <= 'f') { + return c - 'a' + 10; + } + else { + return 0; + } +} + +static char * decode_uri_component(const char * s) { + size_t length = strlen(s); + char * result = xmalloc(length + 1); + char * p = result; + while (*s != '\0') { + if (*s == '%') { + if (s[1] == '\0' || s[2] == '\0') { + *p = '\0'; + return result; + } + *p = hex_value(s[1]) * 16 + hex_value(s[2]); + s += 2; + } + else { + *p = *s; + } + p++; + s++; + } + *p = '\0'; + return result; +} + static const char * get_entity(char c) { switch(c) { case '<': @@ -344,9 +416,9 @@ Stream * stream = Stream_new(0); Stream_write_file_contents(stream, f); - pthread_mutex_lock(&javascript_mutex); + LOCK(&javascript_mutex); int result = jscoverage_parse_json(coverage, stream->data, stream->length); - pthread_mutex_unlock(&javascript_mutex); + UNLOCK(&javascript_mutex); Stream_delete(stream); return result; @@ -372,9 +444,12 @@ case '\t': fputs("\\t", f); break; + /* IE doesn't support this */ + /* case '\v': fputs("\\v", f); break; + */ case '"': fputs("\\\"", f); break; @@ -389,6 +464,13 @@ putc('"', f); } +static void write_source(const char * id, const uint16_t * characters, size_t num_characters, FILE * f) { + Stream * output = Stream_new(num_characters); + jscoverage_write_source(id, characters, num_characters, output); + fwrite(output->data, 1, output->length, f); + Stream_delete(output); +} + static void write_json_for_file(const FileCoverage * file_coverage, int i, void * p) { FILE * f = p; @@ -399,11 +481,11 @@ write_js_quoted_string(f, file_coverage->id, strlen(file_coverage->id)); fputs(":{\"coverage\":[", f); - for (uint32_t i = 0; i <= file_coverage->num_lines; i++) { + for (uint32_t i = 0; i < file_coverage->num_coverage_lines; i++) { if (i > 0) { putc(',', f); } - int timesExecuted = file_coverage->lines[i]; + int timesExecuted = file_coverage->coverage_lines[i]; if (timesExecuted < 0) { fputs("null", f); } @@ -412,53 +494,84 @@ } } fputs("],\"source\":", f); - if (file_coverage->source == NULL) { + if (file_coverage->source_lines == NULL) { if (proxy) { - Stream * stream = find_cached_source(file_coverage->id); - if (stream == NULL) { - stream = Stream_new(0); - if (get(file_coverage->id, stream) == 0) { - write_js_quoted_string(f, stream->data, stream->length); - add_cached_source(file_coverage->id, stream); + const SourceCache * cached = find_cached_source(file_coverage->id); + if (cached == NULL) { + uint16_t * characters; + size_t num_characters; + if (get(file_coverage->id, &characters, &num_characters) == 0) { + write_source(file_coverage->id, characters, num_characters, f); + add_cached_source(file_coverage->id, characters, num_characters); } else { - fputs("\"\"", f); + fputs("[]", f); HTTPServer_log_err("Warning: cannot retrieve URL: %s\n", file_coverage->id); - Stream_delete(stream); } } else { - write_js_quoted_string(f, stream->data, stream->length); + write_source(file_coverage->id, cached->characters, cached->num_characters, f); } } else { /* check that the path begins with / */ if (file_coverage->id[0] == '/') { - char * source_path = make_path(document_root, file_coverage->id + 1); - FILE * source_file = fopen(source_path, "r"); + char * decoded_path = decode_uri_component(file_coverage->id); + if (strstr(decoded_path, "..") != NULL) { + free(decoded_path); + fputs("[]", f); + HTTPServer_log_err("Warning: invalid source path: %s\n", file_coverage->id); + goto done; + } + char * source_path = make_path(document_root, decoded_path + 1); + free(decoded_path); + FILE * source_file = fopen(source_path, "rb"); free(source_path); if (source_file == NULL) { - fputs("\"\"", f); + fputs("[]", f); HTTPServer_log_err("Warning: cannot open file: %s\n", file_coverage->id); } else { Stream * stream = Stream_new(0); Stream_write_file_contents(stream, source_file); fclose(source_file); - write_js_quoted_string(f, stream->data, stream->length); + uint16_t * characters; + size_t num_characters; + int result = jscoverage_bytes_to_characters(jscoverage_encoding, stream->data, stream->length, &characters, &num_characters); Stream_delete(stream); + if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) { + fputs("[]", f); + HTTPServer_log_err("Warning: encoding %s not supported\n", jscoverage_encoding); + } + else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) { + fputs("[]", f); + HTTPServer_log_err("Warning: error decoding %s in file %s\n", jscoverage_encoding, file_coverage->id); + } + else { + write_source(file_coverage->id, characters, num_characters, f); + free(characters); + } } } else { /* path does not begin with / */ - fputs("\"\"", f); + fputs("[]", f); HTTPServer_log_err("Warning: invalid source path: %s\n", file_coverage->id); } } } else { - write_js_quoted_string(f, file_coverage->source, strlen(file_coverage->source)); + fputc('[', f); + for (uint32_t i = 0; i < file_coverage->num_source_lines; i++) { + if (i > 0) { + fputc(',', f); + } + char * source_line = file_coverage->source_lines[i]; + write_js_quoted_string(f, source_line, strlen(source_line)); + } + fputc(']', f); } +done: fputc('}', f); } @@ -466,7 +579,7 @@ static int write_json(Coverage * coverage, const char * path) { /* write the JSON */ - FILE * f = fopen(path, "w"); + FILE * f = fopen(path, "wb"); if (f == NULL) { return -1; } @@ -502,9 +615,9 @@ } Coverage * coverage = Coverage_new(); - pthread_mutex_lock(&javascript_mutex); + LOCK(&javascript_mutex); int result = jscoverage_parse_json(coverage, json->data, json->length); - pthread_mutex_unlock(&javascript_mutex); + UNLOCK(&javascript_mutex); Stream_delete(json); if (result != 0) { @@ -514,15 +627,34 @@ } mkdir_if_necessary(report_directory); - char * path = make_path(report_directory, "jscoverage.json"); - FILE * f = fopen(path, "r"); - if (f != NULL) { + char * current_report_directory; + if (str_starts_with(abs_path, "/jscoverage-store/") && abs_path[18] != '\0') { + char * dir = decode_uri_component(abs_path + 18); + current_report_directory = make_path(report_directory, dir); + free(dir); + } + else { + current_report_directory = xstrdup(report_directory); + } + mkdir_if_necessary(current_report_directory); + char * path = make_path(current_report_directory, "jscoverage.json"); + + /* check if the JSON file exists */ + struct stat buf; + if (stat(path, &buf) == 0) { /* it exists: merge */ - result = merge(coverage, f); - if (fclose(f) == EOF) { + FILE * f = fopen(path, "rb"); + if (f == NULL) { result = 1; } + else { + result = merge(coverage, f); + if (fclose(f) == EOF) { + result = 1; + } + } if (result != 0) { + free(current_report_directory); free(path); Coverage_delete(coverage); send_response(exchange, 500, "Could not merge with existing coverage data\n"); @@ -534,14 +666,16 @@ free(path); Coverage_delete(coverage); if (result != 0) { + free(current_report_directory); send_response(exchange, 500, "Could not write coverage data\n"); return; } /* copy other files */ - jscoverage_copy_resources(report_directory); - path = make_path(report_directory, "jscoverage.js"); - f = fopen(path, "ab"); + jscoverage_copy_resources(current_report_directory); + path = make_path(current_report_directory, "jscoverage.js"); + free(current_report_directory); + FILE * f = fopen(path, "ab"); free(path); if (f == NULL) { send_response(exchange, 500, "Could not write to file: jscoverage.js\n"); @@ -597,13 +731,13 @@ } } -static void instrument_js(const char * id, Stream * input_stream, Stream * output_stream) { - pthread_mutex_lock(&javascript_mutex); - jscoverage_instrument_js(id, input_stream, output_stream); - pthread_mutex_unlock(&javascript_mutex); - +static void instrument_js(const char * id, const uint16_t * characters, size_t num_characters, Stream * output_stream) { const struct Resource * resource = get_resource("report.js"); Stream_write(output_stream, resource->data, resource->length); + + LOCK(&javascript_mutex); + jscoverage_instrument_js(id, characters, num_characters, output_stream); + UNLOCK(&javascript_mutex); } static bool is_hop_by_hop_header(const char * h) { @@ -721,14 +855,36 @@ } const char * request_uri = HTTPExchange_get_request_uri(client_exchange); + char * encoding = HTTPMessage_get_charset(HTTPExchange_get_response_message(server_exchange)); + if (encoding == NULL) { + encoding = xstrdup(jscoverage_encoding); + } + uint16_t * characters; + size_t num_characters; + int result = jscoverage_bytes_to_characters(encoding, input_stream->data, input_stream->length, &characters, &num_characters); + free(encoding); + Stream_delete(input_stream); + if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) { + send_response(client_exchange, 500, "Encoding not supported\n"); + goto done; + } + else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) { + send_response(client_exchange, 502, "Error decoding response\n"); + goto done; + } + Stream * output_stream = Stream_new(0); - instrument_js(request_uri, input_stream, output_stream); + instrument_js(request_uri, characters, num_characters, output_stream); /* send the headers to the client */ for (const HTTPHeader * h = HTTPExchange_get_response_headers(server_exchange); h != NULL; h = h->next) { if (is_hop_by_hop_header(h->name) || strcasecmp(h->name, HTTP_CONTENT_LENGTH) == 0) { continue; } + else if (strcasecmp(h->name, HTTP_CONTENT_TYPE) == 0) { + HTTPExchange_add_response_header(client_exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1"); + continue; + } HTTPExchange_add_response_header(client_exchange, h->name, h->value); } add_via_header(HTTPExchange_get_response_message(client_exchange), HTTPExchange_get_response_http_version(server_exchange)); @@ -739,12 +895,12 @@ HTTPServer_log_err("Warning: error writing to client\n"); } - /* input_stream goes on the cache */ + /* characters go on the cache */ /* - Stream_delete(input_stream); + free(characters); */ Stream_delete(output_stream); - add_cached_source(request_uri, input_stream); + add_cached_source(request_uri, characters, num_characters); } else { /* does not need instrumentation */ @@ -792,22 +948,30 @@ /* add the `Server' response-header (RFC 2616 14.38, 3.8) */ HTTPExchange_add_response_header(exchange, HTTP_SERVER, "jscoverage-server/" VERSION); + char * decoded_path = NULL; char * filesystem_path = NULL; const char * abs_path = HTTPExchange_get_abs_path(exchange); assert(*abs_path != '\0'); - if (str_starts_with(abs_path, "/jscoverage")) { + decoded_path = decode_uri_component(abs_path); + + if (str_starts_with(decoded_path, "/jscoverage")) { handle_jscoverage_request(exchange); goto done; } - if (strstr(abs_path, "..") != NULL) { + if (strstr(decoded_path, "..") != NULL) { send_response(exchange, 403, "Forbidden\n"); goto done; } - filesystem_path = make_path(document_root, abs_path + 1); + filesystem_path = make_path(document_root, decoded_path + 1); + size_t filesystem_path_length = strlen(filesystem_path); + if (filesystem_path_length > 0 && filesystem_path[filesystem_path_length - 1] == '/') { + /* stat on Windows doesn't work with trailing slash */ + filesystem_path[filesystem_path_length - 1] = '\0'; + } struct stat buf; if (stat(filesystem_path, &buf) == -1) { @@ -816,7 +980,7 @@ } if (S_ISDIR(buf.st_mode)) { - if (filesystem_path[strlen(filesystem_path) - 1] != '/') { + if (abs_path[strlen(abs_path) - 1] != '/') { const char * request_uri = HTTPExchange_get_request_uri(exchange); char * uri = xmalloc(strlen(request_uri) + 2); strcpy(uri, request_uri); @@ -851,31 +1015,66 @@ closedir(d); } else if (S_ISREG(buf.st_mode)) { - FILE * f = fopen(filesystem_path, "r"); + FILE * f = fopen(filesystem_path, "rb"); if (f == NULL) { send_response(exchange, 404, "Not found\n"); goto done; } + /* + When do we send a charset with Content-Type? + if Content-Type is "text" or "application" + if instrumented JavaScript + use Content-Type: application/javascript; charset=ISO-8859-1 + else if --encoding is given + use that encoding + else + send no charset + else + send no charset + */ const char * content_type = get_content_type(filesystem_path); - HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type); - const char * request_uri = HTTPExchange_get_request_uri(exchange); - if (strcmp(content_type, "text/javascript") == 0 && ! is_no_instrument(request_uri)) { - Stream * input_stream = Stream_new(0); - Stream * output_stream = Stream_new(0); + if (strcmp(content_type, "text/javascript") == 0 && ! is_no_instrument(abs_path)) { + HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1"); + Stream * input_stream = Stream_new(0); Stream_write_file_contents(input_stream, f); - instrument_js(request_uri, input_stream, output_stream); + uint16_t * characters; + size_t num_characters; + int result = jscoverage_bytes_to_characters(jscoverage_encoding, input_stream->data, input_stream->length, &characters, &num_characters); + Stream_delete(input_stream); + + if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) { + send_response(exchange, 500, "Encoding not supported\n"); + goto done; + } + else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) { + send_response(exchange, 500, "Error decoding JavaScript file\n"); + goto done; + } + + Stream * output_stream = Stream_new(0); + instrument_js(abs_path, characters, num_characters, output_stream); + free(characters); if (HTTPExchange_write_response(exchange, output_stream->data, output_stream->length) != 0) { HTTPServer_log_err("Warning: error writing to client\n"); } - - Stream_delete(input_stream); Stream_delete(output_stream); } else { + /* send the Content-Type with charset if necessary */ + if (specified_encoding != NULL && (str_starts_with(content_type, "text/") || str_starts_with(content_type, "application/"))) { + char * content_type_with_charset = NULL; + xasprintf(&content_type_with_charset, "%s; charset=%s", content_type, specified_encoding); + HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type_with_charset); + free(content_type_with_charset); + } + else { + HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type); + } + char buffer[8192]; size_t bytes_read; while ((bytes_read = fread(buffer, 1, 8192, f)) > 0) { @@ -893,6 +1092,7 @@ done: free(filesystem_path); + free(decoded_path); } static void handler(HTTPExchange * exchange) { @@ -923,8 +1123,7 @@ exit(EXIT_SUCCESS); } else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) { - printf("jscoverage-server %s\n", VERSION); - exit(EXIT_SUCCESS); + version(); } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { verbose = 1; @@ -933,7 +1132,7 @@ else if (strcmp(argv[i], "--report-dir") == 0) { i++; if (i == argc) { - fatal("--report-dir: option requires an argument"); + fatal_command_line("--report-dir: option requires an argument"); } report_directory = argv[i]; } @@ -944,7 +1143,7 @@ else if (strcmp(argv[i], "--document-root") == 0) { i++; if (i == argc) { - fatal("--document-root: option requires an argument"); + fatal_command_line("--document-root: option requires an argument"); } document_root = argv[i]; } @@ -952,10 +1151,23 @@ document_root = argv[i] + 16; } + else if (strcmp(argv[i], "--encoding") == 0) { + i++; + if (i == argc) { + fatal_command_line("--encoding: option requires an argument"); + } + jscoverage_encoding = argv[i]; + specified_encoding = jscoverage_encoding; + } + else if (strncmp(argv[i], "--encoding=", 11) == 0) { + jscoverage_encoding = argv[i] + 11; + specified_encoding = jscoverage_encoding; + } + else if (strcmp(argv[i], "--ip-address") == 0) { i++; if (i == argc) { - fatal("--ip-address: option requires an argument"); + fatal_command_line("--ip-address: option requires an argument"); } ip_address = argv[i]; } @@ -963,10 +1175,25 @@ ip_address = argv[i] + 13; } + else if (strcmp(argv[i], "--js-version") == 0) { + i++; + if (i == argc) { + fatal_command_line("--js-version: option requires an argument"); + } + jscoverage_set_js_version(argv[i]); + } + else if (strncmp(argv[i], "--js-version=", 13) == 0) { + jscoverage_set_js_version(argv[i] + 13); + } + + else if (strcmp(argv[i], "--no-highlight") == 0) { + jscoverage_highlight = false; + } + else if (strcmp(argv[i], "--no-instrument") == 0) { i++; if (i == argc) { - fatal("--no-instrument: option requires an argument"); + fatal_command_line("--no-instrument: option requires an argument"); } no_instrument[num_no_instrument] = argv[i]; num_no_instrument++; @@ -979,7 +1206,7 @@ else if (strcmp(argv[i], "--port") == 0) { i++; if (i == argc) { - fatal("--port: option requires an argument"); + fatal_command_line("--port: option requires an argument"); } port = argv[i]; } @@ -996,10 +1223,10 @@ } else if (strncmp(argv[i], "-", 1) == 0) { - fatal("unrecognized option `%s'", argv[i]); + fatal_command_line("unrecognized option `%s'", argv[i]); } else { - fatal("too many arguments"); + fatal_command_line("too many arguments"); } } @@ -1007,14 +1234,21 @@ char * end; unsigned long numeric_port = strtoul(port, &end, 10); if (*end != '\0') { - fatal("--port: option must be an integer"); + fatal_command_line("--port: option must be an integer"); } if (numeric_port > UINT16_MAX) { - fatal("--port: option must be 16 bits"); + fatal_command_line("--port: option must be 16 bits"); } /* is this a shutdown? */ if (shutdown) { +#ifdef __MINGW32__ + WSADATA data; + if (WSAStartup(MAKEWORD(1, 1), &data) != 0) { + fatal("could not start Winsock"); + } +#endif + /* INADDR_LOOPBACK */ HTTPConnection * connection = HTTPConnection_new_client("127.0.0.1", numeric_port); if (connection == NULL) { @@ -1044,30 +1278,39 @@ jscoverage_init(); +#ifndef __MINGW32__ /* handle broken pipe */ signal(SIGPIPE, SIG_IGN); +#endif + +#ifdef __MINGW32__ +InitializeCriticalSection(&javascript_mutex); +InitializeCriticalSection(&source_cache_mutex); +#endif if (verbose) { printf("Starting HTTP server on %s:%lu\n", ip_address, numeric_port); + fflush(stdout); } HTTPServer_run(ip_address, (uint16_t) numeric_port, handler); if (verbose) { printf("Stopping HTTP server\n"); + fflush(stdout); } jscoverage_cleanup(); free(no_instrument); - pthread_mutex_lock(&source_cache_mutex); + LOCK(&source_cache_mutex); while (source_cache != NULL) { SourceCache * p = source_cache; source_cache = source_cache->next; free(p->url); - Stream_delete(p->source); + free(p->characters); free(p); } - pthread_mutex_unlock(&source_cache_mutex); + UNLOCK(&source_cache_mutex); return 0; }