├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── dub.sdl ├── examples └── files.har ├── harmain.d ├── out └── .gitignore ├── src └── archive │ └── har.d ├── test ├── a.expected │ └── a ├── a.har ├── badHarFiles │ ├── emptyfile.har │ └── nospace.har ├── hartests.d ├── newlines.expected │ ├── another_empty_file.txt │ ├── empty_file.txt │ ├── one_newline_file.txt │ └── two_newlines_file.txt └── newlines.har └── test_command_line_tool.d /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /.dub 3 | 4 | # for -cov 5 | *.lst 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is granted to use/copy/modify/distribute the contents of this 2 | repository without restriction. 3 | 4 | If you would like to use this software and need the terms in the form of a 5 | "standard license" then you can also use this software under the terms of 6 | the "Boost Software License - Version 1.0" included below: 7 | 8 | Boost Software License - Version 1.0 - August 17th, 2003 9 | 10 | Permission is hereby granted, free of charge, to any person or organization 11 | obtaining a copy of the software and accompanying documentation covered by 12 | this license (the "Software") to use, reproduce, display, distribute, 13 | execute, and transmit the Software, and to prepare derivative works of the 14 | Software, and to permit third-parties to whom the Software is furnished to 15 | do so, all subject to the following: 16 | 17 | The copyright notices in the Software and this entire statement, including 18 | the above license grant, this restriction and the following disclaimer, 19 | must be included in all copies of the Software, in whole or in part, and 20 | all derivative works of the Software, unless such copies or derivative 21 | works are solely in the form of machine-executable object code generated by 22 | a source language processor. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 27 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 28 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 29 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 30 | DEALINGS IN THE SOFTWARE. 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Note: recommended way is via `dub build` 2 | out_dir=./out/ 3 | 4 | all: har run_tests 5 | 6 | har: harmain.d src/archive/har.d 7 | dmd -of=${out_dir}/har -g -debug harmain.d src/archive/har.d 8 | 9 | run_tests: 10 | dmd -cov src/archive/har.d -run test/hartests.d 11 | rdmd test_command_line_tool.d 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAR - Human Archive Format 2 | 3 | A human readable-writeable format for representing multiple files, named 4 | after the popular `tar` format. The format is meant to be simple, intuitive, 5 | and copy-pasteable allowing a single block of text (like a forum post) to 6 | represent multiple files. 7 | 8 | ### Example: 9 | ``` 10 | --- hello.txt 11 | Hello, this is a file currently archived in a HAR file. 12 | 13 | --- other.txt 14 | This is another file also archived within the same HAR file. 15 | 16 | --- yetanother.txt 17 | This is yet another file also archived within the same HAR file. 18 | 19 | --- dir1/ 20 | --- dir2/ 21 | ``` 22 | 23 | ### Format 24 | ``` 25 | ( )* [] 26 | 27 | ( )* [] 28 | 29 | ... 30 | ``` 31 | 32 | ## What HAR doesn't do 33 | 34 | * Does not support binary files (use tar for that, har is meant for humans) 35 | 36 | # Details 37 | 38 | ## Directory Separators 39 | 40 | HAR only supports the foward slash `/` as a directory separator, regardless of platform. 41 | 42 | Correct: 43 | ``` 44 | --- foo/bar.txt 45 | ``` 46 | Incorrect: 47 | ``` 48 | --- foo\bar.txt 49 | ``` 50 | 51 | ## Filenames with Spaces 52 | 53 | Use quotes if the filename contains whitespace: 54 | ``` 55 | --- "i like spaces/in my filenames" 56 | ``` 57 | 58 | Note that quoted filenames do not support standard escape sequences (i.e. `\\` or `\"`). If you want to include a special character in your filename, include it. The tradeof is that HAR does not support filenames with quotes or newlines, but any other character will work. 59 | 60 | ## Properties 61 | 62 | The format allows files to specify properties, i.e. 63 | ``` 64 | --- file1.txt owner=root 65 | A file owed by root 66 | 67 | --- file2.txt permissions=0772 68 | A file with custom permissions 69 | 70 | ``` 71 | Properties may only be separated by 1 or more space characters (ascii 0x20). 72 | 73 | ## Empty Directories 74 | 75 | Use a trailing slash in the filename to create an empty directory. 76 | ``` 77 | --- mydir/ 78 | --- anotherdir/ owner=root 79 | --- dir3/ readonly 80 | --- "dir with spaces/" 81 | ``` 82 | 83 | Note that this is only necessary for empty directories. All the parent directories for a file do not need to be explicitly declared. i.e. if you have a HAR file like this: 84 | ``` 85 | --- foo/bar/baz.d 86 | My cool file 87 | ``` 88 | you DO NOT need to include it's parent directories: 89 | ``` 90 | --- foo/ 91 | --- foo/bar/ 92 | ``` 93 | 94 | ## Obvious File Breaks 95 | 96 | Extra deliimters can be used after a file to help distinguish where files begin/end, i.e. 97 | ``` 98 | --- myfile.txt ----------------------------------------- 99 | This is my file 100 | ...lots of text 101 | 102 | --- anotherfile.txt ----------------------------------------- 103 | This is another file. The extra '-' characters after the filename 104 | should make it easier to spot the end/beginning of files. 105 | ``` 106 | 107 | The only requirement is that these extra characters start with the first character in the delimiter. For example, the following file has an odd delimiter `#!ab$`, so as soon as a `#` character is found, the rest of the line is ignored, i.e. 108 | ``` 109 | #!ab$ myfile.txt #0a09fa00asdfj 110 | ``` 111 | 112 | ## Custom Delimiters 113 | 114 | The delimiter is used to mark the end of a file and the beginning of a new one. The standard delimiter is `---`, however, any set of characters not containing a spaces or newlines can be used as a delimiter. Also, since a HAR file always begins with a delimiter, there's no need to declare what your delimiter is, simply use it and the parser will pull it from the first line, i.e. 115 | 116 | ``` 117 | ### showCustomBoundary.txt 118 | This file uses a different type of delimiter. 119 | ### another.txt 120 | Another file to show that the previous file boundary has worked correctly. 121 | ``` 122 | 123 | This being said, using the standard delimiter is encouraged to promote uniformity and familiarity with the format. 124 | 125 | ## Newlines 126 | 127 | All standard newlines sequences are supported, `\n`, `\r\n` or `\r`. 128 | 129 | ## Which Newlines belong to the file? 130 | 131 | All newline characters belong to the line they are terminating. This means that all non-empty files will end with a newline. This rule makes processing har files simpler, because without it, determining which newlines belong to the file would require looking ahead at the next line. 132 | 133 | ### Example: 134 | ``` 135 | --- empty_file.txt 136 | --- one_newline_file.txt 137 | this file has one newline 138 | --- two_newlines_file.txt 139 | this file has two newlines 140 | 141 | --- another_empty_file.txt 142 | ``` 143 | 144 | #### empty_file.txt 145 | ``` 146 | EOF 147 | ``` 148 | #### one_newline_file.txt 149 | ``` 150 | this file has one newline\n 151 | EOF 152 | ``` 153 | #### two_newlines_file.txt 154 | ``` 155 | this file has two newlines\n 156 | \n 157 | EOF 158 | ``` 159 | 160 | Note that even though this mechanism makes it simpler to process HAR files, it comes at the cost of not being able to represent files without newlines at the end of the file. Since HAR is not meant to support everything (i.e. binary files), not supporting this small number of use cases is a sensible tradeoff for the added simplicity. 161 | 162 | ## Using ".." 163 | 164 | HAR doesn't support using `..` to create files in parent directories. This guarantees that if you extract a HAR file, it can only create files in the given output directory, it cannot extract files outside of it. 165 | 166 | ## Absolute filenames 167 | 168 | Absolute filenames aren't supported, i.e. 169 | 170 | ``` 171 | --- /myfile.txt 172 | This isn't valid 173 | ``` 174 | 175 | ## Double slashes 176 | 177 | Double slashes are considered an error, i.e. 178 | ``` 179 | --- foo//bar.txt 180 | ``` 181 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "har" 2 | description "An extractor/archiver for HAR, the human-readable archive format" 3 | authors "Jonathan Marler" 4 | copyright "Copyright © 2018, Jonathan Marler" 5 | license "BSL-1.0" 6 | version "0.1.0" 7 | targetType "executable" 8 | targetPath "out" 9 | dependency "har:library" version="*" 10 | sourceFiles "harmain.d" 11 | sourcePaths 12 | 13 | subPackage { 14 | name "library" 15 | description "An extractor/archiver library for HAR, the human-readable archive format" 16 | targetType "library" 17 | targetName "har" 18 | targetPath "out" 19 | sourcePaths "src" 20 | } 21 | 22 | subPackage { 23 | name "test_command_line_tool" 24 | description "Tests the har command line tool" 25 | targetType "executable" 26 | targetPath "out/test" 27 | dependency "har" version="*" 28 | sourceFiles "test_command_line_tool.d" 29 | sourcePaths 30 | } 31 | 32 | subPackage { 33 | name "test_library" 34 | description "Tests the archive.har library" 35 | targetType "executable" 36 | targetPath "out/test" 37 | dependency "har:library" version="*" 38 | sourceFiles "test/hartests.d" 39 | sourcePaths 40 | } 41 | -------------------------------------------------------------------------------- /examples/files.har: -------------------------------------------------------------------------------- 1 | --- hello.txt 2 | Hello, this is a file currently archived in a HAR file. 3 | 4 | --- other.txt 5 | This is another file also archived within the same HAR file. 6 | 7 | --- yetanother.txt 8 | This is yet another file also archived within the same HAR file. 9 | 10 | -------------------------------------------------------------------------------- /harmain.d: -------------------------------------------------------------------------------- 1 | import std.typecons : Nullable, nullable; 2 | import std.string : startsWith, endsWith; 3 | import std.file : exists, isDir, mkdirRecurse; 4 | import std.stdio; 5 | 6 | import archive.har; 7 | 8 | void help() 9 | { 10 | writeln( 11 | `Extracts and creates HAR archive files 12 | 13 | Examples: 14 | har foo/archive.har # Extracts files to foo/archive 15 | har foo bar > archive.har # Create archive from foo and bar 16 | 17 | Options 18 | --dir= Set output directory for extracted files 19 | --quiet Quiet mode (do not list extracted files) 20 | --verbose Verbose mode (print details) 21 | --dry-run Dry run, process the HAR file but don't extract it 22 | `); 23 | } 24 | 25 | class SilentException : Exception { this() { super(null); } } 26 | auto quit() { return new SilentException(); } 27 | 28 | int main(string[] args) 29 | { 30 | try { return tryMain(args); } 31 | catch (SilentException) { return 1; } 32 | catch (HarException e) 33 | { 34 | stderr.writefln("Error: %s(%s) %s", e.file, e.line, e.msg); 35 | return 1; 36 | } 37 | } 38 | int tryMain(string[] args) 39 | { 40 | args = args[1 .. $]; 41 | if (args.length == 0) 42 | { 43 | help(); 44 | return 1; 45 | } 46 | 47 | string outputDirOption = null; 48 | bool quietMode = false; 49 | bool verbose = false; 50 | bool dryRun = false; 51 | 52 | { 53 | size_t newArgsLength = 0; 54 | scope(exit) args.length = newArgsLength; 55 | for (size_t i = 0; i < args.length; i++) 56 | { 57 | auto arg = args[i]; 58 | if (!arg.startsWith("-")) 59 | { 60 | args[newArgsLength++] = arg; 61 | } 62 | else if (arg.startsWith("--dir=")) 63 | outputDirOption = arg[6 .. $]; 64 | else if (arg == "--quiet") 65 | quietMode = true; 66 | else if (arg == "--verbose") 67 | verbose = true; 68 | else if (arg == "--dry-run") 69 | dryRun = true; 70 | else 71 | { 72 | stderr.writefln("Error: unknown option '%s'", arg); 73 | return 1; 74 | } 75 | } 76 | } 77 | 78 | if (args.length == 0) 79 | { 80 | help(); 81 | return 1; 82 | } 83 | 84 | size_t harFileCount = 0; 85 | foreach (file; args) 86 | { 87 | if (file.endsWith(".har")) 88 | { 89 | harFileCount++; 90 | } 91 | if (file.length == 0) 92 | { 93 | stderr.writefln("Error: filenames cannot be empty"); 94 | return 1; 95 | } 96 | } 97 | if (harFileCount == 0) 98 | return archiveFiles(args); 99 | 100 | if (harFileCount < args.length) 101 | { 102 | stderr.writefln("Error: cannot create a har file with other har files"); 103 | return 1; 104 | } 105 | 106 | void handleNewOutputDir(string outputDir) 107 | { 108 | if (exists(outputDir)) 109 | { 110 | if (!isDir(outputDir)) 111 | { 112 | stderr.writefln("Error: cannot extract files to non-directory %s", outputDir.formatDir); 113 | throw quit; 114 | } 115 | if (verbose) 116 | writefln("output directory %s already exists", outputDir.formatDir); 117 | } 118 | else 119 | { 120 | if (verbose) 121 | writefln("mkdir %s", outputDir.formatDir); 122 | if (!dryRun) 123 | mkdirRecurse(outputDir); 124 | } 125 | } 126 | 127 | if (outputDirOption) 128 | { 129 | handleNewOutputDir(outputDirOption); 130 | } 131 | 132 | foreach(harFilename; args) 133 | { 134 | auto extractor = HarExtractor(); 135 | 136 | extractor.dryRun = dryRun; 137 | if (outputDirOption) 138 | extractor.outputDir = outputDirOption; 139 | else 140 | { 141 | extractor.outputDir = harFilename[0 .. $ - ".har".length]; 142 | if (verbose) 143 | writefln("Using default output directory %s", extractor.outputDir.formatDir); 144 | handleNewOutputDir(extractor.outputDir); 145 | } 146 | 147 | if (verbose) 148 | extractor.enableVerbose(stdout); 149 | 150 | extractor.extractFromFile(harFilename, delegate(string fullFileName, FileProperties fileProps) { 151 | if (!quietMode) 152 | { 153 | writeln(fullFileName); 154 | } 155 | }); 156 | } 157 | return 0; 158 | } 159 | 160 | int archiveFiles(string[] files) 161 | { 162 | stderr.writeln("Error: creating har archives is not implemented"); 163 | return 1; 164 | } 165 | 166 | -------------------------------------------------------------------------------- /out/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/.gitignore 3 | -------------------------------------------------------------------------------- /src/archive/har.d: -------------------------------------------------------------------------------- 1 | /** 2 | HAR - Human Archive Format 3 | 4 | https://github.com/marler8997/har 5 | 6 | HAR is a simple format to represent multiple files in a single block of text, i.e. 7 | --- 8 | --- main.d 9 | import foo; 10 | void main() 11 | { 12 | foofunc(); 13 | } 14 | --- foo.d 15 | module foo; 16 | void foofunc() 17 | { 18 | } 19 | --- 20 | */ 21 | module archive.har; 22 | 23 | import std.typecons : Flag, Yes, No; 24 | import std.array : Appender; 25 | import std.format : format; 26 | import std.string : startsWith, indexOf, stripRight; 27 | import std.utf : decode, replacementDchar; 28 | import std.path : dirName, buildPath; 29 | import std.file : exists, isDir, mkdirRecurse; 30 | import std.stdio : File; 31 | 32 | class HarException : Exception 33 | { 34 | this(string msg, string file, size_t line) 35 | { 36 | super(msg, file, line); 37 | } 38 | } 39 | 40 | struct HarExtractor 41 | { 42 | string filenameForErrors; 43 | string outputDir; 44 | 45 | private bool verbose; 46 | private File verboseFile; 47 | 48 | bool dryRun; 49 | 50 | private size_t lineNumber; 51 | private void extractMkdir(string dir, Flag!"forEmptyDir" forEmptyDir) 52 | { 53 | if (exists(dir)) 54 | { 55 | if (!isDir(dir)) 56 | { 57 | if (forEmptyDir) 58 | throw harFileException("cannot extract empty directory %s since it already exists as non-directory", 59 | dir.formatDir); 60 | throw harFileException("cannot extract files to non-directory %s", dir.formatDir); 61 | } 62 | } 63 | else 64 | { 65 | if (verbose) 66 | verboseFile.writefln("mkdir %s", dir.formatDir); 67 | if (!dryRun) 68 | mkdirRecurse(dir); 69 | } 70 | } 71 | 72 | void enableVerbose(File verboseFile) 73 | { 74 | this.verbose = true; 75 | this.verboseFile = verboseFile; 76 | } 77 | 78 | void extractFromFile(T)(string harFilename, T fileInfoCallback) 79 | { 80 | this.filenameForErrors = harFilename; 81 | auto harFile = File(harFilename, "r"); 82 | extract(harFile.byLine(Yes.keepTerminator), fileInfoCallback); 83 | } 84 | 85 | void extract(T, U)(T lineRange, U fileInfoCallback) 86 | { 87 | if (outputDir is null) 88 | outputDir = ""; 89 | 90 | lineNumber = 1; 91 | if (lineRange.empty) 92 | throw harFileException("file is empty"); 93 | 94 | auto line = lineRange.front; 95 | auto firstLineSpaceIndex = line.indexOf(' '); 96 | if (firstLineSpaceIndex <= 0) 97 | throw harFileException("first line does not start with a delimiter ending with a space"); 98 | 99 | auto delimiter = line[0 .. firstLineSpaceIndex + 1].idup; 100 | 101 | LfileLoop: 102 | for (;;) 103 | { 104 | auto fileInfo = parseFileLine(line[delimiter.length .. $], delimiter[0]); 105 | auto fullFileName = buildPath(outputDir, fileInfo.filename); 106 | fileInfoCallback(fullFileName, fileInfo); 107 | 108 | if (fullFileName[$-1] == '/') 109 | { 110 | if (!dryRun) 111 | extractMkdir(fullFileName, Yes.forEmptyDir); 112 | lineRange.popFront(); 113 | if (lineRange.empty) 114 | break; 115 | lineNumber++; 116 | line = lineRange.front; 117 | if (!line.startsWith(delimiter)) 118 | throw harFileException("expected delimiter after empty directory"); 119 | continue; 120 | } 121 | 122 | { 123 | auto dir = dirName(fileInfo.filename); 124 | if (dir.length > 0) 125 | { 126 | auto fullDir = buildPath(outputDir, dir); 127 | extractMkdir(fullDir, No.forEmptyDir); 128 | } 129 | } 130 | if (verbose) 131 | verboseFile.writefln("creating %s", fullFileName.formatFile); 132 | { 133 | File currentOutputFile; 134 | if (!dryRun) 135 | currentOutputFile = File(fullFileName, "w"); 136 | scope(exit) 137 | { 138 | if (!dryRun) 139 | currentOutputFile.close(); 140 | } 141 | for (;;) 142 | { 143 | lineRange.popFront(); 144 | if (lineRange.empty) 145 | break LfileLoop; 146 | lineNumber++; 147 | line = lineRange.front; 148 | if (line.startsWith(delimiter)) 149 | break; 150 | if (!dryRun) 151 | currentOutputFile.write(line); 152 | } 153 | } 154 | } 155 | } 156 | private HarException harFileException(T...)(string fmt, T args) if (T.length > 0) 157 | { 158 | return harFileException(format(fmt, args)); 159 | } 160 | private HarException harFileException(string msg) 161 | { 162 | return new HarException(msg, filenameForErrors, lineNumber); 163 | } 164 | 165 | FileProperties parseFileLine(const(char)[] line, char firstDelimiterChar) 166 | { 167 | if (line.length == 0) 168 | throw harFileException("missing filename"); 169 | 170 | const(char)[] filename; 171 | const(char)[] rest; 172 | if (line[0] == '"') 173 | { 174 | size_t afterFileIndex; 175 | filename = parseQuotedFilename(line[1 .. $], &afterFileIndex); 176 | rest = line[afterFileIndex .. $]; 177 | } 178 | else 179 | { 180 | filename = parseFilename(line); 181 | rest = line[filename.length .. $]; 182 | } 183 | for (;;) 184 | { 185 | rest = skipSpaces(rest); 186 | if (rest.length == 0 || rest == "\n" || rest == "\r" || rest == "\r\n" || rest[0] == firstDelimiterChar) 187 | break; 188 | throw harFileException("properties not implemented '%s'", rest); 189 | } 190 | return FileProperties(filename); 191 | } 192 | 193 | void checkComponent(const(char)[] component) 194 | { 195 | if (component.length == 0) 196 | throw harFileException("invalid filename, contains double slash '//'"); 197 | if (component == "..") 198 | throw harFileException("invalid filename, contains double dot '..' parent directory"); 199 | } 200 | 201 | inout(char)[] parseFilename(inout(char)[] line) 202 | { 203 | if (line.length == 0 || isEndOfFileChar(line[0])) 204 | throw harFileException("missing filename"); 205 | 206 | if (line[0] == '/') 207 | throw harFileException("absolute filenames are invalid"); 208 | 209 | size_t start = 0; 210 | size_t next = 0; 211 | while (true) 212 | { 213 | auto cIndex = next; 214 | auto c = decode!(Yes.useReplacementDchar)(line, next); 215 | if (c == replacementDchar) 216 | throw harFileException("invalid utf8 sequence"); 217 | 218 | if (c == '/') 219 | { 220 | checkComponent(line[start .. cIndex]); 221 | if (next >= line.length) 222 | return line[0 .. next]; 223 | start = next; 224 | } 225 | else if (isEndOfFileChar(c)) 226 | { 227 | checkComponent(line[start .. cIndex]); 228 | return line[0 .. cIndex]; 229 | } 230 | 231 | if (next >= line.length) 232 | { 233 | checkComponent(line[start .. next]); 234 | return line[0 ..next]; 235 | } 236 | } 237 | } 238 | 239 | inout(char)[] parseQuotedFilename(inout(char)[] line, size_t* afterFileIndex) 240 | { 241 | if (line.length == 0) 242 | throw harFileException("filename missing end-quote"); 243 | if (line[0] == '"') 244 | throw harFileException("empty filename"); 245 | if (line[0] == '/') 246 | throw harFileException("absolute filenames are invalid"); 247 | 248 | size_t start = 0; 249 | size_t next = 0; 250 | while(true) 251 | { 252 | auto cIndex = next; 253 | auto c = decode!(Yes.useReplacementDchar)(line, next); 254 | if (c == replacementDchar) 255 | throw harFileException("invalid utf8 sequence"); 256 | 257 | if (c == '/') 258 | { 259 | checkComponent(line[start .. cIndex]); 260 | start = next; 261 | } 262 | else if (c == '"') 263 | { 264 | checkComponent(line[start .. cIndex]); 265 | *afterFileIndex = next + 1; 266 | return line[0 .. cIndex]; 267 | } 268 | if (next >= line.length) 269 | throw harFileException("filename missing end-quote"); 270 | } 271 | } 272 | } 273 | 274 | private inout(char)[] skipSpaces(inout(char)[] str) 275 | { 276 | size_t i = 0; 277 | for (; i < str.length; i++) 278 | { 279 | if (str[i] != ' ') 280 | break; 281 | } 282 | return str[i .. $]; 283 | } 284 | 285 | private bool isEndOfFileChar(C)(const(C) c) 286 | { 287 | return c == '\n' || c == ' ' || c == '\r'; 288 | } 289 | 290 | struct FileProperties 291 | { 292 | const(char)[] filename; 293 | } 294 | 295 | auto formatDir(const(char)[] dir) 296 | { 297 | if (dir.length == 0) 298 | dir = "."; 299 | 300 | return formatQuotedIfSpaces(dir); 301 | } 302 | auto formatFile(const(char)[] file) 303 | in { assert(file.length > 0); } do 304 | { 305 | return formatQuotedIfSpaces(file); 306 | } 307 | 308 | // returns a formatter that will print the given string. it will print 309 | // it surrounded with quotes if the string contains any spaces. 310 | auto formatQuotedIfSpaces(T...)(T args) 311 | if (T.length > 0) 312 | { 313 | struct Formatter 314 | { 315 | T args; 316 | void toString(scope void delegate(const(char)[]) sink) const 317 | { 318 | import std.string : indexOf; 319 | bool useQuotes = false; 320 | foreach (arg; args) 321 | { 322 | if (arg.indexOf(' ') >= 0) 323 | { 324 | useQuotes = true; 325 | break; 326 | } 327 | } 328 | 329 | if (useQuotes) 330 | sink(`"`); 331 | foreach (arg; args) 332 | sink(arg); 333 | if (useQuotes) 334 | sink(`"`); 335 | } 336 | } 337 | return Formatter(args); 338 | } 339 | -------------------------------------------------------------------------------- /test/a.expected/a: -------------------------------------------------------------------------------- 1 | This is the file a -------------------------------------------------------------------------------- /test/a.har: -------------------------------------------------------------------------------- 1 | --- a 2 | This is the file a -------------------------------------------------------------------------------- /test/badHarFiles/emptyfile.har: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marler8997/har/c2f8858e0900204d74d7a768cd2a1641c96b5cb6/test/badHarFiles/emptyfile.har -------------------------------------------------------------------------------- /test/badHarFiles/nospace.har: -------------------------------------------------------------------------------- 1 | NOSPACE -------------------------------------------------------------------------------- /test/hartests.d: -------------------------------------------------------------------------------- 1 | import std.typecons: tuple; 2 | import std.array : appender; 3 | import std.format : format; 4 | import std.string : lineSplitter; 5 | import std.stdio; 6 | import archive.har; 7 | 8 | void main() 9 | { 10 | testError("", 1, "file is empty"); 11 | testError(" ", 1, "first line does not start with a delimiter ending with a space"); 12 | testError("a", 1, "first line does not start with a delimiter ending with a space"); 13 | testError("a ", 1, "missing filename"); 14 | testError("a ", 1, "missing filename"); 15 | testError("a \r", 1, "missing filename"); 16 | testError("a \n", 1, "missing filename"); 17 | testError("a /", 1, "absolute filenames are invalid"); 18 | testError("a /a", 1, "absolute filenames are invalid"); 19 | 20 | test("a a", ["a"]); 21 | test("a a\r", ["a"]); 22 | test("a a\r\n", ["a"]); 23 | test("a a\n", ["a"]); 24 | test("a a/", ["a/"]); 25 | testError("a a//", 1, "invalid filename, contains double slash '//'"); 26 | testError("a a/..", 1, "invalid filename, contains double dot '..' parent directory"); 27 | testError("a a/../", 1, "invalid filename, contains double dot '..' parent directory"); 28 | test("a a \n", ["a"]); 29 | test("--- a/b", ["a/b"]); 30 | test("--- a/b/", ["a/b/"]); 31 | testError("--- a/b/\na", 2, "expected delimiter after empty directory"); 32 | 33 | // 34 | // Quoted Filenames 35 | // 36 | testError("--- \"", 1, "filename missing end-quote"); 37 | testError("--- \"\"", 1, "empty filename"); 38 | testError("--- \"/", 1, "absolute filenames are invalid"); 39 | test("--- \"a a\"", ["a a"]); 40 | 41 | // 42 | // Extra delimiters 43 | // 44 | test("--- a --------\n", ["a"]); 45 | test("a a a\n", ["a"]); 46 | test("a a aaaa\n", ["a"]); 47 | test("--- \"a a\" --------\n", ["a a"]); 48 | 49 | // 50 | // UTF8 Tests 51 | // 52 | foreach(s; [tuple("- ", ""), tuple("- \"", "\"")]) 53 | { 54 | test (s[0] ~ "\xc3\xb1" ~ s[1], ["\xc3\xb1"]); 55 | testError(s[0] ~ "\xc3\x28" ~ s[1], 1, "invalid utf8 sequence"); 56 | testError(s[0] ~ "\xa0\xa1" ~ s[1], 1, "invalid utf8 sequence"); 57 | test (s[0] ~ "\xe2\x82\xa1" ~ s[1], ["\xe2\x82\xa1"]); 58 | testError(s[0] ~ "\xa0\xa1" ~ s[1], 1, "invalid utf8 sequence"); 59 | testError(s[0] ~ "\xe2\x28\xa1" ~ s[1], 1, "invalid utf8 sequence"); 60 | testError(s[0] ~ "\xe2\x82\x28" ~ s[1], 1, "invalid utf8 sequence"); 61 | test (s[0] ~ "\xf0\x90\x8c\xbc" ~ s[1], ["\xf0\x90\x8c\xbc"]); 62 | } 63 | } 64 | 65 | void testError(string text, size_t lineOfError, string error, size_t testLine = __LINE__) 66 | { 67 | auto extractor = HarExtractor(); 68 | extractor.filenameForErrors = format("%s_line_%s", __FILE__, testLine); 69 | extractor.dryRun = true; 70 | try 71 | { 72 | extractor.extract(text.lineSplitter, delegate(string fileFullName, FileProperties props) { 73 | }); 74 | assert(0, extractor.filenameForErrors); 75 | } 76 | catch(HarException e) 77 | { 78 | writefln("got exception: %s", e.msg); 79 | assert(e.msg == error); 80 | assert(e.line == lineOfError); 81 | } 82 | } 83 | void test(string text, string[] expectedFilenames, size_t testLine = __LINE__) 84 | { 85 | auto extractor = HarExtractor(); 86 | extractor.filenameForErrors = format("%s_line_%s", __FILE__, testLine); 87 | extractor.dryRun = true; 88 | auto extractedFiles = appender!(string[]); 89 | extractor.extract(text.lineSplitter, delegate(string fileFullName, FileProperties props) { 90 | extractedFiles.put(fileFullName); 91 | }); 92 | assert(expectedFilenames == extractedFiles.data); 93 | } 94 | -------------------------------------------------------------------------------- /test/newlines.expected/another_empty_file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marler8997/har/c2f8858e0900204d74d7a768cd2a1641c96b5cb6/test/newlines.expected/another_empty_file.txt -------------------------------------------------------------------------------- /test/newlines.expected/empty_file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marler8997/har/c2f8858e0900204d74d7a768cd2a1641c96b5cb6/test/newlines.expected/empty_file.txt -------------------------------------------------------------------------------- /test/newlines.expected/one_newline_file.txt: -------------------------------------------------------------------------------- 1 | this file has one newline 2 | -------------------------------------------------------------------------------- /test/newlines.expected/two_newlines_file.txt: -------------------------------------------------------------------------------- 1 | this file has two newlines 2 | 3 | -------------------------------------------------------------------------------- /test/newlines.har: -------------------------------------------------------------------------------- 1 | --- empty_file.txt 2 | --- one_newline_file.txt 3 | this file has one newline 4 | --- two_newlines_file.txt 5 | this file has two newlines 6 | 7 | --- another_empty_file.txt -------------------------------------------------------------------------------- /test_command_line_tool.d: -------------------------------------------------------------------------------- 1 | import std.path; 2 | import std.file; 3 | import std.stdio; 4 | 5 | import std.format : format; 6 | import std.process : spawnShell, wait; 7 | 8 | class SilentException : Exception 9 | { 10 | this() 11 | { 12 | super(null); 13 | } 14 | } 15 | 16 | auto quit() 17 | { 18 | return new SilentException(); 19 | } 20 | 21 | int main(string[] args) 22 | { 23 | try 24 | { 25 | return tryMain(args); 26 | } 27 | catch (SilentException) 28 | { 29 | return 1; 30 | } 31 | } 32 | 33 | int tryMain(string[] args) 34 | { 35 | // TODO: move to std.path 36 | version (Windows) 37 | string exeExtention = ".exe"; 38 | else 39 | string exeExtention; 40 | 41 | auto rootDir = __FILE_FULL_PATH__.dirName; 42 | auto outDir = rootDir.buildPath("out"); 43 | auto harExe = outDir.buildPath("har" ~ exeExtention); 44 | 45 | auto testDir = rootDir.buildPath("test"); // workaround https://issues.dlang.org/show_bug.cgi?id=6138 : we need absolutePath 46 | auto outTestDir = outDir.buildPath("test"); 47 | mkdirRecurse(outTestDir); 48 | foreach (entry; dirEntries(testDir, "*.har", SpanMode.shallow)) 49 | { 50 | auto file = entry.name; 51 | auto name = file.baseName.setExtension(".expected"); 52 | run(format("%s %s --dir=%s", harExe, file, outTestDir.buildPath(name))); 53 | auto expected = file.setExtension(".expected"); 54 | auto actual = outTestDir.buildPath(name); 55 | run(format("diff --brief -r %s %s", expected, actual)); 56 | } 57 | return 0; 58 | } 59 | 60 | void run(string command) 61 | { 62 | writefln("[SHELL] %s", command); 63 | auto pid = spawnShell(command); 64 | auto exitCode = wait(pid); 65 | writeln("--------------------------------------------------------------------------------"); 66 | if (exitCode != 0) 67 | { 68 | writefln("last command exited with code %s", exitCode); 69 | throw quit; 70 | } 71 | } 72 | --------------------------------------------------------------------------------