Newer
Older
extern crate argon2;

Jeremy Soller
committed
#[macro_use]
extern crate failure;
extern crate pkgutils;
extern crate rand;
extern crate redoxfs;
extern crate syscall;
pub use config::file::FileConfig;
pub use config::package::PackageConfig;

Jeremy Soller
committed
use redoxfs::{Disk, DiskIo, FileSystem};
use termion::input::TermRead;
use pkgutils::{Repo, Package};

Jeremy Soller
committed
collections::BTreeMap,
env,
fs,
io::{self, stderr, Write},
path::Path,
process::{self, Command},
str::FromStr,
sync::mpsc::channel,
time::{SystemTime, UNIX_EPOCH},
thread,
};
pub(crate) type Result<T> = std::result::Result<T, Error>;
const REMOTE: &'static str = "https://static.redox-os.org/pkg";

Jeremy Soller
committed
fn get_target() -> String {
env::var("TARGET").unwrap_or(
option_env!("TARGET").map_or(
"x86_64-unknown-redox".to_string(),
|x| x.to_string()
)
)
}
/// Converts a password to a serialized argon2rs hash, understandable
/// by redox_users. If the password is blank, the hash is blank.
fn hash_password(password: &str) -> Result<String> {
if password != "" {
let salt = format!("{:X}", OsRng.next_u64());
let config = argon2::Config::default();
let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config)?;
Ok(hash)
fn syscall_error(err: syscall::Error) -> io::Error {
io::Error::from_raw_os_error(err.errno)
}
fn unwrap_or_prompt<T: FromStr>(option: Option<T>, context: &mut liner::Context, prompt: &str) -> Result<T> {
match option {
Some(t) => Ok(t),
None => {
let line = context.read_line(
prompt,
None,
&mut liner::BasicCompleter::new(Vec::<String>::new())
)?;
T::from_str(&line).map_err(|_err| err_msg("failed to parse input"))
}
}
}
/// Returns a password collected from the user (plaintext)
fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<String> {
let stdin = io::stdin();
let mut stdin = stdin.lock();
let stdout = io::stdout();
let mut stdout = stdout.lock();
print!("{}", prompt);
let password = stdin.read_passwd(&mut stdout)?;
print!("\n{}", confirm_prompt);
let confirm_password = stdin.read_passwd(&mut stdout)?;
// Note: Actually comparing two Option<String> values
if confirm_password == password {
Ok(password.unwrap_or("".to_string()))
} else {
Err(err_msg("passwords do not match"))
}
}
//TODO: error handling
fn install_packages<S: AsRef<str>>(config: &Config, dest: &str, cookbook: Option<S>) {

Jeremy Soller
committed
let target = &get_target();
repo.add_remote(REMOTE);
if let Some(cookbook) = cookbook {
let status = Command::new("./repo.sh")
.current_dir(cookbook.as_ref())
.args(config.packages.keys())
.spawn()
.unwrap()
.wait()
.unwrap();
if !status.success() {
write!(stderr(), "./repo.sh failed.").unwrap();
process::exit(1);
}
let dest_pkg = format!("{}/pkg", dest);
if ! Path::new(&dest_pkg).exists() {
fs::create_dir(&dest_pkg).unwrap();
}
for (packagename, _package) in &config.packages {
println!("Installing package {}", packagename);
let pkgar_path = format!("{}/{}/repo/{}/{}.pkgar",
env::current_dir().unwrap().to_string_lossy(),
cookbook.as_ref(), target, packagename);
if Path::new(&pkgar_path).exists() {
let public_path = format!("{}/{}/build/id_ed25519.pub.toml",
env::current_dir().unwrap().to_string_lossy(),
cookbook.as_ref());
pkgar::extract(&public_path, &pkgar_path, dest).unwrap();
let head_path = format!("{}/{}.pkgar_head", dest_pkg, packagename);
pkgar::split(&public_path, &pkgar_path, &head_path, Option::<&str>::None).unwrap();
} else {
let path = format!("{}/{}/repo/{}/{}.tar.gz",
env::current_dir().unwrap().to_string_lossy(),
cookbook.as_ref(), target, packagename);
Package::from_path(&path).unwrap().install(dest).unwrap();
}
}
} else {
for (packagename, _package) in &config.packages {
println!("Installing package {}", packagename);
repo.fetch(&packagename).unwrap().install(dest).unwrap();
}
}
}
pub fn install_dir<P: AsRef<Path>, S: AsRef<str>>(config: Config, output_dir: P, cookbook: Option<S>) -> Result<()> {
//let mut context = liner::Context::new();
macro_rules! prompt {
($dst:expr, $def:expr, $($arg:tt)*) => (if config.general.prompt {
Err(io::Error::new(
io::ErrorKind::Other,
"prompt not currently supported"
))
// match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) {
// Ok(res) => if res.is_empty() {
// Ok($def)
// } else {
// Ok(res)
// },
// Err(err) => Err(err)
// }
} else {
Ok($dst.unwrap_or($def))
})
}
let output_dir = output_dir.as_ref();
let output_dir = output_dir.to_owned();
install_packages(&config, output_dir.to_str().unwrap(), cookbook);
for (username, user) in config.users {
// plaintext
let password = if let Some(password) = user.password {
password
} else if config.general.prompt {
prompt_password(
&format!("{}: enter password: ", username),
&format!("{}: confirm password: ", username))?
} else {
String::new()
};
let uid = user.uid.unwrap_or(next_uid);
if uid >= next_uid {
next_uid = uid + 1;
}
let gid = user.gid.unwrap_or(uid);
let name = prompt!(user.name, username.clone(), "{}: name (GECOS) [{}]: ", username, username)?;
let home = prompt!(user.home, format!("/home/{}", username), "{}: home [/home/{}]: ", username, username)?;
let shell = prompt!(user.shell, "/bin/ion".to_string(), "{}: shell [/bin/ion]: ", username)?;
println!("Adding user {}:", username);
println!("\tPassword: {}", password);
println!("\tUID: {}", uid);
println!("\tGID: {}", gid);
println!("\tName: {}", name);
println!("\tHome: {}", home);
println!("\tShell: {}", shell);
FileConfig {
path: home.clone(),
data: String::new(),
symlink: false,
directory: true,
mode: Some(0o0700),
uid: Some(uid),
gid: Some(gid)
}.create(&output_dir)?;
passwd.push_str(&format!("{};{};{};{};file:{};file:{}\n", username, uid, gid, name, home, shell));
shadow.push_str(&format!("{};{}\n", username, password));
FileConfig {
path: "/etc/passwd".to_string(),
data: passwd,
symlink: false,
directory: false,
// Take defaults
mode: None,
uid: None,
gid: None
}.create(&output_dir)?;
if !shadow.is_empty() {
FileConfig {
path: "/etc/shadow".to_string(),
data: shadow,
symlink: false,
directory: false,
mode: Some(0o0600),
uid: Some(0),
gid: Some(0)
}.create(&output_dir)?;
}

Jeremy Soller
committed
pub fn with_redoxfs<D, T, F>(disk: D, password_opt: Option<&[u8]>, callback: F)
-> Result<T> where

Jeremy Soller
committed
D: Disk + Send + 'static,
F: FnOnce(&Path) -> Result<T>
{
let mount_path = if cfg!(target_os = "redox") {
"file/redox_installer"
} else {
"/tmp/redox_installer"
};
if cfg!(not(target_os = "redox")) {
if ! Path::new(mount_path).exists() {
fs::create_dir(mount_path)?;
}
}
let ctime = SystemTime::now().duration_since(UNIX_EPOCH)?;

Jeremy Soller
committed
let fs = FileSystem::create(
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
disk,
password_opt,
ctime.as_secs(),
ctime.subsec_nanos()
).map_err(syscall_error)?;
let (tx, rx) = channel();
let join_handle = thread::spawn(move || {
let res = redoxfs::mount(
fs,
mount_path,
|real_path| {
tx.send(Ok(real_path.to_owned())).unwrap();
}
);
match res {
Ok(()) => (),
Err(err) => {
tx.send(Err(err)).unwrap();
},
};
});
let res = match rx.recv() {
Ok(ok) => match ok {
Ok(real_path) => callback(&real_path),
Err(err) => return Err(err.into()),
},
Err(_) => return Err(io::Error::new(
io::ErrorKind::NotConnected,
"redoxfs thread did not send a result"
).into()),
};
if cfg!(target_os = "redox") {
fs::remove_file(format!(":{}", mount_path))?;
} else {
let status_res = if cfg!(target_os = "linux") {
Command::new("fusermount")
.arg("-u")
.arg(mount_path)
.status()
} else {
Command::new("umount")
.arg(mount_path)
.status()
};
let status = status_res?;
if ! status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
"redoxfs umount failed"
).into());
}
}
join_handle.join().unwrap();
res
}
pub fn fetch_bootloaders<S: AsRef<str>>(cookbook: Option<S>, live: bool) -> Result<(Vec<u8>, Vec<u8>)> {

Jeremy Soller
committed
//TODO: make it safe to run this concurrently
let bootloader_dir = "/tmp/redox_installer_bootloader";
if Path::new(bootloader_dir).exists() {
fs::remove_dir_all(&bootloader_dir)?;
}
fs::create_dir(bootloader_dir)?;
let mut bootloader_config = Config::default();
bootloader_config.packages.insert("bootloader".to_string(), PackageConfig::default());
install_packages(&bootloader_config, bootloader_dir, cookbook.as_ref());
let boot_dir = Path::new(bootloader_dir).join("boot");
let bios_path = boot_dir.join(if live {
"bootloader-live.bios"
} else {
"bootloader.bios"
});
let efi_path = boot_dir.join(if live {
"bootloader-live.efi"
} else {
"bootloader.efi"
});

Jeremy Soller
committed
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
Ok((
if bios_path.exists() {
fs::read(bios_path)?
} else {
Vec::new()
},
if efi_path.exists() {
fs::read(efi_path)?
} else {
Vec::new()
},
))
}
//TODO: make bootloaders use Option, dynamically create BIOS and EFI partitions
pub fn with_whole_disk<P, F, T>(disk_path: P, bootloader_bios: &[u8], bootloader_efi: &[u8], password_opt: Option<&[u8]>, callback: F)
-> Result<T> where
P: AsRef<Path>,
F: FnOnce(&Path) -> Result<T>
{
let target = get_target();
let bootloader_efi_name = match target.as_str() {
"aarch64-unknown-redox" => "BOOTAA64.EFI",
"x86-unknown-redox" => "BOOTIA32.EFI",
"x86_64-unknown-redox" => "BOOTX64.EFI",
_ => {
return Err(format_err!("target '{}' not supported", target));
}
};
// TODO: support other block sizes?
let block_size = 512;
// Write BIOS bootloader to disk, resetting all partitioning
let disk_size = {
// Open disk
let mut disk = fs::OpenOptions::new()
.read(true)
.write(true)
.open(disk_path.as_ref())?;
// Write bootloader data
disk.write(&bootloader_bios)?;
// Get disk size
disk.metadata()?.len()
};
// Open disk, mark it as not initialized
let mut gpt_disk = gpt::GptConfig::new()
.writable(true)
.initialized(false)
.open(disk_path.as_ref())?;
// Calculate partition offsets
let gpt_reserved = 34 * 512; // GPT always reserves 34 512-byte sectors
let bios_start = gpt_reserved / block_size;
let bios_end = (1024 * 1024 / block_size) - 1; // End at 1 MiB
let efi_end = ((disk_size - gpt_reserved) / block_size) - 1;
let efi_start = efi_end - ((1024 * 1024) / block_size); // 1 MiB from end of disk
let redoxfs_start = bios_end + 1;
let redoxfs_end = efi_start - 1;
// Add BIOS boot partition
let mut partitions = BTreeMap::new();
let mut partition_id = 1;
partitions.insert(partition_id, gpt::partition::Partition {
part_type_guid: gpt::partition_types::BIOS,
part_guid: uuid::Uuid::new_v4(),
first_lba: bios_start,
last_lba: bios_end,
flags: 0, // TODO
name: "BIOS".to_string(),
});
partition_id += 1;
// Add RedoxFS partition
partitions.insert(partition_id, gpt::partition::Partition {
//TODO: Use REDOX_REDOXFS type (needs GPT crate changes)
part_type_guid: gpt::partition_types::LINUX_FS,
part_guid: uuid::Uuid::new_v4(),
first_lba: redoxfs_start,
last_lba: redoxfs_end,
flags: 0,
name: "REDOX".to_string(),
});
partition_id += 1;
// Add EFI boot partition
partitions.insert(partition_id, gpt::partition::Partition {
part_type_guid: gpt::partition_types::EFI,
part_guid: uuid::Uuid::new_v4(),
first_lba: efi_start,
last_lba: efi_end,
flags: 0, // TODO
name: "EFI".to_string(),
});
// Initialize GPT table
gpt_disk.update_partitions(partitions)?;
println!("{:#?}", gpt_disk);
// Write partition layout, returning disk file
let mut disk_file = gpt_disk.write()?;
// Replace MBR tables with protective MBR
let mbr_blocks = (disk_size + block_size - 1) / block_size;
gpt::mbr::ProtectiveMBR::with_lb_size(mbr_blocks as u32 - 1)
.update_conservative(&mut disk_file)?;
// Format and install EFI partition
{
let mut disk_efi = fscommon::StreamSlice::new(
&mut disk_file,
efi_start * block_size,
(efi_end + 1) * block_size,
)?;
fatfs::format_volume(&mut disk_efi, fatfs::FormatVolumeOptions::new())?;
let fs = fatfs::FileSystem::new(disk_efi, fatfs::FsOptions::new())?;
let root_dir = fs.root_dir();
root_dir.create_dir("EFI")?;
let efi_dir = root_dir.open_dir("EFI")?;
efi_dir.create_dir("BOOT")?;
let boot_dir = efi_dir.open_dir("BOOT")?;
let mut file = boot_dir.create_file(bootloader_efi_name)?;
file.truncate()?;
file.write_all(&bootloader_efi)?;
}
// Format and install RedoxFS partition
let disk_redoxfs = DiskIo(fscommon::StreamSlice::new(
disk_file,
redoxfs_start * block_size,
(redoxfs_end + 1) * block_size
)?);
with_redoxfs(
disk_redoxfs,
password_opt,
callback
)
}
pub fn install<P, S>(config: Config, output: P, cookbook: Option<S>, live: bool)
-> Result<()> where
P: AsRef<Path>,
S: AsRef<str>,
{
println!("Install {:#?} to {}", config, output.as_ref().display());
if output.as_ref().is_dir() {
install_dir(config, output, cookbook)
} else {
let (bootloader_bios, bootloader_efi) = fetch_bootloaders(cookbook.as_ref(), live)?;

Jeremy Soller
committed
with_whole_disk(output, &bootloader_bios, &bootloader_efi, None,
move |mount_path| {
install_dir(config, mount_path, cookbook)

Jeremy Soller
committed
)