Skip to main content

skyline_web/
lib.rs

1#![feature(new_uninit)]
2#![feature(new_zeroed_alloc)]
3
4use std::collections::HashMap;
5use std::ffi::CStr;
6use std::fs;
7use std::num::NonZeroU32;
8use std::path::Path;
9use std::str::Utf8Error;
10
11use skyline::info::get_program_id;
12
13use nnsdk::web::offlinewebsession::*;
14use nnsdk::web::*;
15use skyline::nn::os::SystemEventType;
16
17pub use nnsdk::web::{
18    offlinewebsession::OfflineWebSession, OfflineBackgroundKind as Background,
19    OfflineBootDisplayKind as BootDisplay, WebSessionBootMode as Visibility,
20};
21
22mod session;
23pub use session::WebSession;
24
25pub struct PageResult {
26    ret: Box<OfflineHtmlPageReturnValue>,
27}
28
29impl PageResult {
30    pub fn new() -> Self {
31        let mut ret;
32        unsafe {
33            ret = Box::<OfflineHtmlPageReturnValue>::new_zeroed().assume_init();
34
35            OfflineHtmlPageReturnValue(ret.as_mut());
36        }
37
38        PageResult { ret }
39    }
40
41    pub fn get_last_url(&self) -> Result<&str, Utf8Error> {
42        unsafe {
43            let last_url = GetLastUrl(self.ret.as_ref());
44            CStr::from_ptr(last_url as _).to_str()
45        }
46    }
47
48    pub fn get_exit_reason(&self) -> OfflineExitReason {
49        self.ret.get_exit_reason()
50    }
51}
52
53impl AsRef<OfflineHtmlPageReturnValue> for PageResult {
54    fn as_ref(&self) -> &OfflineHtmlPageReturnValue {
55        &self.ret
56    }
57}
58
59impl AsMut<OfflineHtmlPageReturnValue> for PageResult {
60    fn as_mut(&mut self) -> &mut OfflineHtmlPageReturnValue {
61        &mut self.ret
62    }
63}
64
65pub struct Webpage<'a> {
66    files: HashMap<&'a str, &'a [u8]>,
67    dir: Option<&'a Path>,
68    show: Option<&'a str>,
69    htdocs_dir: Option<&'a str>,
70    background: OfflineBackgroundKind,
71    boot_display: OfflineBootDisplayKind,
72    javascript: bool,
73    footer: bool,
74    pointer: bool,
75    boot_icon: bool,
76    web_audio: bool,
77}
78
79impl<'a> Default for Webpage<'a> {
80    fn default() -> Self {
81        Self {
82            files: HashMap::new(),
83            dir: None,
84            show: None,
85            htdocs_dir: None,
86            background: OfflineBackgroundKind::Default,
87            boot_display: OfflineBootDisplayKind::Default,
88            javascript: true,
89            footer: false,
90            pointer: false,
91            boot_icon: false,
92            web_audio: true,
93        }
94    }
95}
96
97#[repr(transparent)]
98#[derive(Debug, Copy, Clone)]
99pub struct OsError(NonZeroU32);
100
101impl<'a> Webpage<'a> {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Add a single file to the context of the webpage
107    pub fn file<S, D>(&mut self, name: &'a S, data: &'a D) -> &mut Self
108    where
109        S: AsRef<str> + ?Sized + 'a,
110        D: AsRef<[u8]> + ?Sized + 'a,
111    {
112        self.files.insert(name.as_ref(), data.as_ref());
113
114        self
115    }
116
117    pub fn files<Str, Data, Arr>(&mut self, files: &'a Arr) -> &mut Self
118    where
119        Str: AsRef<str> + Sized + 'a,
120        Data: AsRef<[u8]> + Sized + 'a,
121        Arr: AsRef<[(Str, Data)]> + ?Sized + 'a,
122    {
123        for (name, data) in files.as_ref().into_iter() {
124            self.files.insert(name.as_ref(), data.as_ref());
125        }
126
127        self
128    }
129
130    pub fn with_dir<P>(&mut self, dir_path: &'a P) -> &mut Self
131    where
132        P: AsRef<Path> + ?Sized + 'a,
133    {
134        self.dir = Some(dir_path.as_ref());
135
136        self
137    }
138
139    pub fn background(&mut self, bg: OfflineBackgroundKind) -> &mut Self {
140        self.background = bg;
141
142        self
143    }
144
145    pub fn boot_display(&mut self, boot: OfflineBootDisplayKind) -> &mut Self {
146        self.boot_display = boot;
147
148        self
149    }
150
151    pub fn javascript(&mut self, js: bool) -> &mut Self {
152        self.javascript = js;
153
154        self
155    }
156
157    pub fn footer(&mut self, footer: bool) -> &mut Self {
158        self.footer = footer;
159
160        self
161    }
162
163    pub fn pointer(&mut self, pointer: bool) -> &mut Self {
164        self.pointer = pointer;
165
166        self
167    }
168
169    pub fn boot_icon(&mut self, boot_icon: bool) -> &mut Self {
170        self.boot_icon = boot_icon;
171
172        self
173    }
174
175    pub fn web_audio(&mut self, audio: bool) -> &mut Self {
176        self.web_audio = audio;
177
178        self
179    }
180
181    pub fn start_page<S>(&mut self, path: &'a S) -> &mut Self
182    where
183        S: AsRef<str> + ?Sized + 'a,
184    {
185        self.show = Some(path.as_ref());
186
187        self
188    }
189
190    pub fn htdocs_dir<S>(&mut self, path: &'a S) -> &mut Self
191    where
192        S: AsRef<str> + ?Sized + 'a,
193    {
194        self.htdocs_dir = Some(path.as_ref());
195
196        self
197    }
198
199    fn into_page_args(&mut self) -> Result<Box<ShowOfflineHtmlPageArg>, OsError> {
200        let program_id = get_program_id();
201
202        let htdocs_dir = self.htdocs_dir.unwrap_or("temp");
203
204        let folder_path = Path::new("sd:/atmosphere/contents")
205            .join(&format!("{:016X}", program_id))
206            .join(&format!("manual_html/html-document/{}.htdocs/", htdocs_dir));
207
208        if let Some(dir) = self.dir {
209            // Copy dir to temp.htdocs
210        } else if !folder_path.exists() {
211            let _ = fs::create_dir_all(&folder_path);
212        }
213
214        for (path, data) in self.files.iter() {
215            let file_path = folder_path.join(path);
216            let file_parent = file_path.parent().unwrap();
217            if !file_parent.exists() {
218                fs::create_dir_all(file_parent).unwrap();
219            }
220            fs::write(file_path, data).unwrap();
221        }
222
223        let mut args = new_boxed_html_page_arg(format!(
224            "{}.htdocs/{}",
225            htdocs_dir,
226            self.show.unwrap_or("index.html")
227        ));
228
229        args.set_background_kind(self.background);
230        args.set_boot_display_kind(self.boot_display);
231        args.enable_javascript(self.javascript);
232        args.display_footer(self.footer);
233        args.enable_pointer(self.pointer);
234        args.enable_boot_loading_icon(self.boot_icon);
235        args.enable_web_audio(self.web_audio);
236
237        Ok(args)
238    }
239
240    pub fn open_session(&mut self, boot_mode: Visibility) -> Result<WebSession, OsError> {
241        self.javascript(true);
242
243        let mut args = self.into_page_args()?;
244        args.set_boot_mode(boot_mode);
245
246        let session = OfflineWebSession::new();
247
248        let placeholder = SystemEventType { _unused: [0; 0x29] };
249        let mut message_event: &SystemEventType = &placeholder;
250
251        unsafe {
252            Start(&session, &mut message_event, &args);
253        }
254
255        Ok(WebSession(session))
256    }
257
258    pub fn open(&mut self) -> Result<PageResult, OsError> {
259        let mut args = self.into_page_args().unwrap();
260        let mut page_result = PageResult::new();
261
262        let result = unsafe { ShowOfflineHtmlPage(page_result.as_mut(), args.as_mut()) };
263
264        match result {
265            0 => Ok(page_result),
266            err => Err(OsError(NonZeroU32::new(err).unwrap())),
267        }
268    }
269}
270
271fn new_boxed_html_page_arg<T>(page_path: T) -> Box<ShowOfflineHtmlPageArg>
272where
273    T: AsRef<[u8]>,
274{
275    let mut path_bytes = page_path.as_ref().to_vec();
276
277    if path_bytes.len() > 3072 {
278        path_bytes.truncate(3071);
279    }
280
281    path_bytes.push(b'\0');
282
283    unsafe {
284        let mut instance = Box::<ShowOfflineHtmlPageArg>::new_zeroed().assume_init();
285        ShowOfflineHtmlPageArg(instance.as_mut(), path_bytes.as_ptr());
286        instance
287    }
288}
289
290pub mod dialog;
291pub mod dialog_ok;
292pub extern crate ramhorns;
293pub use ramhorns::*;