├── .gitignore ├── LICENSE.txt ├── README.md └── library_of_babel.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alec Lownes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Library Of Pybel 2 | ## About 3 | This is an open source python implementation of the [Library of Babel](https://libraryofbabel.info). 4 | ## Functionality 5 | ### Javascript Implementation 6 | The Javascript implementation of the Library of Babel can be found [here](https://cakenggt.github.io/Library-Of-Pybel/). The text and addresses generated will not be the same between the Python implementation and this one. The source code for this JS implementation is hosted in the gh-pages branch. There is a search box and an address selector. 7 | ### Python Implementation 8 | *Note: all numbers are started at 0, not 1. To find the first page of a book, look for page 0* 9 | 10 | Address format: `Hex_Value`:`Wall`:`Shelf`:`Volume`:`Page` 11 | 12 | Examples: 13 | * `98756SDH987S:2:3:14:345` 14 | * `HELLO:0:0:0:0` 15 | 16 | Run the file from the command line with an action argument. The following arguments are supported: 17 | * `--checkout ` Checks out a page of a book. Also displays the page's title. 18 | * `--fcheckout ` Does exactly the checkout does, but with address in the file. 19 | * `--search <'text'>` Does 3 searches for the text you input. 20 | * Page contains: Finds a page which contains the text. 21 | * Page only contains: Finds a page which only contains that text and nothing else. 22 | * Title match: Finds a title which is exactly this string. 23 | Mind the quotemarks.*For a title match, it will only match the first 25 characters. Addresses returned for title matches will need to have a page number added to the tail end, since they lack this.* 24 | * `--fsearch ` Does exactly the search does, but with text in the file. 25 | * `--file ` Dump search result into the file. 26 | * `--help` Prints help message. 27 | 28 | ## Explanation 29 | What was needed for this project was a way to generate seemingly random pages in a near-infinite address space which could also be searched for specific strings. 30 | 31 | I realized not early on that what I needed was not a reversible RNG, but in fact an encoding scheme to cleverly encode the page's text in the address of the book. Paired with a seeded RNG for shorter pages, I could reliably generate random pages, but also encode specific text into the page to be generated. 32 | 33 | To understand the encoding, you must think of the hex address of the book as a base-36 number and the text of the book as a base-29 number (26 letters plus space, comma, and period). The wall, shelf, volume, and page can be thought of as a base-10 number independent of the hex address. This base-10 number will be referred to as the location. 34 | 35 | Specifically, when text is searched for, that text is padded with a random amount of characters on it's front and back side, or in the case of the `Page only contains`, it's padded with spaces on it's back side. Then, a random number in the range of each location value is calculated. 36 | 37 | The page text is then converted from a string to a number. The location number is multiplied by a very large number and is then added to the page text number. Then the new page text number is converted into base-36, and that is the address. 38 | -------------------------------------------------------------------------------- /library_of_babel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import string 3 | import random 4 | import sys 5 | 6 | length_of_page = 3239 7 | loc_mult = pow(30, length_of_page) 8 | title_mult = pow(30, 25) 9 | 10 | #29 output letters: alphabet plus comma, space, and period 11 | #alphanumeric in hex address (base 36): 3260 12 | #in wall: 4 13 | #in shelf: 5 14 | #in volumes: 32 15 | #pages: 410 16 | #letters per page: 3239 17 | #titles have 25 char 18 | 19 | help_text = ''' 20 | --checkout - Checks out a page of a book. Also displays the page's title. 21 | 22 | --fcheckout Does exactly the search does, but with address in the file. 23 | 24 | --search <'text'> - Does 3 searches for the text you input: 25 | >Page contains: Finds a page which contains the text. 26 | >Page only contains: Finds a page which only contains that text and nothing else. 27 | >Title match: Finds a title which is exactly this string. For a title match, it will only match the first 25 characters. Addresses returned for title matches will need to have a page number added to the tail end, since they lack this. 28 | Mind the quotemarks. 29 | 30 | --fsearch - Does exactly the search does, but with text in the file. 31 | 32 | --file - Dump rusult into the file 33 | 34 | --help (or help, or nothing, or word salad) - Prints this message''' 35 | 36 | 37 | 38 | 39 | 40 | def text_prep(text): 41 | digs = set('abcdefghijklmnopqrstuvwxyz, .') 42 | prepared = '' 43 | for letter in text: 44 | if letter in digs: 45 | prepared += letter 46 | elif letter.lower() in digs: 47 | prepared += letter.lower() 48 | elif letter == '\n': 49 | prepared += ' ' 50 | return prepared 51 | 52 | 53 | 54 | def arg_check(input_array): 55 | coms = {'--checkout': [0, None], 56 | '--search': [0, None], 57 | '--test': [0, None], 58 | '--fsearch': [0, None], 59 | '--fcheckout': [0, None], 60 | '--file': [0, None]} 61 | try: 62 | for argv in input_array[1:]: 63 | if argv == '--checkout': 64 | coms['--checkout'][0] = 1 65 | coms['--checkout'][1] = input_array[input_array.index(argv) + 1] 66 | if argv == '--search': 67 | coms['--search'][0] = 1 68 | coms['--search'][1] = input_array[input_array.index(argv) + 1] 69 | if argv == '--test': 70 | coms['--test'][0] = 1 71 | if argv == '--fsearch': 72 | coms['--fsearch'][0] = 1 73 | coms['--fsearch'][1] = input_array[input_array.index(argv) + 1] 74 | if argv == '--fcheckout': 75 | coms['--fcheckout'][0] = 1 76 | coms['--fcheckout'][1] = input_array[input_array.index(argv) + 1] 77 | if argv == '--file': 78 | coms['--file'][0] = 1 79 | coms['--file'][1] = input_array[input_array.index(argv) + 1] 80 | if input_array[1:] is not False or len(input_array) == 1: 81 | in_coms = False 82 | for inp in input_array[1:]: 83 | if inp in coms: 84 | in_coms = True 85 | if not in_coms: 86 | print(help_text) 87 | except Exception as e: 88 | print('Due to \'' + str(e) + ' error\' read this:') 89 | print(help_text) 90 | sys.exit() 91 | return coms 92 | 93 | 94 | def filed(input_dict, text): 95 | if input_dict['--file'][0]: 96 | with open(input_dict['--file'][1], 'w') as file: 97 | file.writelines(text) 98 | print('\nFile '+ input_dict['--file'][1] + ' was writen') 99 | 100 | 101 | 102 | def test(): 103 | assert stringToNumber('a') == 0, stringToNumber('a') 104 | assert stringToNumber('ba') == 29, stringToNumber('ba') 105 | assert len(getPage('asaskjkfsdf:2:2:2:33')) == length_of_page, len(getPage('asasrkrtjfsdf:2:2:2:33')) 106 | assert 'hello kitty' == toText(int(int2base(stringToNumber('hello kitty'), 36), 36)) 107 | assert int2base(4, 36) == '4', int2base(4, 36) 108 | assert int2base(10, 36) == 'A', int2base(10, 36) 109 | test_string = '.................................................' 110 | assert test_string in getPage(search(test_string)) 111 | print ('Tests completed') 112 | 113 | 114 | def main(input_dict): 115 | if input_dict['--checkout'][0]: 116 | key_str = input_dict['--checkout'][1] 117 | text ='\nTitle: '+getTitle(key_str) + '\n'+getPage(key_str)+'\n' 118 | print(text) 119 | filed(input_dict, text) 120 | elif input_dict['--search'][0]: 121 | search_str = text_prep(input_dict['--search'][1]) 122 | key_str = search(text_prep(search_str)) 123 | text1 = '\nPage which includes this text:\n' + getPage(key_str)+'\n\n@ address '+key_str+'\n' 124 | only_key_str = search(search_str.ljust(length_of_page)) 125 | text2 = '\nPage which contains only this text:\n'+ getPage(only_key_str)+'\n\n@ address '+only_key_str+'\n' 126 | text3 = '\nTitle which contains this text:\n@ address '+ searchTitle(search_str) 127 | text = text1 + text2 + text3 128 | print(text) 129 | filed(input_dict, text) 130 | elif input_dict['--test'][0]: 131 | test() 132 | elif input_dict['--fsearch'][0]: 133 | file = input_dict['--fsearch'][1] 134 | with open(file, 'r') as f: 135 | lines = ''.join([line for line in f.readlines() if line != '\n']) 136 | search_str = text_prep(lines) 137 | key_str = search(search_str) 138 | text1 = '\nPage which includes this text:\n'+ getPage(key_str) +'\n\n@ address '+ key_str +'\n' 139 | only_key_str = search(search_str.ljust(length_of_page)) 140 | text2 = '\nPage which contains only this text:\n' + getPage(only_key_str) + '\n\n@ address '+ only_key_str +'\n' 141 | text3 = '\nTitle which contains this text:\n@ address ' + searchTitle(search_str) +'\n' 142 | text = text1 + text2 + text3 143 | print(text) 144 | filed(input_dict, text) 145 | elif input_dict['--fcheckout'][0]: 146 | file = input_dict['--fcheckout'][1] 147 | with open(file, 'r') as f: 148 | key_str = ''.join([line for line in f.readlines() if line != '\n'])[:-1] 149 | text ='\nTitle: '+getTitle(key_str) + '\n'+getPage(key_str)+'\n' 150 | print(text) 151 | filed(input_dict, text) 152 | 153 | def search(search_str): 154 | wall = str(int(random.random()*4)) 155 | shelf = str(int(random.random()*5)) 156 | volume = str(int(random.random()*32)).zfill(2) 157 | page = str(int(random.random()*410)).zfill(3) 158 | #the string made up of all of the location numbers 159 | loc_str = page + volume + shelf + wall 160 | loc_int = int(loc_str) #make integer 161 | an = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 162 | digs = 'abcdefghijklmnopqrstuvwxyz, .' 163 | hex_addr = '' 164 | depth = int(random.random()*(length_of_page-len(search_str))) 165 | #random padding that goes before the text 166 | front_padding = '' 167 | for x in range(depth): 168 | front_padding += digs[int(random.random()*len(digs))] 169 | #making random padding that goes after the text 170 | back_padding = '' 171 | for x in range(length_of_page-(depth+len(search_str))): 172 | back_padding += digs[int(random.random()*len(digs))] 173 | search_str = front_padding + search_str + back_padding 174 | hex_addr = int2base(stringToNumber(search_str)+(loc_int*loc_mult), 36) #change to base 36 and add loc_int, then make string 175 | key_str = hex_addr + ':' + wall + ':' + shelf + ':' + volume + ':' + page 176 | page_text = getPage(key_str) 177 | assert page_text == search_str, '\npage text:\n'+page_text+'\nstrings:\n'+search_str 178 | return key_str 179 | 180 | def getTitle(address): 181 | addressArray = address.split(':') 182 | hex_addr = addressArray[0] 183 | wall = addressArray[1] 184 | shelf = addressArray[2] 185 | volume = addressArray[3].zfill(2) 186 | loc_int = int(volume+shelf+wall) 187 | key = int(hex_addr, 36) 188 | key -= loc_int*title_mult 189 | str_36 = int2base(key, 36) 190 | result = toText(int(str_36, 36)) 191 | if len(result) < 25: 192 | #adding pseudorandom chars 193 | random.seed(result) 194 | digs = 'abcdefghijklmnopqrstuvwxyz, .' 195 | while len(result) < 25: 196 | result += digs[int(random.random()*len(digs))] 197 | elif len(result) > 25: 198 | result = result[-25:] 199 | return result 200 | 201 | def searchTitle(search_str): 202 | wall = str(int(random.random()*4)) 203 | shelf = str(int(random.random()*5)) 204 | volume = str(int(random.random()*32)).zfill(2) 205 | #the string made up of all of the location numbers 206 | loc_str = volume + shelf + wall 207 | loc_int = int(loc_str) #make integer 208 | hex_addr = '' 209 | search_str = search_str[:25].ljust(25) 210 | hex_addr = int2base(stringToNumber(search_str)+(loc_int*title_mult), 36) #change to base 36 and add loc_int, then make string 211 | key_str = hex_addr + ':' + wall + ':' + shelf + ':' + volume 212 | assert search_str == getTitle(key_str) 213 | return key_str 214 | 215 | def getPage(address): 216 | hex_addr, wall, shelf, volume, page = address.split(':') 217 | volume = volume.zfill(2) 218 | page = page.zfill(3) 219 | loc_int = int(page+volume+shelf+wall) 220 | key = int(hex_addr, 36) 221 | key -= loc_int*loc_mult 222 | str_36 = int2base(key, 36) 223 | result = toText(int(str_36, 36)) 224 | if len(result) < length_of_page: 225 | #adding pseudorandom chars 226 | random.seed(result) 227 | digs = 'abcdefghijklmnopqrstuvwxyz, .' 228 | while len(result) < length_of_page: 229 | result += digs[int(random.random()*len(digs))] 230 | elif len(result) > length_of_page: 231 | result = result[-length_of_page:] 232 | return result 233 | 234 | def toText(x): 235 | digs = 'abcdefghijklmnopqrstuvwxyz, .' 236 | if x < 0: sign = -1 237 | elif x == 0: return digs[0] 238 | else: sign = 1 239 | x *= sign 240 | digits = [] 241 | while x: 242 | digits.append(digs[x % 29]) 243 | x //= 29 244 | if sign < 0: 245 | digits.append('-') 246 | digits.reverse() 247 | return ''.join(digits) 248 | 249 | def stringToNumber(iString): 250 | digs = 'abcdefghijklmnopqrstuvwxyz, .' 251 | result = 0 252 | for x in range(len(iString)): 253 | result += digs.index(iString[len(iString)-x-1])*pow(29,x) 254 | return result 255 | 256 | def int2base(x, base): 257 | digs = string.digits + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 258 | if x < 0: sign = -1 259 | elif x == 0: return digs[0] 260 | else: sign = 1 261 | x *= sign 262 | digits = [] 263 | while x: 264 | digits.append(digs[x % base]) 265 | x //= base 266 | if sign < 0: 267 | digits.append('-') 268 | digits.reverse() 269 | return ''.join(digits) 270 | 271 | if __name__ == "__main__": 272 | input_dict= arg_check(sys.argv) 273 | main(input_dict) 274 | --------------------------------------------------------------------------------