grubeditor.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. #!/usr/bin/env bash
  2. #
  3. # grubeditor.sh -- conveniently edit grub{test}.cfg files by automating their
  4. # extraction with cbfstool and the user's editor of choice.
  5. #
  6. # Copyright (C) 2017 Zyliwax <zyliwax@protonmail.com>
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. # THIS BLOCK IS EXPERIMENTAL
  22. # Allow debugging by running DEBUG= ${0}.
  23. [[ "x${DEBUG+set}" = 'xset' ]] && set -v
  24. # -u kills the script if any variables are unassigned
  25. # -e kills the script if any function returns not-zero
  26. #set -u
  27. ##############
  28. # HELP COMMAND
  29. ##############
  30. show_help() {
  31. cat << HELPSCREEN
  32. $0 -- conveniently edit grub{test}.cfg files in Libreboot
  33. ROM image files by automating their extraction with cbfstool
  34. and the user's editor of choice.
  35. Usage:
  36. $0 [OPTIONS] [ROMFILE]
  37. Options:
  38. -h | --help: show usage help
  39. -r | --realcfg: generate grub.cfg instead of grubtest.cfg
  40. -i | --inplace: do not create a modified romfile, instead
  41. modify the existing file
  42. -e | --editor [/path/to/]editor: open the cfg file with
  43. /path/to/editor instead of the value of \$EDITOR
  44. -s | --swapcfg: swap grub.cfg and grubtest.cfg
  45. -d | --diffcfg: diff grub.cfg and grubtest.cfg
  46. -D | --differ [/path/to/]differ: use /path/to/differ instead
  47. of "diff", can be an interactive program like vimdiff
  48. -x | --extract: extract either grub.cfg or grubtest.cfg
  49. depending on -r option
  50. HELPSCREEN
  51. }
  52. # Version number of script
  53. geversion="0.2.0"
  54. # Define the list of available option in both short and long form.
  55. shortopts="hvrie:sdD:x"
  56. longopts="help,version,realcfg,inplace,editor:,swapcfgs,diffcfgs,differ:,extract"
  57. # Variables for modifying the program's operation
  58. edit_realcfg=0
  59. edit_inplace=0
  60. do_swapcfgs=0
  61. do_diffcfgs=0
  62. do_extract=0
  63. # Path to cbfstool, filled by detect_architecture
  64. # (Possible to provide explicitly and disclaim warranty?)
  65. cbfstool=""
  66. # Editor variables
  67. use_editor=""
  68. default_editor="vi"
  69. editor_rawarg=""
  70. # Differ variables
  71. use_differ=""
  72. default_differ="diff"
  73. differ_rawarg=""
  74. # Last but not least, the rom file itself
  75. romfile=""
  76. ############################
  77. #### PRIMARY FUNCTIONS #####
  78. ############################
  79. # The script effectively lives in a series of function definitions, which are
  80. # provided here before their calls to ensure that they have been declared.
  81. #
  82. # Please scroll to the bottom of the script to see how this cascade of
  83. # functions gets initiated.
  84. ################
  85. # OPTION SCRAPER
  86. ################
  87. get_options() {
  88. # Test for enhanced getopt.
  89. getopt --test > /dev/null
  90. if [[ $? -ne 4 ]]; then
  91. echo "Your environment lacks enhanced getopt, so you cannot run this script properly."
  92. exit 205
  93. fi
  94. # Parse the command line options based on the previously defined values.
  95. parsedopts=$(getopt --options $shortopts --longoptions $longopts --name "$0" -- "$@")
  96. if [[ $? -ne 0 ]]; then # getopt didn't approve of your arguments
  97. echo "Unrecognized options."
  98. exit 206
  99. fi
  100. # Use eval set to properly quote the arguments
  101. eval set -- "$parsedopts"
  102. # Interpret the arguments one-by-one until you run out.
  103. while [[ 1 ]]; do
  104. case "$1" in
  105. -h|--help)
  106. show_help
  107. # I return non-zero here just so nobody thinks we successfully edited grub{,test}.cfg
  108. exit 200
  109. ;;
  110. -v|--version)
  111. show_version
  112. # I return non-zero here just so nobody thinks we successfully edited grub{,test}.cfg
  113. exit 201
  114. ;;
  115. -r|--realcfg)
  116. edit_realcfg=1
  117. shift
  118. ;;
  119. -i|--inplace)
  120. edit_inplace=1
  121. shift
  122. ;;
  123. -e|--editor)
  124. editor_rawarg="$2"
  125. shift 2
  126. ;;
  127. -s|--swapcfgs)
  128. do_swapcfgs=1
  129. shift
  130. ;;
  131. -d|--diffcfgs)
  132. do_diffcfgs=1
  133. shift
  134. ;;
  135. -D|--differ)
  136. differ_rawarg="$2"
  137. shift 2
  138. ;;
  139. -x|--extract)
  140. do_extract=1
  141. shift
  142. ;;
  143. --)
  144. # Stop interpreting arguments magically.
  145. shift
  146. break
  147. ;;
  148. *)
  149. echo "Something went wrong while interpreting the arguments!"
  150. echo "I hit \"$1\" and don't know what to do with it."
  151. exit 209
  152. ;;
  153. esac
  154. done
  155. # The only option remaining should be the input file. Ensure it was
  156. # provided before proceeding with the rest of the script and capture it if
  157. # it was.
  158. if [[ $# -ne 1 ]]; then
  159. echo "You have specified multiple (or no) input files, which is unsupported."
  160. echo "Please rerun this command with just a single filename to remove any chance for ambiguity."
  161. exit 210
  162. fi
  163. romfile="$1"
  164. }
  165. determine_architecture() {
  166. # The cbfstool in _util is packed only for i686, x86_64, and armv7l. This
  167. # procedure is necessary for this process to not fail. After this process
  168. # is over, the variable $cbfstool gets filled with the appropriate value
  169. # for use by the rest of the script.
  170. arch="$(uname -m)"
  171. case "$arch" in
  172. armv7l|i686|x86_64)
  173. echo "Supported architecture \"$arch\" detected. You may proceed."
  174. cbfstool="${0%/*}/cbfstool/$arch/cbfstool"
  175. ;;
  176. *)
  177. echo "Unsupported architecture \"$arch\" detected! You may not proceed."
  178. exit 230
  179. ;;
  180. esac
  181. }
  182. determine_operation() {
  183. if [[ $do_swapcfgs -eq 1 ]]; then
  184. swap_configs
  185. exit $?
  186. elif [[ $do_diffcfgs -eq 1 ]]; then
  187. diff_configs
  188. exit $?
  189. elif [[ $do_extract -eq 1 ]]; then
  190. extract_config
  191. exit $?
  192. else
  193. edit_config
  194. exit $?
  195. fi
  196. }
  197. ################
  198. # VERSION SHOWER
  199. ################
  200. show_version() {
  201. echo "$0 $geversion"
  202. }
  203. ##########################
  204. # EXTERNAL COMMAND FINDERS
  205. ##########################
  206. find_differ() {
  207. found_differ=0
  208. if [[ -n "$differ_rawarg" ]]; then
  209. which "$differ_rawarg" &> /dev/null
  210. if [[ $? -eq 0 ]]; then
  211. echo "Using differ \"$differ_rawarg\"..."
  212. use_differ="$differ_rawarg"
  213. found_differ=1
  214. else
  215. echo "The provided \"$differ_rawarg\" is not a valid command!"
  216. echo "Defaulting to $default_differ..."
  217. use_differ="$default_differ"
  218. fi
  219. fi
  220. if [[ $found_differ -eq 1 ]]; then
  221. return
  222. else
  223. echo "Defaulting to $default_differ..."
  224. use_differ="$default_differ"
  225. fi
  226. }
  227. find_editor() {
  228. found_editor=0
  229. if [[ -n "$editor_rawarg" ]]; then
  230. which "$editor_rawarg" &> /dev/null
  231. if [[ $? -eq 0 ]]; then
  232. echo "Using editor \"$editor_rawarg\"..."
  233. use_editor="$editor_rawarg"
  234. found_editor=1
  235. else
  236. echo "The provided \"$editor_rawarg\" is not a valid command!"
  237. echo "Defaulting to $default_editor..."
  238. use_editor="$default_editor"
  239. fi
  240. fi
  241. if [[ $found_editor -eq 1 ]]; then
  242. return
  243. else
  244. if [[ -n "$EDITOR" ]]; then
  245. which "$EDITOR" &> /dev/null
  246. if [[ $? -ne 0 ]]; then
  247. echo "Your \$EDITOR is defined as $EDITOR, but is not a valid command!"
  248. echo "(This is bad. I highly suggest fixing this in your ~/.bashrc.)"
  249. echo "Defaulting to $default_editor..."
  250. use_editor="$default_editor"
  251. else
  252. echo "Using your defined \$EDITOR \"$EDITOR\"..."
  253. use_editor="$EDITOR"
  254. fi
  255. else
  256. echo "\$EDITOR blank, defaulting to $default_editor..."
  257. use_editor="$default_editor"
  258. fi
  259. fi
  260. }
  261. #######################
  262. # FILE NAMING FUNCTIONS
  263. #######################
  264. random_tempcfg() {
  265. # Inputs:
  266. # $1 is a descriptive label for the file
  267. # $2 is directory (becomes /tmp if not set)
  268. [[ -n "$1" ]] && label="$1" || label="tempfile"
  269. [[ -n "$2" ]] && savedir="${2%/}" || savedir="/tmp"
  270. # Hardcoded string size for multiple good reasons (no processing weird
  271. # input, prevent malicious overflows, etc.)
  272. size=5
  273. # Loop forever until a free filename is found.
  274. while [[ 1 ]]; do
  275. # Read data from /dev/urandom and convert into random ASCII strings.
  276. rand="$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w $size | head -n 1)"
  277. # Build a complete filename with a hardcoded extension.
  278. possible="$savedir/${label}_${rand}.tmp.cfg"
  279. # See if file doesn't exist and return it as string or keep going.
  280. if [[ ! -f "$possible" ]]; then
  281. echo "$possible"
  282. break
  283. fi
  284. done
  285. }
  286. modified_romfile() {
  287. # Inputs:
  288. # $1 is the path to the file, optional
  289. # $2 provides an optional description of the modification
  290. [[ -n "$1" ]] && fullname="$1" || fullname=""
  291. [[ -n "$2" ]] && operation="$2" || operation="altered"
  292. # Attempt to extract an extension from the file (it's probably .rom but
  293. # can't assume so). $extension will not include prefixing dot, just the
  294. # extension itself!
  295. if [[ "$fullname" = *.* ]]; then
  296. extension="${fullname##*.}"
  297. else
  298. extension=""
  299. fi
  300. # Break up full path into containing directory and raw filename, providing
  301. # an attempt at graceful failure if they are absent.
  302. if [[ -z "$fullname" ]]; then
  303. dirname="/tmp"
  304. filename="grubedited"
  305. else
  306. dirname="$(dirname "$fullname")"
  307. if [[ -z "$extension" ]]; then
  308. filename="$(basename "$fullname")"
  309. else
  310. filename="$(basename "$fullname" ".$extension")"
  311. fi
  312. fi
  313. # Loop forever until a free filename is found.
  314. while [[ 1 ]]; do
  315. # Grab the current date.
  316. now="$(date +"%Y%m%d_%H%M%S")"
  317. # Build a complete filename with a hardcoded extension.
  318. possible="$dirname/${filename}-${now}-${operation}.${extension}"
  319. # See if file doesn't exist and return it as string or keep going.
  320. if [[ ! -f "$possible" ]]; then
  321. echo "$possible"
  322. break
  323. fi
  324. done
  325. }
  326. ###########################
  327. # PRIMARY PROGRAM FUNCTIONS
  328. ###########################
  329. swap_configs() {
  330. # Procedure:
  331. # 1. Call cbfstool twice, once each to extract grub.cfg and grubtest.cfg.
  332. # 2. If --inplace not specified, copy $romfile to a new file and implement
  333. # remaining steps on this copy. Otherwise, implement remaining steps on
  334. # $romfile.
  335. # 3. Call cbfstool twice, once each to delete grub.cfg and grubtest.cfg
  336. # from romfile.
  337. # 4. Call cbfstool twice, once to embed grubtest.cfg as grub.cfg into
  338. # romfile and again to embed grub.cfg as grubtest.cfg into romfile.
  339. # 5. Delete the extracted grub.cfg and grubtest.cfg files.
  340. # 6. You're done!
  341. # Extract config files from provided romfile.
  342. real2test="$(random_tempcfg "real2test")"
  343. test2real="$(random_tempcfg "test2real")"
  344. "$cbfstool" "$romfile" extract -n grub.cfg -f "$real2test"
  345. "$cbfstool" "$romfile" extract -n grubtest.cfg -f "$test2real"
  346. # Determine whether to edit inplace or make a copy.
  347. if [[ $edit_inplace -eq 1 ]]; then
  348. outfile="$romfile"
  349. else
  350. outfile="$(modified_romfile "$romfile" "swapped")"
  351. cp "$romfile" "$outfile"
  352. echo "Saving modified romfile to $outfile"
  353. fi
  354. # Since we are swapping the configs, we must modify their "load test config"
  355. # options or else they will simply reference themselves, rendering the user
  356. # unable to (easily) load the other config.
  357. sed -i -e 's/Load test configuration (grubtest.cfg)/Load test configuration (grub.cfg)/' "$real2test"
  358. sed -i -e 's/configfile \/grubtest.cfg/configfile \/grub.cfg/' "$real2test"
  359. sed -i -e 's/Load test configuration (grub.cfg)/Load test configuration (grubtest.cfg)/' "$test2real"
  360. sed -i -e 's/configfile \/grub.cfg/configfile \/grubtest.cfg/' "$test2real"
  361. # Remove config files from the output file.
  362. "$cbfstool" "$outfile" remove -n grub.cfg
  363. "$cbfstool" "$outfile" remove -n grubtest.cfg
  364. # Embed new configs into the output file.
  365. "$cbfstool" "$outfile" add -t raw -n grub.cfg -f "$test2real"
  366. "$cbfstool" "$outfile" add -t raw -n grubtest.cfg -f "$real2test"
  367. # Delete the tempfiles.
  368. rm "$test2real" "$real2test"
  369. }
  370. diff_configs() {
  371. # Procedure:
  372. # 1. Call cbfstool twice, once to extract grub.cfg and grubtest.cfg.
  373. # 2. Execute $use_differ grub.cfg grubtest.cfg #.
  374. # 3. Delete the extracted grub.cfg and grubtest.cfg files.
  375. # 4. You're done!
  376. # Determine the differ command to use.
  377. find_differ
  378. grubcfg="$(random_tempcfg "grubcfg")"
  379. grubtestcfg="$(random_tempcfg "grubtestcfg")"
  380. # Extract config files from provided romfile.
  381. "$cbfstool" "$romfile" extract -n grub.cfg -f "$grubcfg"
  382. "$cbfstool" "$romfile" extract -n grubtest.cfg -f "$grubtestcfg"
  383. # Run the differ command with real as first option, test as second option.
  384. "$use_differ" "$grubcfg" "$grubtestcfg"
  385. # Delete the temporary copies of the configuration files.
  386. rm "$grubcfg"
  387. rm "$grubtestcfg"
  388. }
  389. edit_config() {
  390. # Procedure:
  391. # 1. If --realcfg specified, set $thisconfig to "grub.cfg". Otherwise,
  392. # set $thisconfig to "grubtest.cfg".
  393. # 2. Call cbfstool once to extract $thisconfig from $romfile.
  394. # 3. Run $use_editor $thisconfig.
  395. # 4. If $use_editor returns zero, proceed with update procedure:
  396. # 5. Call cbfstool once to extract $thisconfig from $romfile.
  397. # 6. Quietly diff the extracted file with the edited file. If diff returns
  398. # zero, take no action: warn the user that the files were the same, delete
  399. # both files, then skip the remaining steps (you're done)! Otherwise, the
  400. # files are different and you must continue with the update procedure.
  401. # 7. If --inplace not specified, copy $romfile to a new filename and
  402. # implement remaining steps on this copy. Otherwise, implement remaining
  403. # steps on $romfile.
  404. # 8. Call cbfstool once to delete internal pre-update $thisconfig from
  405. # the rom file.
  406. # 9. Call cbfstool once to embed the updated $thisconfig that was just
  407. # edited into the rom file.
  408. # 10. Alert the user of success (either explicitly or by not saying
  409. # anything, either way return zero).
  410. # 11. You're done!
  411. # Determine the editor command to use.
  412. find_editor
  413. # Determine whether we are editing the real config or the test config.
  414. if [[ $edit_realcfg -eq 1 ]]; then
  415. thisconfig="grub.cfg"
  416. else
  417. thisconfig="grubtest.cfg"
  418. fi
  419. # Extract the desired configuration file from the romfile.
  420. tmp_editme="$(random_tempcfg "${thisconfig%.cfg}")"
  421. "$cbfstool" "$romfile" extract -n "$thisconfig" -f "$tmp_editme"
  422. # Launch the editor!
  423. "$use_editor" "$tmp_editme"
  424. # Did the user commit the edit?
  425. if [[ $? -eq 0 ]]; then
  426. # See if it actually changed from what exists in the cbfs.
  427. tmp_refcfg="/tmp/${thisconfig%.cfg}_ref.cfg"
  428. "$cbfstool" "$romfile" extract -n "$thisconfig" -f "$tmp_refcfg"
  429. # Diff the files as quietly as possible.
  430. diff -q "$tmp_editme" "$tmp_refcfg" &> /dev/null
  431. if [[ $? -ne 0 ]]; then
  432. # The files differ, so it won't be frivolous to update the config.
  433. # See if the user wants to edit the file in place.
  434. # (This code should really be genericized and placed in a function
  435. # to avoid repetition.)
  436. if [[ $edit_inplace -eq 1 ]]; then
  437. outfile="$romfile"
  438. else
  439. if [[ $edit_realcfg -eq 1 ]]; then
  440. op="realcfg"
  441. else
  442. op="testcfg"
  443. fi
  444. outfile="$(modified_romfile "$romfile" "${op}_edited")"
  445. cp "$romfile" "$outfile"
  446. echo "Saving modified romfile to $outfile"
  447. fi
  448. # Remove the old config, add in the new one.
  449. "$cbfstool" "$outfile" remove -n "$thisconfig"
  450. "$cbfstool" "$outfile" add -t raw -n "$thisconfig" -f "$tmp_editme"
  451. else
  452. echo "No changes to config file. Not updating the ROM image."
  453. fi
  454. # We are done with the config files. Delete them.
  455. rm "$tmp_editme"
  456. rm "$tmp_refcfg"
  457. fi
  458. }
  459. extract_config() {
  460. # This simply extracts a given config and responds to
  461. # the -r flag.
  462. # Determine whether we are extracting the real config or the test config.
  463. if [[ $edit_realcfg -eq 1 ]]; then
  464. thisconfig="grub.cfg"
  465. else
  466. thisconfig="grubtest.cfg"
  467. fi
  468. # Extract to a unique filename.
  469. tmp_extractme="$(random_tempcfg "${thisconfig%.cfg}" "$(dirname "$romfile")")"
  470. "$cbfstool" "$romfile" extract -n "$thisconfig" -f "$tmp_extractme"
  471. err=$?
  472. # Determine if cbfstool reported success.
  473. if [[ $err -ne 0 ]]; then
  474. echo "cbfstool reported an error ($err). If it succeeded anyway, check $tmp_extractme."
  475. else
  476. echo "Extracted $thisconfig from $romfile to file $tmp_extractme."
  477. fi
  478. }
  479. #################################
  480. #### PRIMARY EXECUTION FLOW #####
  481. #################################
  482. # Run through the primary function cascade.
  483. get_options $@
  484. determine_architecture
  485. determine_operation