unit game_mount;

{$mode ObjFPC}{$H+}

interface

uses
 game_info,
 kern_mtx;

type
 TGameMountConfig=class
  LocalDir:RawByteString;
  TitleId :String[10];
  //
  mount_mtx:mtx;
  //
  TemporaryMount:Boolean;
  //
  DownloadKb:array[0..1] of QWORD;
  //
  Constructor Create;
  function GetTemporaryTitleIdFile:RawByteString;
  function GetAppTemporaryFolder:RawByteString;
  function GetAppDownloadFolder(i:Byte):RawByteString;
 end;

var
 GameMountConfig:TGameMountConfig;

procedure InitMount(GameStartupInfo:TGameStartupInfo);

//

function TemporaryDataMount  (mountPoint:pchar;format:Boolean):Integer;
function TemporaryDataUnmount(mountPoint:pchar):Integer;
function TemporaryDataFormat (mountPoint:pchar):Integer;
function TemporaryDataGetAvailableSpaceKb(mountPoint:pchar;availableSpaceKb:PQWORD):Integer;
function DownloadDataGetAvailableSpaceKb (mountPoint:pchar;availableSpaceKb:PQWORD):Integer;

implementation

uses
 sysutils,
 fileutil,
 LazFileUtils,
 strings,
 errno,
 kern_proc,
 vfs_mountroot,
 subr_backtrace;

function unix_to_host(const name:RawByteString):RawByteString;
var
 i:Integer;
begin
 Result:=name;
 if (DirectorySeparator<>'/') then
 For i:=1 to Length(Result) do
 begin
  if (Result[i]='/') then
  begin
   Result[i]:=DirectorySeparator;
  end;
 end;
end;

Constructor TGameMountConfig.Create;
begin
 LocalDir:='';
 TitleId :='?????????'#0;
 //
 mtx_init(mount_mtx,'mount_mtx');
end;

function TGameMountConfig.GetTemporaryTitleIdFile:RawByteString;
const
 TEMP_FILE='/system_data/game/tempdata.dat';
begin
 Result:=ExcludeTrailingPathDelimiter(LocalDir)+unix_to_host(TEMP_FILE);
end;

function TGameMountConfig.GetAppTemporaryFolder:RawByteString;
const
 APP_TEMP ='/app_tmp/';
begin
 Result:=ExcludeTrailingPathDelimiter(LocalDir)+unix_to_host(APP_TEMP);
end;

function TGameMountConfig.GetAppDownloadFolder(i:Byte):RawByteString;
const
 APP_DOWNLOAD='%s/user/download/%s/download%d.dat'; // On PS4 these are files, on the emulator, folders
begin
 Result:=Format(unix_to_host(APP_DOWNLOAD),[
  ExcludeTrailingPathDelimiter(LocalDir),
  TitleId,
  i
 ]);
end;

function get_errno_str(err:Integer):RawByteString;
begin
 case err of
  EPERM  :Result:='Operation not permitted';
  ENOENT :Result:='No such file or directory';
  EACCES :Result:='Permission denied';
  EBUSY  :Result:='Directory is busy';
  EEXIST :Result:='Directory exists';
  ENOTDIR:Result:='Not a directory';
  else
          Result:=IntToStr(err);
 end;
end;

function mount_mkdir(path:PChar):Integer;
begin
 Result:=vfs_mountroot.mount_mkdir(path);
 if (Result<>0) then
 begin
  print_error_td('[mkdir error]'+#13#10+
                 ' path:"'+path+'"'#13#10+
                 '  err:'+get_errno_str(Result)
                ,True);
 end;
end;

function mount_into_sandbox(fstype,fspath,from,opts:PChar;flags:QWORD;ignore:Boolean):Integer;
begin
 Result:=vfs_mountroot.mount_into_sandbox(fstype,fspath,from,opts,flags);
 if (Result<>0) and (not ignore) then
 begin
  print_error_td('[mount error]'+#13#10+
                 ' from:"'+from+'"'#13#10+
                 '   to:"'+fspath+'"'#13#10+
                 '  err:'+get_errno_str(Result)
                ,True);
 end;
end;

const
 MM_CREATE  =-1;
 MM_GAME    =0;
 MM_FIRMWARE=1;
 MM_LOCAL   =2;
 MM_LAST    =2;

type
 t_mnt_flags=Set of (mfReadOnly,mfIgnoreErr,mfForceDir,mfPFS);

type
 pp_mount_dir=^p_mount_dir;
 p_mount_dir=^t_mount_dir;
 t_mount_dir=object
  dst   :pchar;
  src   :pchar;
  mode  :Shortint;
  flags :t_mnt_flags;
  term  :Boolean;
  childs:p_mount_dir;
 end;

 t_mount_dir_iterator=object
  _curr:pp_mount_dir;
  stack:array[0..3] of p_mount_dir;
  function  curr:p_mount_dir;          inline;
  procedure init(dir:p_mount_dir);     inline;
  function  next(err:Integer):Boolean; inline;
 end;

function t_mount_dir_iterator.curr:p_mount_dir; inline;
begin
 Result:=_curr^;
end;

procedure t_mount_dir_iterator.init(dir:p_mount_dir); inline;
begin
 stack[0]:=dir;
 _curr:=@stack[0];
end;

function t_mount_dir_iterator.next(err:Integer):Boolean; inline;
var
 prev:p_mount_dir;
begin
 if (_curr=nil) then Exit(False);

 prev:=_curr^;

 if (err=0) and (prev^.childs<>nil) then
 begin
  _curr:=_curr+1; //down
  _curr^:=prev^.childs;
 end else
 begin
  _curr^:=prev+1; //next
 end;

 repeat

  if (_curr^^.term) then
  begin
   if (_curr=@stack[0]) then
   begin
    _curr:=nil;
    Exit(False);
   end else
   begin
    _curr :=_curr -1; //up
    _curr^:=_curr^+1; //next
   end;
  end else
  begin
   Exit(True);
  end;

 until false;

end;

const
 SYSTEM_COMMON_DIRS:array[0..12] of t_mount_dir=(
  (dst:'/%s/common/cert'          ;src:'%s/system/common/cert'          ;mode:MM_FIRMWARE;flags:[mfReadOnly]),             // CA_LIST.cer
  (dst:'/%s/common/etc'           ;src:'%s/system/common/etc'           ;mode:MM_FIRMWARE;flags:[mfReadOnly,mfIgnoreErr]),
  (dst:'/%s/common/font'          ;src:'%s/preinst/common/font'         ;mode:MM_FIRMWARE;flags:[mfReadOnly]),             // *.ttf
  (dst:'/%s/common/font2'         ;src:'%s/system/common/font2'         ;mode:MM_FIRMWARE;flags:[mfReadOnly]),
  (dst:'/%s/common/httpcache'     ;src:'%s/system_data/common/httpcache';mode:MM_LOCAL   ;flags:[mfForceDir]),
  (dst:'/%s/common/lib'           ;src:'%s/system/common/lib'           ;mode:MM_FIRMWARE;flags:[mfReadOnly]),
  (dst:'/%s/common/mms'           ;src:'%s/system_data/common/mms/'     ;mode:MM_LOCAL   ;flags:[mfForceDir]),             // av_content.db
  (dst:'/%s/common/mms_ro'        ;src:'%s/system/common/mms_ro'        ;mode:MM_FIRMWARE;flags:[mfReadOnly,mfIgnoreErr]), // template_content.db
  (dst:'/%s/common/playready'     ;src:'%s/user/common/playready'       ;mode:MM_LOCAL   ;flags:[mfReadOnly,mfForceDir ]),
  (dst:'/%s/common/text_layout'   ;src:'%s/system/common/text_layout'   ;mode:MM_FIRMWARE;flags:[mfReadOnly,mfIgnoreErr]),
  (dst:'/%s/common/text_to_speech';src:'%s/system/common/text_to_speech';mode:MM_FIRMWARE;flags:[mfReadOnly,mfIgnoreErr]),
  (dst:'/%s/common/webkit'        ;src:'%s/system/common/webkit'        ;mode:MM_FIRMWARE;flags:[mfReadOnly,mfIgnoreErr]),
  (term:True)
 );

 SYSTEM_DIRS:array[0..5] of t_mount_dir=(
  (dst:'/%s/becore'     ;src:''              ;mode:MM_CREATE  ;flags:[]),           // system app only
  (dst:'/%s/common'     ;src:''              ;mode:MM_CREATE  ;flags:[];childs:@SYSTEM_COMMON_DIRS),
  (dst:'/%s/common_temp';src:''              ;mode:MM_CREATE  ;flags:[]),
  (dst:'/%s/priv'       ;src:'%s/system/priv';mode:MM_FIRMWARE;flags:[mfReadOnly]), // system app only
  (dst:'/%s/sqlite'     ;src:''              ;mode:MM_CREATE  ;flags:[]),
  (term:True)
 );

 SANDBOX_DIRS:array[0..7] of t_mount_dir=(
  (dst:'/app0'       ;src:'%s'                 ;mode:MM_GAME  ;flags:[mfReadOnly,mfPFS]),
  (dst:'/av_contents';src:'%s/user/av_contents';mode:MM_LOCAL ;flags:[mfForceDir]),
  (dst:'/data'       ;src:'%s/user/data'       ;mode:MM_LOCAL ;flags:[mfForceDir]),
  (dst:'/host'       ;src:''                   ;mode:MM_CREATE;flags:[mfReadOnly]),
  (dst:'/hostapp'    ;src:''                   ;mode:MM_CREATE;flags:[mfReadOnly]),
  (dst:'/system_tmp' ;src:'%s/system_tmp'      ;mode:MM_LOCAL ;flags:[mfForceDir]),
  (dst:'/%s'         ;src:''                   ;mode:MM_CREATE;flags:[mfReadOnly];childs:@SYSTEM_DIRS),
  (term:True)
 );

 DOWNLOAD_DIRS:array[0..1] of pchar=(
  '/download0',
  '/download1'
 );

procedure InitMount(GameStartupInfo:TGameStartupInfo);
var
 i,err:Integer;

 fs_iterator:t_mount_dir_iterator;

 fs_source:array[0..MM_LAST] of RawByteString;
 fs_dst:RawByteString;
 fs_src:RawByteString;
begin

 with GameStartupInfo.FGameItem.GameInfo do
 begin
  if (TitleId='') then
  begin
   TitleId:='?????????';
  end;
 end;

 //save to global
 GameMountConfig:=TGameMountConfig.Create;
 GameMountConfig.LocalDir:=GameStartupInfo.LocalDir;
 GameMountConfig.TitleId :=GameStartupInfo.FGameItem.GameInfo.TitleId;

 GameMountConfig.DownloadKb[0]:=GameStartupInfo.DownloadMb_0*1024;
 GameMountConfig.DownloadKb[1]:=GameStartupInfo.DownloadMb_1*1024;
 //save to global

 //temp hack
 err:=mount_into_sandbox('ufs','/savedata0','savedata',nil,0,True);

 fs_source[MM_GAME    ]:=ExcludeTrailingPathDelimiter(GameStartupInfo.FGameItem.FMountList.game    );
 fs_source[MM_FIRMWARE]:=ExcludeTrailingPathDelimiter(GameStartupInfo.FGameItem.FMountList.firmware);
 fs_source[MM_LOCAL   ]:=ExcludeTrailingPathDelimiter(GameStartupInfo.LocalDir);

 //--sandbox--
 fs_iterator.init(@SANDBOX_DIRS);
 repeat

  with fs_iterator.curr^ do
  begin

   fs_dst:=Format(dst,[p_proc.p_randomized_path]);

   if (mode=MM_CREATE) then
   begin
    err:=mount_mkdir(pchar(fs_dst));
   end else
   begin
    fs_src:=Format(unix_to_host(src),[fs_source[mode]]);

    if (mfForceDir in flags) then
    begin
     ForceDirectories(fs_src);
    end;

    err:=mount_into_sandbox('ufs',
                            pchar(fs_dst),
                            pchar(fs_src),
                            nil,
                            ord(mfReadOnly in flags)*MNT_RDONLY or
                            ord(mfPFS      in flags)*MNT_EMU_PFS,
                            mfIgnoreErr in flags);
   end;

  end;

 until (not fs_iterator.next(err));
 //--sandbox--

 //download
 For i:=0 to High(GameMountConfig.DownloadKb) do
 if (GameMountConfig.DownloadKb[i]<>0) then
 begin
  fs_src:=GameMountConfig.GetAppDownloadFolder(i);
  ForceDirectories(fs_src);

  err:=mount_into_sandbox('ufs',
                          DOWNLOAD_DIRS[i],
                          pchar(fs_src),
                          nil,
                          MNT_EMU_PFS,
                          False);
 end;
 //download

 //UPDATE: sandbox root IS NOT read-only
 //err:=vfs_mount_path('ufs','/','/',nil,MNT_RDONLY or MNT_UPDATE);

end;

const
 DIRNAME_MAXSIZE=32;
 MOUNT_MAXSIZE  =16;

 TEMP0:pchar='/temp0';

Function ReadTemporaryTitleId:RawByteString;
var
 fs_src:RawByteString;
 F:THandle;
 s:Integer;
begin
 fs_src:=GameMountConfig.GetTemporaryTitleIdFile;

 F:=FileOpen(fs_src,fmOpenRead);
 if (F=THandle(-1)) then Exit('');

 s:=FileSeek(F,0,fsFromEnd);
 if (s>9) then s:=9;

 if (s>0) then
 begin
  SetLength(Result,s);
  FillChar(pchar(Result)^,s,#0);
  FileSeek(F,0,fsFromBeginning);
  FileRead(F,pchar(Result)^,s);
  s:=strlen(pchar(Result));
  SetLength(Result,s);
 end;

 FileClose(F);
end;

procedure SaveTemporaryTitleId(const TitleId:RawByteString);
var
 fs_src:RawByteString;
 fs_dir:RawByteString;
 F:THandle;
begin
 fs_src:=GameMountConfig.GetTemporaryTitleIdFile;
 fs_dir:=ExtractFilePath(fs_src);
 ForceDirectories(fs_dir);

 F:=FileCreate(fs_src);
 if (F=THandle(-1)) then Exit;

 FileWrite(F,pchar(TitleId)^,Length(TitleId)+1);

 FileClose(F);
end;

function DeleteDirectory(const DirectoryName: RawByteString; OnlyChildren: boolean): boolean;
type
 PNode=^TNode;
 TNode=record
  N:PNode;
  S:RawByteString;
  B:Boolean;
 end;

var
 stack:PNode;

 procedure Push(const S:RawByteString;B:Boolean);
 var
  new:PNode;
 begin
  new:=GetMem(SizeOf(TNode));
  Initialize(new^);
  new^.N:=stack;
  new^.S:=S;
  new^.B:=B;
  stack:=new;
 end;

 Function Pop(var S:RawByteString;var B:Boolean):Boolean;
 var
  old:PNode;
 begin
  if (stack<>nil) then
  begin
   old:=stack;
   stack:=old^.N;
   S:=old^.S;
   B:=old^.B;
   Finalize(old^);
   FreeMem(old);
   Result:=True;
  end else
  begin
   Result:=False;
  end;
 end;

const
  //Don't follow symlinks on *nix, just delete them
  DeleteMask = faAnyFile {$ifdef unix} or faSymLink{%H-} {$endif unix};
var
  FileInfo: TSearchRec;
  CurSrcDir: RawByteString;
  CurFilename: RawByteString;
label
  _next;
begin
  Result:=false;
  CurSrcDir:=CleanAndExpandDirectory(DirectoryName);
  stack:=nil;
_next:
  if SysUtils.FindFirst(CurSrcDir+GetAllFilesMask,DeleteMask,FileInfo)=0 then
  begin
   repeat
     // check if special file
     if (FileInfo.Name='.') or (FileInfo.Name='..') or (FileInfo.Name='') then
     begin
       continue;
     end;
     CurFilename:=CurSrcDir+FileInfo.Name;
     if ((FileInfo.Attr and faDirectory)>0)
        {$ifdef unix} and ((FileInfo.Attr and faSymLink{%H-})=0) {$endif unix} then
     begin
       Push(CurSrcDir,OnlyChildren);
       CurSrcDir:=CleanAndExpandDirectory(CurFilename);
       OnlyChildren:=False;
       SysUtils.FindClose(FileInfo);
       goto _next;
     end else
     begin
       if not SysUtils.DeleteFile(CurFilename) then exit;
     end;
   until SysUtils.FindNext(FileInfo)<>0;
   SysUtils.FindClose(FileInfo);
  end;

  if (not OnlyChildren) then
  begin
   if (not SysUtils.RemoveDir(CurSrcDir)) then exit;
  end;

  if Pop(CurSrcDir,OnlyChildren) then
  begin
   goto _next;
  end;

  Result:=true;
end;

function GetDirectorySizeLikePFS(const DirectoryName:RawByteString):Int64;
type
 PNode=^TNode;
 TNode=record
  N:PNode;
  S:RawByteString;
  F:TSearchRec;
 end;

var
 stack:PNode;

 procedure Push(const S:RawByteString;const F:TSearchRec);
 var
  new:PNode;
 begin
  new:=GetMem(SizeOf(TNode));
  Initialize(new^);
  new^.N:=stack;
  new^.S:=S;
  new^.F:=F;
  stack:=new;
 end;

 Function Pop(var S:RawByteString;var F:TSearchRec):Boolean;
 var
  old:PNode;
 begin
  if (stack<>nil) then
  begin
   old:=stack;
   stack:=old^.N;
   S:=old^.S;
   F:=old^.F;
   Finalize(old^);
   FreeMem(old);
   Result:=True;
  end else
  begin
   Result:=False;
  end;
 end;

 function AlignUp(addr:Int64;alignment:Int64):Int64; inline;
 var
  tmp:Int64;
 begin
  if (alignment=0) then Exit(addr);
  tmp:=addr+Int64(alignment-1);
  Result:=tmp-(tmp mod alignment);
 end;

const
  //Don't follow symlinks on *nix, just delete them
  FindMask = faAnyFile {$ifdef unix} or faSymLink{%H-} {$endif unix};
var
  files_size :Int64;
  inode_count:Int64;
  dirent_size:Int64;
  //
  FileInfo: TSearchRec;
  CurSrcDir: RawByteString;
  CurFilename: RawByteString;
const
  c_block_size =4*1024; //????
  c_dirent_size=16;
  c_inode_size =168;
  c_inodes_per_block=c_block_size div c_inode_size;
label
  _down,
  _next;
begin
  Result:=0;
  files_size :=0;
  inode_count:=0;
  dirent_size:=0;
  CurSrcDir:=CleanAndExpandDirectory(DirectoryName);
  stack:=nil;
 _down:
  if SysUtils.FindFirst(CurSrcDir+GetAllFilesMask,FindMask,FileInfo)=0 then
  begin
   repeat
     // check if special file
     if (FileInfo.Name='.') or (FileInfo.Name='..') or (FileInfo.Name='') then
     begin
       continue;
     end;
     //
     CurFilename:=CurSrcDir+FileInfo.Name;
     //
     inode_count:=inode_count+1;
     dirent_size:=dirent_size+AlignUp(c_dirent_size+Length(FileInfo.Name)+1,8);
     //
     if ((FileInfo.Attr and faDirectory)>0)
        {$ifdef unix} and ((FileInfo.Attr and faSymLink{%H-})=0) {$endif unix} then
     begin
       Push(CurSrcDir,FileInfo);
       CurSrcDir:=CleanAndExpandDirectory(CurFilename);
       goto _down;
     end else
     begin
       files_size:=files_size+AlignUp(FileInfo.Size,c_block_size);
     end;
     //
     _next:
   until SysUtils.FindNext(FileInfo)<>0;
   SysUtils.FindClose(FileInfo);
  end;

  if Pop(CurSrcDir,FileInfo) then
  begin
   goto _next;
  end;

  //Very approximate size
  Result:=c_block_size+
          files_size+
          AlignUp(dirent_size,c_block_size)+
          AlignUp(inode_count,c_inodes_per_block)*c_block_size;
end;

function FormatMount(const fs_src:RawByteString):Integer;
begin
 //Delete all content in directory
 if DeleteDirectory(fs_src,True) then
 begin
  Result:=0;
 end else
 begin
  Result:=EPERM;
 end;
end;

function TemporaryDataMount(mountPoint:pchar;format:Boolean):Integer;
var
 fs_src:RawByteString;
 ValidTitleId:Boolean;
begin
 mtx_lock(GameMountConfig.mount_mtx);

 if GameMountConfig.TemporaryMount then
 begin
  Result:=EBUSY;
 end else
 begin
  fs_src:=GameMountConfig.GetAppTemporaryFolder;
  ForceDirectories(fs_src);

  Result:=vfs_mountroot.mount_into_sandbox('ufs',
                                           TEMP0,
                                           pchar(fs_src),
                                           nil,
                                           MNT_EMU_PFS);

  if (Result=0) then
  begin
   strlcopy(mountPoint,TEMP0,MOUNT_MAXSIZE);
   GameMountConfig.TemporaryMount:=True;

   ValidTitleId:=(ReadTemporaryTitleId=GameMountConfig.TitleId);

   if format or (not ValidTitleId) then
   begin
    FormatMount(fs_src);
   end;

   if (not ValidTitleId) then
   begin
    SaveTemporaryTitleId(GameMountConfig.TitleId);
   end;

  end;

 end;

 mtx_unlock(GameMountConfig.mount_mtx);
end;

function TemporaryDataUnmount(mountPoint:pchar):Integer;
begin
 if (strlcomp(mountPoint,TEMP0,MOUNT_MAXSIZE)<>0) then Exit(ENOTDIR);

 mtx_lock(GameMountConfig.mount_mtx);

 if GameMountConfig.TemporaryMount then
 begin
  Result:=vfs_mountroot.unmount_from_sandbox(TEMP0,0);

  if (Result=0) then
  begin
   GameMountConfig.TemporaryMount:=False;
  end;

 end else
 begin
  Result:=ENOTDIR;
 end;

 mtx_unlock(GameMountConfig.mount_mtx);
end;

function TemporaryDataFormat(mountPoint:pchar):Integer;
var
 fs_src:RawByteString;
begin
 if (strlcomp(mountPoint,TEMP0,MOUNT_MAXSIZE)<>0) then Exit(ENOTDIR);

 mtx_lock(GameMountConfig.mount_mtx);

 if GameMountConfig.TemporaryMount then
 begin
  fs_src:=GameMountConfig.GetAppTemporaryFolder;

  Result:=FormatMount(fs_src);
 end else
 begin
  Result:=ENOTDIR;
 end;

 mtx_unlock(GameMountConfig.mount_mtx);
end;

function TemporaryDataGetAvailableSpaceKb(mountPoint:pchar;availableSpaceKb:PQWORD):Integer;
const
 MAX_SIZE_KB=1*1024*1024;
var
 size:QWORD;
 fs_src:RawByteString;
begin
 if (strlcomp(mountPoint,TEMP0,MOUNT_MAXSIZE)<>0) then Exit(ENOTDIR);

 mtx_lock(GameMountConfig.mount_mtx);

 if GameMountConfig.TemporaryMount then
 begin
  fs_src:=GameMountConfig.GetAppTemporaryFolder;

  size:=GetDirectorySizeLikePFS(fs_src);
  size:=size div 1024; //to KB

  if (size>MAX_SIZE_KB) then
  begin
   size:=0;
  end else
  begin
   size:=MAX_SIZE_KB-size;
  end;

  availableSpaceKb^:=size;

  Result:=0;
 end else
 begin
  Result:=ENOTDIR;
 end;

 mtx_unlock(GameMountConfig.mount_mtx);
end;

function DownloadDataGetAvailableSpaceKb(mountPoint:pchar;availableSpaceKb:PQWORD):Integer;
var
 i:Byte;
 size:QWORD;
 fs_src:RawByteString;
begin
 if (strlcomp(mountPoint,DOWNLOAD_DIRS[0],MOUNT_MAXSIZE)=0) then
 begin
  i:=0;
 end else
 if (strlcomp(mountPoint,DOWNLOAD_DIRS[1],MOUNT_MAXSIZE)=0) then
 begin
  i:=1;
 end else
 begin
  Exit(ENOTDIR);
 end;

 if (GameMountConfig.DownloadKb[i]=0) then
 begin
  Exit(ENOTDIR);
 end;

 mtx_lock(GameMountConfig.mount_mtx);

  fs_src:=GameMountConfig.GetAppDownloadFolder(i);

  size:=GetDirectorySizeLikePFS(fs_src);
  size:=size div 1024; //to KB

  if (size>GameMountConfig.DownloadKb[i]) then
  begin
   size:=0;
  end else
  begin
   size:=GameMountConfig.DownloadKb[i]-size;
  end;

  availableSpaceKb^:=size;

 mtx_unlock(GameMountConfig.mount_mtx);

 Result:=0;
end;


end.

