1#!/usr/bin/env python3
2import argparse
3import mimetypes
4from pathlib import Path
5import re
6
7response_types = {
8  200: "HTTP/1.0 200 OK",
9  400: "HTTP/1.0 400 Bad Request",
10  404: "HTTP/1.0 404 File not found",
11  501: "HTTP/1.0 501 Not Implemented",
12}
13
14PAYLOAD_ALIGNMENT = 4
15HTTPD_SERVER_AGENT = "lwIP/2.2.0d (http://savannah.nongnu.org/projects/lwip)"
16LWIP_HTTPD_SSI_EXTENSIONS = [".shtml", ".shtm", ".ssi", ".xml", ".json"]
17
18def process_file(input_dir, file):
19    results = []
20
21    # Check content type
22    content_type, _ = mimetypes.guess_type(file)
23    if content_type is None:
24        content_type = "application/octet-stream"
25
26    # file name
27    data = f"/{file.relative_to(input_dir)}\x00"
28    comment = f"\"/{file.relative_to(input_dir)}\" ({len(data)} chars)"
29    while(len(data) % PAYLOAD_ALIGNMENT != 0):
30        data += "\x00"
31    results.append({'data': bytes(data, "utf-8"), 'comment': comment});
32
33    # Header
34    response_type = 200
35    for response_id in response_types:
36        if file.name.startswith(f"{response_id}."):
37            response_type = response_id
38            break
39    data = f"{response_types[response_type]}\r\n"
40    comment = f"\"{response_types[response_type]}\" ({len(data)} chars)"
41    results.append({'data': bytes(data, "utf-8"), 'comment': comment});
42
43    # user agent
44    data = f"Server: {HTTPD_SERVER_AGENT}\r\n"
45    comment = f"\"Server: {HTTPD_SERVER_AGENT}\" ({len(data)} chars)"
46    results.append({'data': bytes(data, "utf-8"), 'comment': comment});
47
48    if file.suffix not in LWIP_HTTPD_SSI_EXTENSIONS:
49        # content length
50        file_size = file.stat().st_size
51        data = f"Content-Length: {file_size}\r\n"
52        comment = f"\"Content-Length: {file_size}\" ({len(data)} chars)"
53        results.append({'data': bytes(data, "utf-8"), 'comment': comment});
54
55    # content type
56    data = f"Content-Type: {content_type}\r\n\r\n"
57    comment = f"\"Content-Type: {content_type}\" ({len(data)} chars)"
58    results.append({'data': bytes(data, "utf-8"), 'comment': comment});
59
60    # file contents
61    data = file.read_bytes()
62    comment = f"raw file data ({len(data)} bytes)"
63    results.append({'data': data, 'comment': comment});
64
65    return results;
66
67def process_file_list(fd, input):
68    data = []
69    fd.write("#include \"lwip/apps/fs.h\"\n")
70    fd.write("\n")
71    # generate the page contents
72    input_dir = None
73    for name in input:
74        file = Path(name)
75        if not file.is_file():
76            raise RuntimeError(f"File not found: {name}")
77        # Take the input directory from the first file
78        if input_dir is None:
79            input_dir = file.parent
80        results = process_file(input_dir, file)
81
82        # make a variable name
83        var_name = str(file.relative_to(input_dir))
84        var_name = re.sub(r"\W+", "_", var_name, flags=re.ASCII)
85
86        # Add a suffix if the variable name is used already
87        if any(d["data_var"] == f"data_{var_name}" for d in data):
88            var_name += f"_{len(data)}"
89
90        data_var = f"data_{var_name}"
91        file_var = f"file_{var_name}"
92
93        # variable containing the raw data
94        fd.write(f"static const unsigned char {data_var}[] = {{\n")
95        for entry in results:
96            fd.write(f"\n    /* {entry['comment']} */\n")
97            byte_count = 0;
98            for b in entry['data']:
99                if byte_count % 16 == 0:
100                    fd.write("    ")
101                byte_count += 1
102                fd.write(f"0x{b:02x},")
103                if byte_count % 16 == 0:
104                    fd.write("\n")
105            if byte_count % 16 != 0:
106                fd.write("\n")
107        fd.write(f"}};\n\n")
108
109        # set the flags
110        flags = "FS_FILE_FLAGS_HEADER_INCLUDED"
111        if file.suffix not in LWIP_HTTPD_SSI_EXTENSIONS:
112            flags += " | FS_FILE_FLAGS_HEADER_PERSISTENT"
113        else:
114            flags += " | FS_FILE_FLAGS_SSI"
115
116        # add variable details to the list
117        data.append({'data_var': data_var, 'file_var': file_var, 'name_size': len(results[0]['data']), 'flags': flags})
118
119    # generate the page details
120    last_var = "NULL"
121    for entry in data:
122        fd.write(f"const struct fsdata_file {entry['file_var']}[] = {{{{\n")
123        fd.write(f"    {last_var},\n")
124        fd.write(f"    {entry['data_var']},\n")
125        fd.write(f"    {entry['data_var']} + {entry['name_size']},\n")
126        fd.write(f"    sizeof({entry['data_var']}) - {entry['name_size']},\n")
127        fd.write(f"    {entry['flags']},\n")
128        fd.write(f"}}}};\n\n")
129        last_var = entry['file_var']
130    fd.write(f"#define FS_ROOT {last_var}\n")
131    fd.write(f"#define FS_NUMFILES {len(data)}\n")
132
133def run_tool():
134    parser = argparse.ArgumentParser(prog="makefsdata.py", description="Generates a source file for the lwip httpd server")
135    parser.add_argument(
136        "-i",
137        "--input",
138        help="input files to add as http content",
139        required=True,
140        nargs='+'
141    )
142    parser.add_argument(
143        "-o",
144        "--output",
145        help="name of the source file to generate",
146        required=True,
147    )
148    args = parser.parse_args()
149    print(args.input)
150
151    mimetypes.init()
152    for ext in [".shtml", ".shtm", ".ssi"]:
153        mimetypes.add_type("text/html", ext)
154
155    with open(args.output, "w") as fd:
156        process_file_list(fd, args.input)
157
158if __name__ == "__main__":
159    run_tool()
160