Batcera.linux: ファイル名扱いあれこれ

前回まで魔改造を一気に進めてきましたが、ここで一旦クールダウン。
今後のハックに先立って準備しておかなければならないところありまして、
今回はファイル(というかpath)の扱いについて解説を入れときましょうか。
ここでもいくらか機能強化入れてますが、大したものでもないので
今回は無料にしときます。

このあたりの処理について、C#(というか.NET)ならちゃんとした
ディレクトリ単位で扱える公式機能が用意されているのですが、
今回はC/C++なので頑張って通常の文字列で扱っていくことになります。
もちろんそれなりの機能としてファンクションには起こされて
いるわけですが、パッケージ毎に独自仕様で実装されているため、
それぞれの挙動を個別に把握しなおしておく必要があります。
(思い込みで適当に扱ってるとバグんぞ)

EmulationStation編

es-core/src/utils/FileSystemUtil.cpp

全てnamespace Utils::FileSystem 内のファンクションで
直接呼び出せます。

多くのファンクションでは、自動的な正規化が行われます。
・ 先頭の \\?\ は除去
・ \ は / に変換
・ だぶった / をひとまとめ
・ 最後の / は除去

一部のファンクションでは、 :/ で始まるpathを
EmulationStation内のリソースを指す記述として扱います。

注意点として
・4095バイトを超える絶対pathや32階層を超えるディレクトリは
 正常動作しません(というか脆弱性あり)
・Utils::Time::DateTimeの既定値は内部表現的に一貫していません。
 getTime()やsetTimeStruct()は1900年1月1日を指しますが
 getIsoString()は0年0月0日を指します。

changeExtension(_path, extension)
_pathから拡張子部分を除去し、extentionが空文字以外であれば
"."+extension を繋げて返します。
単純に . の位置だけみてるので、親ディレクトリ側に . がある
拡張子なしファイルは親ディレクトリ側の . から差し替えられます。

combine(const std::string& _path, const std::string& filename)
_pathとfilenameを / 区切りで接続して返します。
filenameが /.. で始まるときだけ (.. で始まってても対象外)
_pathからの逆流を解決します。

copyFile(src, dst)
ファイルをsrcからdstに複写します。
srcが存在しないとき、何もせずtrueを返します。
dst側の不足ディレクトリは自動的に作成されます。
srcやdstのfopenに失敗したときはfalseを返しますが、
途中で発生するエラーは無視され、trueを返します。

createDirectory (_path)
ディレクトリを作成します。
失敗したときはその親ディレクトリから作成を試みます。
その成否をboolで返します。
処理するまでもなく既にないときはtrueが返ります。

createRelativePath (_path, _relativeTo, _allowHome)
_allowHomeがfalseのときは_relativeToから、
trueのときはgetHomePath()から_pathへの相対pathを作ります。
・_relativeToが空
・_pathが ./ で始まる
・removeCommonPath()に_pathと_relativeToを渡して処理できない
・_allowHomeがfalseで_pathが ~/ から始まる
・_allowHomeがtrueで_pathが / 以外で始まる
といったケースでは_pathをそのまま返します。
removeCommonPath()で処理されたときは、
./ にその結果を繋げて返します。
_allowHomeがtrueのときはgetHomePath()から_pathへの
相対位置を特定し、 ~/ に繋げて返します。

createRelativePath_undot (_path, _relativeTo, _allowHome)
(NullPopPoCustom追加)
createRelativePath()の結果から拡張子部分を . 含めて除去します。
createRelativePath()の結果をchangeExtension()に渡して
正常動作しないケースあるので代替動作として書いてます。

deleteDirectoryFiles(path, deleteDirectory = false)
指定ディレクトリ内のファイルやディレクトリを全て削除します。
deleteDirectoryがtrueのときは指定ディレクトリも削除します。

exists (_path)
ファイルまたはディレクトリの存在を調べ
有無をboolで返します。

getAbsolutePath (_path, _base = getCWDPath())
_pathを正規化された絶対pathで返します。

getCanonicalPath (_path)
:/ で始まる_pathはそのまま返します。
 他については、正規化された絶対pathから . と .. を
解決して返します。

getCWDPath ()
現在の作業ディレクトリを正規化し、文字列で返します。

getDirContent (_path, _recursive = false, includeHidden = false)
_pathがディレクトリでないときは空のファイル名listを返します。
ディレクトリのときは中身のファイルやディレクトリ名を
正規化された_path+'/'に接続してファイル名listに列挙します。
includeHiddenがfalseのときは隠しファイルを除外します。
_recursiveがtrueのときはサブディレクトリも再起処理します。

getDirectoryFiles(_path)
_pathがディレクトリでないときは空のファイル情報listを返します。
(ファイル情報にはファイル名,隠しフラグ,ディレクトリフラグが含まれます)
ディレクトリのときは中身のファイルやディレクトリについて
ファイル情報listに列挙します。

getEscapedPath (_path)
正規化の上、シェルでそのまま使えない_pathを
エスケープ処理して返します。
Windowsでは無差別に / が \ に変換され、 "" で括られます。

getExtension (_path, withPoint = true)
_pathから拡張子部分を抽出して返します。
withPointで . を抽出対象に含めるか指定します。
単純に . の位置だけみてるのと、複数の . があるときは
後方からの抽出となるため getStem() とは対になりません。
また、親ディレクトリ側に . がある拡張子なしファイルは
親ディレクトリ側の . から抽出されます。

getFileCrc32(filename)
ファイルのCRCハッシュを16進数にして文字列で返します。
ファイルが読めないときは空文字列を返します。

getFileCreationDate(_path)
ファイル作成日時をUtils::Time::DateTime型で返します。
情報取得できないときは既定値となります。

getFileMd5(filename)
ファイルのMD5ハッシュを16進数にして文字列で返します。
ファイルが読めないときは空文字列を返します。

getFileName (_path)
_pathから親ディレクトリ部分を除去して返します。  
ディレクトリ区切りがないときは_pathをそのまま返します。

getFileModificationDate(_path)
ファイル変更日時をUtils::Time::DateTime型で返します。
情報取得できないときは既定値となります。

getFileSize(_path)
ファイルサイズを64bit uintで返します。
ファイルがないときは0が返ります。

getGenericPath (_path)
_path文字列を正規化して返します。

getParent (_path)
_pathの親ディレクトリを返します。
親ディレクトリが存在しないときは空文字列を返します。
(相対pathは正常動作しないので別途getCanonicalPath()で)

getPathList (_path)
正規化された_pathを / でバラし、各要素を文字列vectorに
列挙して返します。

std::string getPdfTempPath()
PDF用のテンポラリディレクトリのpathを文字列で返します。

getPreferredPath (_path)
Windows以外では_pathをそのまま返します。
Windowsでは / を \ に変換した文字列を返します。

getStem (_path)
_pathから親ディレクトリ部分と拡張子部分を除去して返します。  
 . が複数あるときは先頭側から除去します。  

getTempPath()
テンポラリディレクトリのpathを文字列で返します。

isAbsolute (_path)
_pathが絶対pathかを調べ、boolで返します。
:/ で始まるpathも絶対path扱いとします。

isAudio (_path)
拡張子が .mp3 .wav .ogg .flac .mod .xm .stm .s3m .far .it .669 .mtm
かを調べてboolで返します。
大小文字は区別しません。

isDirectory (_path)
_pathがディレクトリか調べてboolで返します。
シンボリックリンクのときはその参照先で調べます。

isHidden (_path)
_pathが隠しファイルか調べてboolで返します。

isImage (_path)
拡張子が .jpg .png .jpeg .gif かを調べてboolで返します。
大小文字は区別しません。

isRegularFile (_path)
_pathが通常のファイルか調べてboolで返します。

isSymlink (_path)
_pathがシンボリックリンクか調べてboolで返します。

isVideo (_path)
拡張子が .mp4 .avi .mkv .webm かを調べてboolで返します。
大小文字は区別しません。

kiloBytesToString(size)
指定のサイズに応じてKB,MB,GB,TB単位の文字列で返します。

megaBytesToString(size)
指定のサイズに応じてMB,GB,TB単位の文字列で返します。

removeCommonPath (_path, _common, &_contains)
_pathの先頭が_commonを含むとき、その_common部分を切除して返す
…という趣旨のようだが、ちょっと挙動の怪しげなファンクション。
・_pathと_commonが一致するときは処理しない
・_commonが / で終端しないときはその次に / がある前提で処理
・処理の有無を_containsに書き出す
・処理しないときは_pathをそのまま返す
つまり _path="a/" _common="a/" では処理されず a/ が返り、
_path="a/" _common="a" だと処理対象になって空文字列が返ると。

resolveRelativePath(_path, _relativeTo, _allowHome)
相対pathを解決します。
_pathが空のときは、空文字列が返ります。
_pathが . のときは、_relativeToを正規化して返します。
./ で始まる_pathは、_relativeToと_pathを合成し、正規化して返します。
~/ で始まる_pathは、_allowHomeがtrueのときは
ホームディレクトリと_pathを合成し、正規化して返します。
(注) いくつか不足点あり
・./ で始まらない相対pathは処理されません
・先頭以外の . や .. は処理されません

resolveSymlink (_path)
シンボリックリンクを解決します。
WindowsではGetFinalPathNameByHandle()丸投げ。
その他はreadlink()丸投げ。
解決可能ならその結果を正規化して返し、
できなかったら空文字列を返します。

removeDirectory (_path)
空のディレクトリを削除します。
その成否をboolで返します。
処理するまでもなく既にないときはtrueが返ります。

removeFile (_path)
ファイルまたは空のディレクトリを削除します。
その成否をboolで返します。
処理するまでもなく既にないときはtrueが返ります。

readAllText(fileName)
ファイルの内容を文字列で返します。
エラーは無視されます。

writeAllText(fileName, text)
ファイルの内容を文字列で差し替えます。
エラーは無視されます。

renameFile(src, dst, overWrite = true)
ファイル名をsrcからdstに変更します。
srcが存在しないとき、何もせずtrueを返します。
dstが存在してoverWriteがtrueのとき、dstは削除されます。
変更を試み、成否をboolで返します。

ハック済ソース

以上、NullPopPoCustom38向けの作業ブランチ

RetroArch編

EmulationStationはC++なのでstd::string単位の処理でしたが、
RetroArchは素のCなので単なるchar配列またはポインタで、
結果も指定の配列に書き出すスタイル。
手抜き実装も多く、取扱いには細心の注意が要求されるのであしからず。
なお、ここで挙げた以外にもいくつか関連ファンクションありますが、
未調査のため省略してます。

RetroArchでは共通機能として特殊なpath指定があります。
(実装側が対応してなければ無効ではありますが)
・アプリケーションディレクトリからの相対参照
 :/ 以降に相対pathで繋げる
・.zip .7z .apk 圧縮書庫内の特定のファイルを参照
 圧縮書庫path#対象ファイルpathで示されますが、
 圧縮書庫path自体に .zip# .7z# .apk#
 が含まれるときは誤認識するのであしからず。

なお、注意点として
・PATH_MAX_LENGTH-1バイトを超えるpathは正常動作しません。
 (途切れます)

libretro-common/file/file_path.c

fill_dated_filename(out_filename, ext, size)
RetroArch-%m%d-%H%M%S 形式の現在日時+extを
out_filenameからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。

fill_pathname(out_path, in_path, replace, size)
in_pathから拡張子部分を除外した内容に
replaceの内容(拡張子に限らない)を継ぎ足して
out_pathからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。
out_pathとin_pathは同じ場所を指定でき、
同バッファで連続処理ができます。

fill_pathname_abbreviate_special(out_path, in_path, size)
in_pathがホームディレクトリやアプリケーションディレクトリ内を
指しているときは省略形に変換し、
out_pathからsizeバイトのバッファに書き出します。
変換後の文字列が短くなる前提なら
out_dirとin_pathは同じ場所を指定できるはずたぶん。

fill_pathname_abbreviated_or_relative(out_path, in_refpath,  in_path, size)
絶対in_pathあるいはref_pathからの相対in_pathを展開し、
ref_path,ホーム,アプリケーションディレクトリからの相対pathに再変換し、
out_pathからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。
out_pathとin_refpathまたはin_pathは同じ場所を指定できます。

fill_pathname_application_path(s, len)
アプリケーションの実行ファイルを指すpathを
sからlenバイトのバッファに書き込みます。

fill_pathname_application_dir(s, len)
アプリケーションの実行ファイルを含むディレクトリpathを
sからlenバイトのバッファに書き込みます。

fill_pathname_base(out, in_path, size)
ディレクトリを抜いたin_pathを
outからsizeバイトのバッファに書き戻し、
結果文字列のバイト数を返します。
outとin_pathは同じ場所を指定できます。

fill_pathname_basedir(out_dir, in_path, size)
in_pathのディレクトリ部分を抽出して
outからsizeバイトのバッファに書き戻しますが、
in_pathが1文字以内のときはその内容を
outに書き出すだけであることに注意。
out_dirとin_pathは同じ場所を指定でき、
同バッファで連続処理ができます。

fill_pathname_dir(in_dir, in_basename, replace, size)
in_dirで指定したディレクトリから
ディレクトリを抜いたin_basename+replaceを接続し、
(replaceってあるけどreplaceじゃない点に注意)
in_dirからsizeバイトのバッファに書き戻し、
結果文字列のバイト数を返します。

fill_pathname_expand_special(out_path, in_path, size)
in_path先頭の ~ はホームディレクトリを展開し、
in_path先頭の : はアプリケーションディレクトリを展開し、
out_pathからsizeバイトのバッファに書き出します。
ここで、in_pathの2文字目は / であることを想定して
読み飛ばされることに注意。
返り値は結果文字列ではなく、in_pathから展開された
符号部分が除去された値であることにも注意。
なお、out_pathとin_pathは重複不可。

fill_pathname_home_dir(s, len)
ホームディレクトリpathを
sからlenバイトのバッファに書き込みます。

fill_pathname_join(out_path, dir, path, size)
(非推奨)
dirをディレクトリとしてpathを接続し、
out_pathからsizeバイトのバッファに書き出し、
結果文字列のバイト数を返します。
dirが / で終端されていないとき、
fill_pathname_slash()と同様の問題あります。
out_pathとdirは同じ場所を指定でき、
同バッファで連続処理ができます。
(以下NullPopPoCustom修正)
・dirがNULLのとき、out_pathを空文字列から始めます。
・pathがNULLのとき、dirのみ処理して返します。

fill_pathname_join_special(out_path, dir, path, size)
非推奨のfill_pathname_join()に代わるもののようで
なんか趣旨が謎なファンクション。
やってることは fill_pathname_join() と同じようで
fill_pathname_slash()と同様の問題もあって、
さらにdirに / が含まれないケースでも
バッファ終端保護されなくて余計ヤバいだけに見える。
(以下NullPopPoCustom修正)
・dirがNULLのとき、out_pathを空文字列から始めます。
・pathがNULLのとき、dirのみ処理して返します。

fill_pathname_join_special_ext(out_path, dir, path, last, ext, size)
dirをディレクトリとしてpathとlastとextを接続し、
out_pathからsizeバイトのバッファに書き出し、
結果文字列のバイト数を返します。
fill_pathname_join()と同様の問題あります。
out_pathとdirは同じ場所を指定でき、
同バッファで連続処理ができます。
(以下NullPopPoCustom修正)
・dirがNULLのとき、out_pathを空文字列から始めます。
・pathがNULLのとき、dirのみ処理して返します。
・lastやextがNULLのとき、接続処理を省略します。

fill_pathname_parent_dir(out_dir, in_dir, size)
in_dirの親ディレクトリを抽出し、
out_dirからsizeバイトのバッファに書き出します。
親ディレクトリがないときは空文字列になります。
out_dirとin_dirは同じ場所を指定でき、
同バッファで連続処理ができます。

fill_pathname_parent_dir_name(out_dir, in_dir, size)
in_dirの親ディレクトリ名だけ抽出し、
out_dirからsizeバイトのバッファに書き出します。
in_dirが2文字以内のときは書き出し自体が発生しないことと、
in_dirに / が1つもないときはin_dirの内容そのまま
書き出されることに注意。
また、in_dirが絶対pathのときは先頭の / を除去する趣旨で
1文字飛ばして書き出しますが、Windows等では変な挙動に
なるケースあることにも注意。
out_dirとin_dirは同じ場所を指定できます。

fill_pathname_resolve_relative(out_path,  in_refpath, in_path, size)
最後尾を / とするin_refpathと最後尾を / とするin_pathを繋げ、
その絶対pathをシンボリックリンク解決せずに
out_pathからsizeバイトのバッファに書き出します。
out_pathとin_refpathは同じ場所を指定でき、
同バッファで連続処理ができます。

fill_pathname_slash(path, size)
pathの最後尾に / を配置します。
(注) pathに / がないときはsizeをバッファサイズとして
  保護されますが、path途中に / があって
  バッファめいっぱいの最後尾が / ではないとき、
  バッファ終端位置に / が書き込まれることになり
  不具合の原因となり得ます。

fill_pathname_specific_boot_name(out_dir, in_dir, rootpath, gamepath, size, mkdir)
(NullPopPoCustom追加)
想定として rootpathには /userdata/roms/ を指し、
gamepathはゲームファイルを指します。
そうすることでin_dirにシステム名~中間フォルダ名~ゲームファイル名
の部分を接続し、これをout_dirからsizeバイトのバッファに書き出します。
mkdirがtrueのときは、out_dirに書き出すpathを含むディレクトリを
自動的に作成します。

fill_pathname_specific_folder_name(out_dir, in_dir, rootpath,  gamepath, size, mkdir)
(NullPopPoCustom追加)
想定として rootpathには /userdata/roms/ を指し、
gamepathはゲームファイルを指します。
そうすることでin_dirにシステム名~中間フォルダ名
の部分を接続し、これをout_dirからsizeバイトのバッファに書き出します。
mkdirがtrueのときは、out_dirに書き出すpathに相当するディレクトリを
自動的に作成します。

fill_pathname_subdir(in_dir, in_basename, replace, size)
(NullPopPoCustom追加)
in_dirで指定したディレクトリから
/ + in_basename + replaceを接続し、
(replaceってあるけどreplaceじゃない点に注意)
in_dirからsizeバイトのバッファに書き戻し、
結果文字列のバイト数を返します。
(fill_pathname_dir() から path_basename() 呼び出しを省略しただけ)

fill_str_dated_filename(out_filename, in_str, ext, size)
in_str + - + %y%m%d-%H%M%S 形式の現在日時 + extあれば . + extを
out_filenameからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。
out_filenameとin_strは同じ場所を指定できます。

fill_str_filenamed_date(out_filename, in_str, ext, size)
(NullPopPoCustom追加)
%y%m%d-%H%M%S 形式の現在日時 + - + in_str + extあれば . + extを
out_filenameからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。
out_filenameとin_strは同じ場所を指定できます。
(ファイル名順に並べたとき日時で並ぶ趣旨)

find_last_slash(str)
最後の / または \ 位置を返します。
どちらもないときはNULLを返します。

get_pathname_num_slashes(in_path)
in_pathに含まれる / と \ の個数を返します。

path_basename(path)
圧縮書庫については、区切りの # 部分まで除外した位置を返します。
通常のファイルについては、ディレクトリ部分を / まで除外した
位置を返します。
# も / もないpathは、その先頭位置を返します。

path_basename_nocompression(path)
ディレクトリ部分を / まで除外した位置を返します。
/ のないpathは、その先頭位置を返します。

path_basedir(path)
pathのディレクトリ部分を抽出するものですが
最後尾に / がないディレクトリは認識されず
その親ディレクトリで抽出されます。
/ が1つもないpathは ./ が書き出されます。
1文字以内のpathは処理しないことに注意。

path_basedir_wrapper(path)
圧縮書庫ファイルまたはpathを含む / 付きディレクトリを
抽出してpathに書き戻します。
# も / も含まないpathは ./ が書き戻されます。

path_get_archive_delim(path)
圧縮書庫内のファイルを指すpathに対し、
区切りとなる # の位置を返します。
pathに # が複数含まれるときは、先頭優先で
直前が .zip .7z .apk のときだけ採用されます。

path_get_extension(path)
. を除外した拡張子部分の位置を返します。
. が複数あるときは後方が採用されます。
該当しないときは空文字列のポインタを返します。
(こちらは読み取り専用)

path_get_extension_mutable(path)
. を含む拡張子部分の位置を返します。
. が複数あるときは後方が採用されます。
該当しないときはNULLを返します。
(こちらは書き込み可な前提)

path_is_absolute(path)
pathが絶対pathかを調べ、boolで返します。

path_is_compressed_file(path)
拡張子が .zip .7z .apk かを調べ、boolで返します。

path_parent_dir(path, len)
pathの親ディレクトリを抽出します。
親ディレクトリがないときは空文字列になります。

path_remove_extension(path)
拡張子部分がなければNULLを返します。
あれば . 位置に0を書き込み、pathを返します。

path_relative_to(out, path, base, size)
pathをbaseからの相対pathに変換し、
outからsizeバイトのバッファに書き込み、
結果文字列のバイト数を返します。
outは独立したバッファであることが前提となります。

path_resolve_realpath(buf, size, resolve_symlinks)
bufの絶対pathをbufからsizeバイトのバッファに書き戻します。
resolve_symlinks=trueのときはシンボリックリンクが展開されます。

pathname_conform_slashes_to_os(path)
ディレクトリ区切りの / と \ をOS指定の文字に統一します。

pathname_make_slashes_portable(path)
Windows向けディレクトリ区切りの \ を / に統一します。

trim_tail_slash(str)
(NullPopPoCustom追加)
srtの最後尾が / のときは除去します。

libretro-common/file/file_path_io.c

path_get_size(path)
ファイルサイズを調べ、31bit値で返します。
ファイルがないときは-1を返します。
2GB以上のファイルは正常値を返さないことに注意。

path_is_character_special(path)
pathが文字型特殊デバイスかを調べ、boolで返します。

path_is_directory(path)
pathがディレクトリかを調べ、boolで返します。

path_is_valid(path)
pathが有効かを調べ、boolで返します。

path_mkdir(path)
pathに相当するディレクトリを作成します。
親ディレクトリがないときも自動的に作成されます。

path_parent_mkdir(dir)
(NullPopPoCustom追加)
dirの親ディレクトリまでを作成します。
(素のCで毎度個別に親ディレクトリ文字列作るのめんどいんだもん)

ハック済ソース

以上、NullPopPoCustom38向けの作業ブランチ

今回の成果

今回改めて関連ファンクションを本気で読んでみたところ、
今になって発覚した問題点もいろいろでてきました。
これらは今後の改善対象ということで。


この記事が気に入ったらサポートをしてみませんか?